From ccd684c3376d5c637c14992d4d9615e4e233aa37 Mon Sep 17 00:00:00 2001 From: owen Date: Wed, 10 Mar 2021 16:53:31 -0500 Subject: [PATCH] Fixed bugs in build selection, and while determining if an update is necessary. Added some new features for better handling backups, new installs, and getting server/script infomration. --- .gitignore | 4 + README.md | 82 +++++++++++++---- server_update.py | 228 ++++++++++++++++++++++++++++++++--------------- 3 files changed, 225 insertions(+), 89 deletions(-) diff --git a/.gitignore b/.gitignore index 5d9b031..ae238e1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,10 @@ out gen +# VSCode files: + +.vscode/ + # Python Environments .env .venv diff --git a/README.md b/README.md index 6ce0477..f94bfa2 100644 --- a/README.md +++ b/README.md @@ -2,34 +2,46 @@ A simple CLI script for automating the checking, downloading, and installing of PaperMC server updates. NOTE: This script can only be used for updating a [PaperMC Minecraft Server](https://papermc.io/). I highly recommend -their implementation, as it is incredibly fast, supports multiple plugin formats, and is highly customisable. +their implementation, as it is incredibly fast, supports multiple plugin formats, and is highly customizable. Consider changing if any of that sounds good to you. You can find the PaperMC documentation [Here](https://paper.readthedocs.io/en/latest/), and you can find their github page [Here](https://github.com/PaperMC). # Prerequisites -You must have Python 3+ installed. It should come pre-installed on most systems. +You must have Python 3+ installed. It should come pre-installed on most linux systems. This script has no other dependencies, so all you need is python! Instructions for installation are below: ## Linux: RPM: ->yum install python3.7 +>yum install python3.9 Debian: ->apt install python3.7 +>apt install python3.9 -(Or use whatever packet manager is installed on your system) -## MacOS: - ->brew install python3.7 +Or use whatever packet manager is installed on your system. +The only important part is specifying python3.9! ## Windows: -Windows users can download python [Here](https://www.python.org/downloads/). The installation is very straightforward, +Windows users can download python [Here](https://www.python.org/downloads/windows/). The installation is very straightforward, although we recommend you add python to your PATH environment variable, as this makes using python much easier. More information on installing/configuring python can be found [Here](https://www.python.org/downloads/). -(Any python 3 version works, although I recommend python 3.7) +(Any python 3 version works, although I recommend python 3.9) + +We also supply windows binaries that can be ran directly on windows systems without python! +We use [PyInstaller](https://www.pyinstaller.org/) to build these binaries, and they are built using the one file option, or '-F'. + +These binaries are much slower than running via python, and there might be some weird bugs or quirks. +They are built for windows only. We provide one with each version change. +Find them [here](https://github.com/Owen-Cochell/PaperMC-Update/releases) in the releases! + +## MacOS: + +MacOS users can download python [Here](https://www.python.org/downloads/mac-osx/). + +Again, the installation is very straightforward, but more information can be found [Here](https://docs.python.org/3/using/mac.html). + # Usage @@ -49,14 +61,16 @@ information for un-official builds). If no configuration data is found, and vers command line, then the version and build for the currently installed server will default to 0. 2. Check for a new version/build using the [PaperMC download API](https://paper.readthedocs.io/en/latest/site/api.html#downloads-api). -3. If a new version/build is available, the default version and build(usually the latest) will be installed. +3. If a new version/build is available, the default version and build(usually the latest) will be installed. +If you wish to select a version/build to install, then you can use '--version' and '--build' arguments to specify this. Alternatively, the user can be prompted to manually select which version/build they want to be installed. You can use the `--interactive` flag for this. 4. The selected version is downloaded to a temporary directory located somewhere on your computer (This directory is generated using the python tempfile module, meaning that it will be generated in a safe, unobtrusive manner, and will be automatically removed at termination of the script). -5. The currently installed version of the server is backed up to the temporary directory, and deleted. -6. The newly downloaded server is moved from the temporary directory to the path of the old server, +5. The currently installed version of the server is backed up to the temporary directory, and deleted. +This option can be disabled if you so choose. +6. The newly downloaded server jar is moved from the temporary directory to the path of the old server, and will retain the name of the old server(If an error occurs for any reason during the instillation procedure, then the script will attempt to recover your backed up version of the old server from the temporary directory). @@ -91,9 +105,17 @@ Will not load configuration data: >-nlc, --no-load-config Sets config file path(`/PATH_TO_SERVER_JAR/version_history.json` by default): - >-cf, --config-file [PATH TO CONFIG FILE] +Disables the backup feature: +>-nb, --no-backup + +Displays script version information: +>-V, --script-version + +Displays server version, extracted from configuration file: +>-sv, --server-version + Will only output errors and interactive questions to the terminal: >-q, --quiet @@ -120,10 +142,10 @@ Download and install the latest build of version 1.13.2, without checking: >python server_update.py --no-check --version 1.13.2 [PATH] Install latest version, regardless of server version: ->python server_update.py --no-load-config --no-check [PATH] +>python server_update.py --no-check [PATH] Install specific version, regardless of installed version: ->python server_update.py --no-load-config --no-check --version [VERSION TO INSTALL] --build [BUILD TO INSTALL] [PATH] +>python server_update.py --no-check --version [VERSION TO INSTALL] --build [BUILD TO INSTALL] [PATH] Interactively select a version you want to install, regardless of server version: >python server_update.py --no-check --no-load --interactive [PATH] @@ -131,6 +153,13 @@ Interactively select a version you want to install, regardless of server version Check to see if a newer version is available, does not install: >python server_update.py --check-only [PATH] +Display currently installed server version: +>python server_update.py --server-version [PATH] + +Install a paper jar at the given location, without going through the update process. +Great if you want to set up a new server install. +>python server_update.py --new [PATH] + # Notes on Deprecated Features In earlier versions of PaperMC-Update, the script would keep a config file in the users home directory @@ -192,6 +221,27 @@ so they don't have to. # Changelog +## 1.2.0 + + Bug Fixes: + + - Fixed an issue where the script would always determine that an update is necessary due to a type mismatch + - Fixed an issue in the interactive menu when selecting the build where no input would be valid, also a type mismatch + + Features Added: + + - '--no-backup' argument for disabling the backup operation + - '-V' argument for displaying script version and exiting + - '--server-version' argument for displaying the server version and exiting + - '--new' argument for skipping update operations and downloading a paper jar at the given location + + Other Fixes: + + - Added grouping to argparse, so the help menu should feel less cluttered + - Cleaned up the formatting of all docstrings + - Changed the wording in code comments + - Fixed many typos + ## 1.1.0 - Added command line option '-q' for quiet output diff --git a/server_update.py b/server_update.py index cdf753c..e143350 100644 --- a/server_update.py +++ b/server_update.py @@ -15,6 +15,8 @@ > As long as it is LOGGED or DISPLAYED somewhere for the user to see, it has been handled. """ +__version__ = '1.2.0' + def output(text): @@ -33,10 +35,10 @@ def output(text): def error_report(exc, net=False): """ - Function for displaying error information to the terminal + Function for displaying error information to the terminal. + :param exc: Exception object :param net: Whether to include network information - :return: """ print("+==================================================+") @@ -95,16 +97,19 @@ def __init__(self, ver): 'DNT': '1', } # Request headers for contacting Paper Download API, emulating a Google client - def _progress_bar(self, total, step, end, prefix="", size=60, prog_char="#", empty_char="."): + def _progress_bar(self, total, step, end , prefix="", size=60, prog_char="#", empty_char="."): """ - Outputs a simple progress bar to stdout + Outputs a simple progress bar to stdout. + + We act as a generator, continuing to iterate and add to the bar progress + as we download more information. + :param total: Total amount of computations :param step: Amount to increase the counter by :param end: Number to end on :param prefix: What to show before the progress bar :param size: Size of the progress bar - :return: """ # Iterating over the total number of iterations: @@ -137,9 +142,9 @@ def _progress_bar(self, total, step, end, prefix="", size=60, prog_char="#", emp def _url_report(self, point): """ - Reports an error during a request operation + Reports an error during a request operation. + :param point: Point of failure - :return: """ print("\n+==================================================+") @@ -152,8 +157,9 @@ def _url_report(self, point): def download(self, path, version, build_num='latest'): """ - Gets file from Paper API, and displays a progress bar - Write to the file specified in chunks, as to not fill up the memory + Gets file from Paper API, and displays a progress bar. + We write to the file specified in chunks, as to not fill up the memory. + :param version: Version to download :param build_num: Build to download :param path: Path to file to write to @@ -248,7 +254,8 @@ def download(self, path, version, build_num='latest'): def _get(self, version=None, build_num=None): """ - Gets RAW data from the Paper API, version info only + Gets RAW data from the Paper API, version info only. + :param version: Version to include in the URL :param build_num: Build number to include in the URL :return: urllib Request object @@ -295,7 +302,8 @@ def _get(self, version=None, build_num=None): def get_versions(self): """ - Gets available versions of the server + Gets available versions of the server. + :return: List of available versions """ @@ -322,7 +330,8 @@ def get_versions(self): def get_buildnums(self, version): """ - Gets available build for a particular version + Gets available build for a particular version. + :param version: Version to get builds for :return: List of builds """ @@ -361,7 +370,8 @@ def __init__(self, path, config=None): def create_temp_dir(self): """ - Creates a temporary directory + Creates a temporary directory. + :return: Temp file instance """ @@ -372,8 +382,7 @@ def create_temp_dir(self): def close_temp_dir(self): """ - Closes created temporary directory - :return: + Closes created temporary directory. """ self.temp.close() @@ -460,9 +469,9 @@ def load_config(self, config): def _fail_install(self, point): """ - Shows where the error occurred during the installation + Shows where the error occurred during the installation. + :param point: Point of failure - :return: """ print("\n+==================================================+") @@ -474,40 +483,52 @@ def _fail_install(self, point): return - def install(self): + def install(self, backup=True, new=False): """ "Installs" the contents of the temporary file into the target in the root server directory. - :return: + + The new file should exist in the temporary directory before this method is invoked! + + We backup the old jar file by default to the temporary directory, + and we will attempt to recover the old jar file in the event of any failures. + This feature can be disabled. + + :param backup: Value determining if we should back up the old file + :type backup: bool + :param new: Determines if we are doing a new install, aka if we care about file operation errors + :type new: bool """ - output("\n[ --== installation: ==-- ]") + output("\n[ --== Installation: ==-- ]") # Creating backup of old file: - output("# Creating backup of previous installation...") + if backup and not new: - try: + output("# Creating backup of previous installation ...") - shutil.copyfile(self.path, os.path.join(self.temp.name, 'backup')) + try: - except Exception as e: + shutil.copyfile(self.path, os.path.join(self.temp.name, 'backup')) - # Show install error + except Exception as e: - self._fail_install("File Backup") + # Show install error - # Show error info + self._fail_install("File Backup") - error_report(e) + # Show error info - return False + error_report(e) + + return False - output("# Backup created at: {}".format(os.path.join(self.temp.name, 'backup'))) + output("# Backup created at: {}".format(os.path.join(self.temp.name, 'backup'))) # Removing current file: - output("# Deleting current file at {}...".format(self.path)) + output("# Deleting current file at {} ...".format(self.path)) try: @@ -515,17 +536,21 @@ def install(self): except Exception as e: - self._fail_install("Old File Deletion") + if not new: - # Showing error + self._fail_install("Old File Deletion") - error_report(e) + # Showing error - # Recovering backup + error_report(e) - self._recover_backup() + # Recovering backup - return False + if backup: + + self._recover_backup() + + return False output("# Removed original file!") @@ -533,7 +558,7 @@ def install(self): try: - output("# Copying download data to root directory...") + output("# Copying download data to root directory ...") output("# ({} > {})".format(os.path.join(self.temp.name, 'download_data'), self.path)) @@ -551,9 +576,11 @@ def install(self): # Recover backup - self._recover_backup() + if backup and not new: - return False + self._recover_backup() + + return False output("# Done copying download data to root directory!") @@ -565,15 +592,14 @@ def install(self): output("# Done cleaning temporary directory!") - output("[ --== installation complete! ==-- ]") + output("[ --== Installation complete! ==-- ]") return True def _recover_backup(self): """ - Recovers the backup of the old server jar file - :return: + Attempts to recover the backup of the old server jar file. """ print("+==================================================+") @@ -643,7 +669,7 @@ class ServerUpdater: Class that binds all server updater classes together """ - def __init__(self, path, config_file=None, version=None, build=None, config=True, prompt=True): + def __init__(self, path, config_file=None, version='0', build=0, config=True, prompt=True): self.version = version # Version of minecraft server we are running self.fileutil = FileUtil(path) # Fileutility instance @@ -661,8 +687,7 @@ def __init__(self, path, config_file=None, version=None, build=None, config=True def _start(self, config): """ - Starts the object, loads configuration - :return: + Starts the object, loads configuration. """ temp_version = '0' @@ -683,16 +708,25 @@ def _start(self, config): self.version = (self.version if self.version != '0' else temp_version) self.buildnum = (self.buildnum if self.buildnum != 0 else temp_build) + self.report_version() + + return + + def report_version(self): + + """ + Outputs the current server version and build to the terminal. + """ + output("\nServer Version Information:") output(" > Version: [{}]".format(self.version)) output(" > Build: [{}]".format(self.buildnum)) - return - def check(self): """ - Checks if a new version is available + Checks if a new version is available. + :return: True is new version, False if not/error """ @@ -733,7 +767,7 @@ def check(self): return False - if build[0] != str(self.buildnum): + if build[0] != self.buildnum: # New build available! @@ -751,7 +785,9 @@ def _select(self, val, choice, default, name): """ Selects a value from the choices. - Support updater keywords + We support updater keywords, + like 'latest' and ''. + :param val: Value entered :param choice: Choices to choose from :param default: Default value @@ -769,7 +805,7 @@ def _select(self, val, choice, default, name): # User wants latest - output("# Selecting latest {} - [{}]...".format(name, self._available_versions[0])) + output("# Selecting latest {} - [{}]...".format(name, choice[0])) val = choice[0] @@ -792,9 +828,10 @@ def _select(self, val, choice, default, name): def version_select(self, default_version='latest', default_build='latest'): """ - Prompts the user to select a version to download - Checks input against values from Paper API - Default value is recommended values + Prompts the user to select a version to download, + and checks input against values from Paper API. + Default value is recommended option, usually 'latest'. + :param default_build: Default build number :param default_version: Default version :return: (version, build) @@ -835,7 +872,7 @@ def version_select(self, default_version='latest', default_build='latest'): for i in self._available_versions: - print(" > Version: [{}]".format(i)) + print(" Version: [{}]".format(i)) while True: @@ -886,10 +923,12 @@ def version_select(self, default_version='latest', default_build='latest'): # Displaying available builds - for i in nums: + for num, i in enumerate(nums): print(" > Build Num: [{}]".format(i)) + nums[num] = str(i) + while True: # Prompting user for build info @@ -926,12 +965,24 @@ def version_select(self, default_version='latest', default_build='latest'): return ver, build - def get_new(self, default_version='latest', default_build='latest'): + def get_new(self, default_version='latest', default_build='latest', backup=True, new=False): """ - Downloads and installs the new version - Prompts the user to select a specific version - :return: + Downloads and installs the new version, + Prompts the user to select a specific version. + + After the version is selected, + then we invoke the process of downloading the the file, + and installing it to the current location. + + :param default_version: Default version to select if none is specified + :type default_version: str + :param default_build: Default build to select if none is specified + :type default_build: str + :param backup: Value determining if we should back up the old jar file + :type backup: bool + :param new: Value determining if we are doing a new install + :type new: bool """ # Prompting user for version info: @@ -981,7 +1032,7 @@ def get_new(self, default_version='latest', default_build='latest'): # Installing downloaded data: - val = self.fileutil.install() + val = self.fileutil.install(backup=backup, new=new) if not val: @@ -1007,20 +1058,32 @@ def get_new(self, default_version='latest', default_build='latest'): epilog="Please check the github page for more info: " "https://github.com/Owen-Cochell/PaperMC-Update.") - parser.add_argument('path', help='Path to file to be updated') - parser.add_argument('-v', '--version', help='Server version to install(Sets default value)', default='latest') - parser.add_argument('-b', '--build', help='Server build to install(Sets default value)', default='latest') - parser.add_argument('-iv', help='Sets the currently installed server version, ignores config', default='0') - parser.add_argument('-ib', help='Sets the currently installed server build, ignores config', default=0) + parser.add_argument('path', help='Path to paper jar file') + + version = parser.add_argument_group('Version Options', 'Arguments for selecting and altering server version information') + + version.add_argument('-v', '--version', help='Server version to install(Sets default value)', default='latest') + version.add_argument('-b', '--build', help='Server build to install(Sets default value)', default='latest') + version.add_argument('-iv', help='Sets the currently installed server version, ignores config', default='0') + version.add_argument('-ib', help='Sets the currently installed server build, ignores config', default=0) + + file = parser.add_argument_group("File Options", "Arguments for altering how we work with files") + + file.add_argument('-nlc', '--no-load-config', help='Will not load Paper version config', action='store_false') + file.add_argument('-cf', '--config-file', help='Path to Paper configuration file to read from' + '(Defaults to [SERVER_JAR_DIR]/version_history.json)') + file.add_argument('-nb', '--no-backup', help='Disables the backup operating of the old server jar', action='store_true') + file.add_argument('-n', '--new', help='Installs a new paper jar instead of updating. Great for configuring a new server install.', + action='store_true') + parser.add_argument('-c', '--check-only', help='Checks for an update, does not install', action='store_true') parser.add_argument('-nc', '--no-check', help='Does not check for an update, skips to install', action='store_true') parser.add_argument('-i', '--interactive', help='Prompts the user for the version they would like to install', action='store_true') - parser.add_argument('-nlc', '--no-load-config', help='Will not load Paper version config.', action='store_false') - parser.add_argument('-cf', '--config-file', help='Path to Paper configuration file to read from' - '(Defaults to [SERVER_JAR_DIR]/version_history.json)') parser.add_argument('-q', '--quiet', help="Will only output errors and interactive questions to the terminal", action='store_true') + parser.add_argument('-V', '--script-version', help='Displays script version', action='store_true') + parser.add_argument('-sv', '--server-version', help="Displays server version from configuration file", action='store_true') # Deprecated arguments - Included for compatibility, but do nothing @@ -1042,14 +1105,32 @@ def get_new(self, default_version='latest', default_build='latest'): output("[Handles the checking, downloading, and installation of server versions]") output("[Written by: Owen Cochell]\n") - serv = ServerUpdater(args.path, config_file=args.config_file, config=args.no_load_config, prompt=args.interactive, + # Check if we want the version of the script: + + if args.script_version: + + # Just display the script information: + + print("PaperMC-Update Version: {}".format(__version__)) + + exit() + + serv = ServerUpdater(args.path, config_file=args.config_file, config=args.no_load_config or args.server_version, prompt=args.interactive, version=args.iv, build=args.ib) update_available = True + # Check if we are just looking for server info: + + if args.server_version: + + # Already printed it, lets exit + + exit() + # Checking if we are skipping the update - if not args.no_check: + if not args.no_check and not args.new: # Allowed to check for update: @@ -1061,4 +1142,5 @@ def get_new(self, default_version='latest', default_build='latest'): # Allowed to install/Can install - serv.get_new(default_version=args.version, default_build=args.build) + serv.get_new(default_version=args.version, default_build=args.build, backup=args.no_backup or args.new, + new=args.new)