diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f6c374b..839a0a5a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,8 @@ ## --- [4.4.1] - 2024/TBD ### New features TBD +### Refactor +- Backups | Allow multiple backup configurations ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/711)) ### Bug fixes - Fix zip imports so the root dir selection is functional ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/764)) - Fix bug where full access gives minimal access ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/768)) diff --git a/app/classes/controllers/management_controller.py b/app/classes/controllers/management_controller.py index cab755b6..fc17b8be 100644 --- a/app/classes/controllers/management_controller.py +++ b/app/classes/controllers/management_controller.py @@ -5,6 +5,7 @@ from prometheus_client import CollectorRegistry, Gauge from app.classes.models.management import HelpersManagement, HelpersWebhooks from app.classes.models.servers import HelperServers +from app.classes.shared.helpers import Helpers logger = logging.getLogger(__name__) @@ -75,7 +76,7 @@ class ManagementController: # Commands Methods # ********************************************************************************** - def send_command(self, user_id, server_id, remote_ip, command): + def send_command(self, user_id, server_id, remote_ip, command, action_id=None): server_name = HelperServers.get_server_friendly_name(server_id) # Example: Admin issued command start_server for server Survival @@ -86,7 +87,12 @@ class ManagementController: remote_ip, ) self.queue_command( - {"server_id": server_id, "user_id": user_id, "command": command} + { + "server_id": server_id, + "user_id": user_id, + "command": command, + "action_id": action_id, + } ) def queue_command(self, command_data): @@ -123,6 +129,7 @@ class ManagementController: cron_string="* * * * *", parent=None, delay=0, + action_id=None, ): return HelpersManagement.create_scheduled_task( server_id, @@ -137,6 +144,7 @@ class ManagementController: cron_string, parent, delay, + action_id, ) @staticmethod @@ -175,34 +183,47 @@ class ManagementController: # Backups Methods # ********************************************************************************** @staticmethod - def get_backup_config(server_id): - return HelpersManagement.get_backup_config(server_id) + def get_backup_config(backup_id): + return HelpersManagement.get_backup_config(backup_id) - def set_backup_config( - self, - server_id: int, - backup_path: str = None, - max_backups: int = None, - excluded_dirs: list = None, - compress: bool = False, - shutdown: bool = False, - before: str = "", - after: str = "", - ): - return self.management_helper.set_backup_config( - server_id, - backup_path, - max_backups, - excluded_dirs, - compress, - shutdown, - before, - after, + @staticmethod + def get_backups_by_server(server_id, model=False): + return HelpersManagement.get_backups_by_server(server_id, model) + + @staticmethod + def delete_backup_config(backup_id): + HelpersManagement.remove_backup_config(backup_id) + + @staticmethod + def update_backup_config(backup_id, updates): + if "backup_location" in updates: + updates["backup_location"] = Helpers.wtol_path(updates["backup_location"]) + return HelpersManagement.update_backup_config(backup_id, updates) + + def add_backup_config(self, data) -> str: + if "backup_location" in data: + data["backup_location"] = Helpers.wtol_path(data["backup_location"]) + return self.management_helper.add_backup_config(data) + + def add_default_backup_config(self, server_id, backup_path): + return self.management_helper.add_backup_config( + { + "backup_name": "Default Backup", + "backup_location": Helpers.wtol_path(backup_path), + "max_backups": 0, + "before": "", + "after": "", + "compress": False, + "shutdown": False, + "server_id": server_id, + "excluded_dirs": [], + "default": True, + } ) @staticmethod - def get_excluded_backup_dirs(server_id: int): - return HelpersManagement.get_excluded_backup_dirs(server_id) + def get_excluded_backup_dirs(backup_id: int): + return HelpersManagement.get_excluded_backup_dirs(backup_id) def add_excluded_backup_dir(self, server_id: int, dir_to_add: str): self.management_helper.add_excluded_backup_dir(server_id, dir_to_add) diff --git a/app/classes/controllers/servers_controller.py b/app/classes/controllers/servers_controller.py index 6a5cce4e..7743ad84 100644 --- a/app/classes/controllers/servers_controller.py +++ b/app/classes/controllers/servers_controller.py @@ -48,7 +48,6 @@ class ServersController(metaclass=Singleton): name: str, server_uuid: str, server_dir: str, - backup_path: str, server_command: str, server_file: str, server_log_file: str, @@ -83,7 +82,6 @@ class ServersController(metaclass=Singleton): server_uuid, name, server_dir, - backup_path, server_command, server_file, server_log_file, @@ -148,8 +146,7 @@ class ServersController(metaclass=Singleton): PermissionsServers.delete_roles_permissions(role_id, role_data["servers"]) # Remove roles from server PermissionsServers.remove_roles_of_server(server_id) - # Remove backup configs tied to server - self.management_helper.remove_backup_config(server_id) + self.management_helper.remove_all_server_backups(server_id) # Finally remove server self.servers_helper.remove_server(server_id) diff --git a/app/classes/models/management.py b/app/classes/models/management.py index e59dd39c..1a3e4a11 100644 --- a/app/classes/models/management.py +++ b/app/classes/models/management.py @@ -16,6 +16,7 @@ from app.classes.models.base_model import BaseModel from app.classes.models.users import HelperUsers from app.classes.models.servers import Servers from app.classes.models.server_permissions import PermissionsServers +from app.classes.shared.helpers import Helpers from app.classes.shared.websocket_manager import WebSocketManager logger = logging.getLogger(__name__) @@ -87,6 +88,7 @@ class Schedules(BaseModel): interval_type = CharField() start_time = CharField(null=True) command = CharField(null=True) + action_id = CharField(null=True) name = CharField() one_time = BooleanField(default=False) cron_string = CharField(default="") @@ -102,13 +104,19 @@ class Schedules(BaseModel): # Backups Class # ********************************************************************************** class Backups(BaseModel): + backup_id = CharField(primary_key=True, default=Helpers.create_uuid) + backup_name = CharField(default="New Backup") + backup_location = CharField(default="") excluded_dirs = CharField(null=True) - max_backups = IntegerField() + max_backups = IntegerField(default=0) server_id = ForeignKeyField(Servers, backref="backups_server") compress = BooleanField(default=False) shutdown = BooleanField(default=False) before = CharField(default="") after = CharField(default="") + default = BooleanField(default=False) + status = CharField(default='{"status": "Standby", "message": ""}') + enabled = BooleanField(default=True) class Meta: table_name = "backups" @@ -263,6 +271,7 @@ class HelpersManagement: cron_string="* * * * *", parent=None, delay=0, + action_id=None, ): sch_id = Schedules.insert( { @@ -273,6 +282,7 @@ class HelpersManagement: Schedules.interval_type: interval_type, Schedules.start_time: start_time, Schedules.command: command, + Schedules.action_id: action_id, Schedules.name: name, Schedules.one_time: one_time, Schedules.cron_string: cron_string, @@ -335,133 +345,81 @@ class HelpersManagement: # Backups Methods # ********************************************************************************** @staticmethod - def get_backup_config(server_id): - try: - row = ( - Backups.select().where(Backups.server_id == server_id).join(Servers)[0] - ) - conf = { - "backup_path": row.server_id.backup_path, - "excluded_dirs": row.excluded_dirs, - "max_backups": row.max_backups, - "server_id": row.server_id_id, - "compress": row.compress, - "shutdown": row.shutdown, - "before": row.before, - "after": row.after, - } - except IndexError: - conf = { - "backup_path": None, - "excluded_dirs": None, - "max_backups": 0, - "server_id": server_id, - "compress": False, - "shutdown": False, - "before": "", - "after": "", - } - return conf + def get_backup_config(backup_id): + return model_to_dict(Backups.get(Backups.backup_id == backup_id)) @staticmethod - def remove_backup_config(server_id): + def get_backups_by_server(server_id, model=False): + if not model: + data = {} + for backup in ( + Backups.select().where(Backups.server_id == server_id).execute() + ): + data[str(backup.backup_id)] = { + "backup_id": backup.backup_id, + "backup_name": backup.backup_name, + "backup_location": backup.backup_location, + "excluded_dirs": backup.excluded_dirs, + "max_backups": backup.max_backups, + "server_id": backup.server_id_id, + "compress": backup.compress, + "shutdown": backup.shutdown, + "before": backup.before, + "after": backup.after, + "default": backup.default, + "enabled": backup.enabled, + } + else: + data = Backups.select().where(Backups.server_id == server_id).execute() + return data + + @staticmethod + def get_default_server_backup(server_id: str) -> dict: + print(server_id) + bu_query = Backups.select().where( + Backups.server_id == server_id, + Backups.default == True, # pylint: disable=singleton-comparison + ) + for item in bu_query: + print("HI", item) + backup_model = bu_query.first() + + if backup_model: + return model_to_dict(backup_model) + raise IndexError + + @staticmethod + def remove_all_server_backups(server_id): Backups.delete().where(Backups.server_id == server_id).execute() - def set_backup_config( - self, - server_id: int, - backup_path: str = None, - max_backups: int = None, - excluded_dirs: list = None, - compress: bool = False, - shutdown: bool = False, - before: str = "", - after: str = "", - ): - logger.debug(f"Updating server {server_id} backup config with {locals()}") - if Backups.select().where(Backups.server_id == server_id).exists(): - new_row = False - conf = {} - else: - conf = { - "excluded_dirs": None, - "max_backups": 0, - "server_id": server_id, - "compress": False, - "shutdown": False, - "before": "", - "after": "", - } - new_row = True - if max_backups is not None: - conf["max_backups"] = max_backups - if excluded_dirs is not None: - dirs_to_exclude = ",".join(excluded_dirs) + @staticmethod + def remove_backup_config(backup_id): + Backups.delete().where(Backups.backup_id == backup_id).execute() + + def add_backup_config(self, conf) -> str: + if "excluded_dirs" in conf: + dirs_to_exclude = ",".join(conf["excluded_dirs"]) conf["excluded_dirs"] = dirs_to_exclude - conf["compress"] = compress - conf["shutdown"] = shutdown - conf["before"] = before - conf["after"] = after - if not new_row: - with self.database.atomic(): - if backup_path is not None: - server_rows = ( - Servers.update(backup_path=backup_path) - .where(Servers.server_id == server_id) - .execute() - ) - else: - server_rows = 0 - backup_rows = ( - Backups.update(conf).where(Backups.server_id == server_id).execute() - ) - logger.debug( - f"Updating existing backup record. " - f"{server_rows}+{backup_rows} rows affected" - ) - else: - with self.database.atomic(): - conf["server_id"] = server_id - if backup_path is not None: - Servers.update(backup_path=backup_path).where( - Servers.server_id == server_id - ) - Backups.create(**conf) - logger.debug("Creating new backup record.") + backup = Backups.create(**conf) + logger.debug("Creating new backup record.") + return backup.backup_id @staticmethod - def get_excluded_backup_dirs(server_id: int): - excluded_dirs = HelpersManagement.get_backup_config(server_id)["excluded_dirs"] + def update_backup_config(backup_id, data): + if "excluded_dirs" in data: + dirs_to_exclude = ",".join(data["excluded_dirs"]) + data["excluded_dirs"] = dirs_to_exclude + Backups.update(**data).where(Backups.backup_id == backup_id).execute() + + @staticmethod + def get_excluded_backup_dirs(backup_id: int): + excluded_dirs = HelpersManagement.get_backup_config(backup_id)["excluded_dirs"] if excluded_dirs is not None and excluded_dirs != "": dir_list = excluded_dirs.split(",") else: dir_list = [] return dir_list - def add_excluded_backup_dir(self, server_id: int, dir_to_add: str): - dir_list = self.get_excluded_backup_dirs(server_id) - if dir_to_add not in dir_list: - dir_list.append(dir_to_add) - excluded_dirs = ",".join(dir_list) - self.set_backup_config(server_id=server_id, excluded_dirs=excluded_dirs) - else: - logger.debug( - f"Not adding {dir_to_add} to excluded directories - " - f"already in the excluded directory list for server ID {server_id}" - ) - - def del_excluded_backup_dir(self, server_id: int, dir_to_del: str): - dir_list = self.get_excluded_backup_dirs(server_id) - if dir_to_del in dir_list: - dir_list.remove(dir_to_del) - excluded_dirs = ",".join(dir_list) - self.set_backup_config(server_id=server_id, excluded_dirs=excluded_dirs) - else: - logger.debug( - f"Not removing {dir_to_del} from excluded directories - " - f"not in the excluded directory list for server ID {server_id}" - ) - # ********************************************************************************** # Webhooks Class diff --git a/app/classes/models/servers.py b/app/classes/models/servers.py index 13d9096a..e5d85c69 100644 --- a/app/classes/models/servers.py +++ b/app/classes/models/servers.py @@ -26,7 +26,6 @@ class Servers(BaseModel): created = DateTimeField(default=datetime.datetime.now) server_name = CharField(default="Server", index=True) path = CharField(default="") - backup_path = CharField(default="") executable = CharField(default="") log_path = CharField(default="") execution_command = CharField(default="") @@ -65,7 +64,6 @@ class HelperServers: server_id: str, name: str, server_dir: str, - backup_path: str, server_command: str, server_file: str, server_log_file: str, @@ -81,7 +79,6 @@ class HelperServers: name: The name of the server server_uuid: This is the UUID of the server server_dir: The directory where the server is located - backup_path: The path to the backup folder server_command: The command to start the server server_file: The name of the server file server_log_file: The path to the server log file @@ -111,7 +108,6 @@ class HelperServers: server_port=server_port, server_ip=server_host, stop_command=server_stop, - backup_path=backup_path, type=server_type, created_by=created_by, ).server_id diff --git a/app/classes/shared/file_helpers.py b/app/classes/shared/file_helpers.py index 90d8e65c..574fe8dc 100644 --- a/app/classes/shared/file_helpers.py +++ b/app/classes/shared/file_helpers.py @@ -4,7 +4,7 @@ import logging import pathlib import tempfile import zipfile -from zipfile import ZipFile, ZIP_DEFLATED +from zipfile import ZipFile, ZIP_DEFLATED, ZIP_STORED import urllib.request import ssl import time @@ -229,74 +229,15 @@ class FileHelpers: return True - def make_compressed_backup( - self, path_to_destination, path_to_zip, excluded_dirs, server_id, comment="" - ): - # create a ZipFile object - path_to_destination += ".zip" - ex_replace = [p.replace("\\", "/") for p in excluded_dirs] - total_bytes = 0 - dir_bytes = Helpers.get_dir_size(path_to_zip) - results = { - "percent": 0, - "total_files": self.helper.human_readable_file_size(dir_bytes), - } - WebSocketManager().broadcast_page_params( - "/panel/server_detail", - {"id": str(server_id)}, - "backup_status", - results, - ) - with ZipFile(path_to_destination, "w", ZIP_DEFLATED) as zip_file: - zip_file.comment = bytes( - comment, "utf-8" - ) # comments over 65535 bytes will be truncated - for root, dirs, files in os.walk(path_to_zip, topdown=True): - for l_dir in dirs: - if str(os.path.join(root, l_dir)).replace("\\", "/") in ex_replace: - dirs.remove(l_dir) - ziproot = path_to_zip - for file in files: - if ( - str(os.path.join(root, file)).replace("\\", "/") - not in ex_replace - and file != "crafty.sqlite" - ): - try: - logger.info(f"backing up: {os.path.join(root, file)}") - if os.name == "nt": - zip_file.write( - os.path.join(root, file), - os.path.join(root.replace(ziproot, ""), file), - ) - else: - zip_file.write( - os.path.join(root, file), - os.path.join(root.replace(ziproot, "/"), file), - ) - - except Exception as e: - logger.warning( - f"Error backing up: {os.path.join(root, file)}!" - f" - Error was: {e}" - ) - total_bytes += os.path.getsize(os.path.join(root, file)) - percent = round((total_bytes / dir_bytes) * 100, 2) - results = { - "percent": percent, - "total_files": self.helper.human_readable_file_size(dir_bytes), - } - WebSocketManager().broadcast_page_params( - "/panel/server_detail", - {"id": str(server_id)}, - "backup_status", - results, - ) - - return True - def make_backup( - self, path_to_destination, path_to_zip, excluded_dirs, server_id, comment="" + self, + path_to_destination, + path_to_zip, + excluded_dirs, + server_id, + backup_id, + comment="", + compressed=None, ): # create a ZipFile object path_to_destination += ".zip" @@ -313,7 +254,15 @@ class FileHelpers: "backup_status", results, ) - with ZipFile(path_to_destination, "w") as zip_file: + WebSocketManager().broadcast_page_params( + "/panel/edit_backup", + {"id": str(server_id)}, + "backup_status", + results, + ) + # Set the compression mode based on the `compressed` parameter + compression_mode = ZIP_DEFLATED if compressed else ZIP_STORED + with ZipFile(path_to_destination, "w", compression_mode) as zip_file: zip_file.comment = bytes( comment, "utf-8" ) # comments over 65535 bytes will be truncated @@ -364,6 +313,7 @@ class FileHelpers: results = { "percent": percent, "total_files": self.helper.human_readable_file_size(dir_bytes), + "backup_id": backup_id, } # send status results to page. WebSocketManager().broadcast_page_params( @@ -372,6 +322,12 @@ class FileHelpers: "backup_status", results, ) + WebSocketManager().broadcast_page_params( + "/panel/edit_backup", + {"id": str(server_id)}, + "backup_status", + results, + ) return True @staticmethod diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py index 64d4e1d1..a5fd7840 100644 --- a/app/classes/shared/helpers.py +++ b/app/classes/shared/helpers.py @@ -1010,6 +1010,11 @@ class Helpers: except PermissionError as e: logger.critical(f"Check generated exception due to permssion error: {e}") return False + except FileNotFoundError as e: + logger.critical( + f"Check generated exception due to file does not exist error: {e}" + ) + return False def create_self_signed_cert(self, cert_dir=None): if cert_dir is None: diff --git a/app/classes/shared/main_controller.py b/app/classes/shared/main_controller.py index e31486ea..0d90a967 100644 --- a/app/classes/shared/main_controller.py +++ b/app/classes/shared/main_controller.py @@ -566,7 +566,6 @@ class Controller: name=data["name"], server_uuid=server_fs_uuid, server_dir=new_server_path, - backup_path=backup_path, server_command=server_command, server_file=server_file, server_log_file=log_location, @@ -576,7 +575,7 @@ class Controller: server_host=monitoring_host, server_type=monitoring_type, ) - self.management.set_backup_config( + self.management.add_default_backup_config( new_server_id, backup_path, ) @@ -722,7 +721,6 @@ class Controller: server_name, server_id, new_server_dir, - backup_path, server_command, server_jar, server_log_file, @@ -776,7 +774,6 @@ class Controller: server_name, server_id, new_server_dir, - backup_path, server_command, server_exe, server_log_file, @@ -821,7 +818,6 @@ class Controller: server_name, server_id, new_server_dir, - backup_path, server_command, server_exe, server_log_file, @@ -869,7 +865,6 @@ class Controller: server_name, server_id, new_server_dir, - backup_path, server_command, server_exe, server_log_file, @@ -893,16 +888,13 @@ class Controller: # ********************************************************************************** def rename_backup_dir(self, old_server_id, new_server_id, new_uuid): - server_data = self.servers.get_server_data_by_id(old_server_id) server_obj = self.servers.get_server_obj(new_server_id) - old_bu_path = server_data["backup_path"] ServerPermsController.backup_role_swap(old_server_id, new_server_id) - backup_path = old_bu_path + backup_path = os.path.join(self.helper.backup_path, old_server_id) backup_path = Path(backup_path) backup_path_components = list(backup_path.parts) backup_path_components[-1] = new_uuid new_bu_path = pathlib.PurePath(os.path.join(*backup_path_components)) - server_obj.backup_path = new_bu_path default_backup_dir = os.path.join(self.helper.backup_path, new_uuid) try: os.rmdir(default_backup_dir) @@ -916,7 +908,6 @@ class Controller: name: str, server_uuid: str, server_dir: str, - backup_path: str, server_command: str, server_file: str, server_log_file: str, @@ -931,7 +922,6 @@ class Controller: name, server_uuid, server_dir, - backup_path, server_command, server_file, server_log_file, @@ -996,16 +986,16 @@ class Controller: f"Unable to delete server files for server with ID: " f"{server_id} with error logged: {e}" ) - if Helpers.check_path_exists( - self.servers.get_server_data_by_id(server_id)["backup_path"] - ): - FileHelpers.del_dirs( - Helpers.get_os_understandable_path( - self.servers.get_server_data_by_id(server_id)[ - "backup_path" - ] + backup_configs = HelpersManagement.get_backups_by_server( + server_id, True + ) + for config in backup_configs: + if Helpers.check_path_exists(config.backup_location): + FileHelpers.del_dirs( + Helpers.get_os_understandable_path( + config.backup_location + ) ) - ) # Cleanup scheduled tasks try: diff --git a/app/classes/shared/server.py b/app/classes/shared/server.py index a6c98b89..ab8ca54a 100644 --- a/app/classes/shared/server.py +++ b/app/classes/shared/server.py @@ -207,9 +207,6 @@ class ServerInstance: self.server_scheduler.start() self.dir_scheduler.start() self.start_dir_calc_task() - self.backup_thread = threading.Thread( - target=self.backup_server, daemon=True, name=f"backup_{self.name}" - ) self.is_backingup = False # Reset crash and update at initialization self.stats_helper.server_crash_reset() @@ -940,8 +937,7 @@ class ServerInstance: WebSocketManager().broadcast_user(user, "send_start_reload", {}) def restart_threaded_server(self, user_id): - bu_conf = HelpersManagement.get_backup_config(self.server_id) - if self.is_backingup and bu_conf["shutdown"]: + if self.is_backingup: logger.info( "Restart command detected. Supressing - server has" " backup shutdown enabled and server is currently backing up." @@ -1111,12 +1107,16 @@ class ServerInstance: f.write("eula=true") self.run_threaded_server(user_id) - def a_backup_server(self): - if self.settings["backup_path"] == "": - logger.critical("Backup path is None. Canceling Backup!") - return + def server_backup_threader(self, backup_id, update=False): + # Check to see if we're already backing up + if self.check_backup_by_id(backup_id): + return False + backup_thread = threading.Thread( - target=self.backup_server, daemon=True, name=f"backup_{self.name}" + target=self.backup_server, + daemon=True, + name=f"backup_{backup_id}", + args=[backup_id, update], ) logger.info( f"Starting Backup Thread for server {self.settings['server_name']}." @@ -1127,27 +1127,20 @@ class ServerInstance: "Backup Thread - Local server path not defined. " "Setting local server path variable." ) - # checks if the backup thread is currently alive for this server - if not self.is_backingup: - try: - backup_thread.start() - self.is_backingup = True - except Exception as ex: - logger.error(f"Failed to start backup: {ex}") - return False - else: - logger.error( - f"Backup is already being processed for server " - f"{self.settings['server_name']}. Canceling backup request" - ) + + try: + backup_thread.start() + except Exception as ex: + logger.error(f"Failed to start backup: {ex}") return False logger.info(f"Backup Thread started for server {self.settings['server_name']}.") @callback - def backup_server(self): + def backup_server(self, backup_id, update): was_server_running = None logger.info(f"Starting server {self.name} (ID {self.server_id}) backup") server_users = PermissionsServers.get_server_user_list(self.server_id) + # Alert the start of the backup to the authorized users. for user in server_users: WebSocketManager().broadcast_user( user, @@ -1157,30 +1150,40 @@ class ServerInstance: ).format(self.name), ) time.sleep(3) - conf = HelpersManagement.get_backup_config(self.server_id) + + # Get the backup config + conf = HelpersManagement.get_backup_config(backup_id) + # Adjust the location to include the backup ID for destination. + backup_location = os.path.join(conf["backup_location"], conf["backup_id"]) + + # Check if the backup location even exists. + if not backup_location: + Console.critical("No backup path found. Canceling") + return None if conf["before"]: - if self.check_running(): - logger.debug( - "Found running server and send command option. Sending command" - ) - self.send_command(conf["before"]) + logger.debug( + "Found running server and send command option. Sending command" + ) + self.send_command(conf["before"]) + # Pause to let command run + time.sleep(5) if conf["shutdown"]: - if conf["before"]: - # pause to let people read message. - time.sleep(5) logger.info( "Found shutdown preference. Delaying" + "backup start. Shutting down server." ) - if self.check_running(): - self.stop_server() - was_server_running = True + if not update: + was_server_running = False + if self.check_running(): + self.stop_server() + was_server_running = True + + self.helper.ensure_dir_exists(backup_location) - self.helper.ensure_dir_exists(self.settings["backup_path"]) try: backup_filename = ( - f"{self.settings['backup_path']}/" + f"{backup_location}/" f"{datetime.datetime.now().astimezone(self.tz).strftime('%Y-%m-%d_%H-%M-%S')}" # pylint: disable=line-too-long ) logger.info( @@ -1188,42 +1191,36 @@ class ServerInstance: f" (ID#{self.server_id}, path={self.server_path}) " f"at '{backup_filename}'" ) - excluded_dirs = HelpersManagement.get_excluded_backup_dirs(self.server_id) + excluded_dirs = HelpersManagement.get_excluded_backup_dirs(backup_id) server_dir = Helpers.get_os_understandable_path(self.settings["path"]) - if conf["compress"]: - logger.debug( - "Found compress backup to be true. Calling compressed archive" - ) - self.file_helper.make_compressed_backup( - Helpers.get_os_understandable_path(backup_filename), - server_dir, - excluded_dirs, - self.server_id, - ) - else: - logger.debug( - "Found compress backup to be false. Calling NON-compressed archive" - ) - self.file_helper.make_backup( - Helpers.get_os_understandable_path(backup_filename), - server_dir, - excluded_dirs, - self.server_id, - ) + + self.file_helper.make_backup( + Helpers.get_os_understandable_path(backup_filename), + server_dir, + excluded_dirs, + self.server_id, + backup_id, + conf["backup_name"], + conf["compress"], + ) while ( - len(self.list_backups()) > conf["max_backups"] + len(self.list_backups(conf)) > conf["max_backups"] and conf["max_backups"] > 0 ): - backup_list = self.list_backups() + backup_list = self.list_backups(conf) oldfile = backup_list[0] - oldfile_path = f"{conf['backup_path']}/{oldfile['path']}" + oldfile_path = f"{backup_location}/{oldfile['path']}" logger.info(f"Removing old backup '{oldfile['path']}'") os.remove(Helpers.get_os_understandable_path(oldfile_path)) - self.is_backingup = False logger.info(f"Backup of server: {self.name} completed") - results = {"percent": 100, "total_files": 0, "current_file": 0} + results = { + "percent": 100, + "total_files": 0, + "current_file": 0, + "backup_id": backup_id, + } if len(WebSocketManager().clients) > 0: WebSocketManager().broadcast_page_params( "/panel/server_detail", @@ -1248,7 +1245,6 @@ class ServerInstance: ) self.run_threaded_server(HelperUsers.get_user_id_by_name("system")) time.sleep(3) - self.last_backup_failed = False if conf["after"]: if self.check_running(): logger.debug( @@ -1256,12 +1252,21 @@ class ServerInstance: ) self.send_command(conf["after"]) # pause to let people read message. + HelpersManagement.update_backup_config( + backup_id, + {"status": json.dumps({"status": "Standby", "message": ""})}, + ) time.sleep(5) - except: + except Exception as e: logger.exception( f"Failed to create backup of server {self.name} (ID {self.server_id})" ) - results = {"percent": 100, "total_files": 0, "current_file": 0} + results = { + "percent": 100, + "total_files": 0, + "current_file": 0, + "backup_id": backup_id, + } if len(WebSocketManager().clients) > 0: WebSocketManager().broadcast_page_params( "/panel/server_detail", @@ -1269,56 +1274,51 @@ class ServerInstance: "backup_status", results, ) - self.is_backingup = False if was_server_running: logger.info( "Backup complete. User had shutdown preference. Starting server." ) self.run_threaded_server(HelperUsers.get_user_id_by_name("system")) - self.last_backup_failed = True - - def backup_status(self, source_path, dest_path): - results = Helpers.calc_percent(source_path, dest_path) - self.backup_stats = results - if len(WebSocketManager().clients) > 0: - WebSocketManager().broadcast_page_params( - "/panel/server_detail", - {"id": str(self.server_id)}, - "backup_status", - results, + HelpersManagement.update_backup_config( + backup_id, + {"status": json.dumps({"status": "Failed", "message": f"{e}"})}, ) + self.set_backup_status() def last_backup_status(self): return self.last_backup_failed - def send_backup_status(self): - try: - return self.backup_stats - except: - return {"percent": 0, "total_files": 0} + def set_backup_status(self): + backups = HelpersManagement.get_backups_by_server(self.server_id, True) + alert = False + for backup in backups: + if json.loads(backup.status)["status"] == "Failed": + alert = True + self.last_backup_failed = alert - def list_backups(self): - if not self.settings["backup_path"]: + def list_backups(self, backup_config: dict) -> list: + if not backup_config: logger.info( f"Error putting backup file list for server with ID: {self.server_id}" ) return [] + backup_location = os.path.join( + backup_config["backup_location"], backup_config["backup_id"] + ) if not Helpers.check_path_exists( - Helpers.get_os_understandable_path(self.settings["backup_path"]) + Helpers.get_os_understandable_path(backup_location) ): return [] files = Helpers.get_human_readable_files_sizes( Helpers.list_dir_by_date( - Helpers.get_os_understandable_path(self.settings["backup_path"]) + Helpers.get_os_understandable_path(backup_location) ) ) return [ { "path": os.path.relpath( f["path"], - start=Helpers.get_os_understandable_path( - self.settings["backup_path"] - ), + start=Helpers.get_os_understandable_path(backup_location), ), "size": f["size"], } @@ -1330,7 +1330,7 @@ class ServerInstance: def jar_update(self): self.stats_helper.set_update(True) update_thread = threading.Thread( - target=self.a_jar_update, daemon=True, name=f"exe_update_{self.name}" + target=self.threaded_jar_update, daemon=True, name=f"exe_update_{self.name}" ) update_thread.start() @@ -1371,10 +1371,13 @@ class ServerInstance: def check_update(self): return self.stats_helper.get_server_stats()["updating"] - def a_jar_update(self): + def threaded_jar_update(self): server_users = PermissionsServers.get_server_user_list(self.server_id) was_started = "-1" - self.a_backup_server() + # Get default backup configuration + backup_config = HelpersManagement.get_default_server_backup(self.server_id) + # start threaded backup + self.server_backup_threader(backup_config["backup_id"], True) # checks if server is running. Calls shutdown if it is running. if self.check_running(): was_started = True @@ -1403,47 +1406,22 @@ class ServerInstance: "string": message, }, ) - backup_dir = os.path.join( - Helpers.get_os_understandable_path(self.settings["path"]), - "crafty_executable_backups", - ) - # checks if backup directory already exists - if os.path.isdir(backup_dir): - backup_executable = os.path.join(backup_dir, self.settings["executable"]) - else: - logger.info( - f"Executable backup directory not found for Server: {self.name}." - f" Creating one." - ) - os.mkdir(backup_dir) - backup_executable = os.path.join(backup_dir, self.settings["executable"]) - - if len(os.listdir(backup_dir)) > 0: - # removes old backup - logger.info(f"Old backups found for server: {self.name}. Removing...") - for item in os.listdir(backup_dir): - os.remove(os.path.join(backup_dir, item)) - logger.info(f"Old backups removed for server: {self.name}.") - else: - logger.info(f"No old backups found for server: {self.name}") - current_executable = os.path.join( Helpers.get_os_understandable_path(self.settings["path"]), self.settings["executable"], ) - - try: - # copies to backup dir - FileHelpers.copy_file(current_executable, backup_executable) - except FileNotFoundError: - logger.error("Could not create backup of jarfile. File not found.") - + backing_up = True # wait for backup - while self.is_backingup: - time.sleep(10) + while backing_up: + # Check to see if we're already backing up + backing_up = self.check_backup_by_id(backup_config["backup_id"]) + time.sleep(2) # check if backup was successful - if self.last_backup_failed: + backup_status = json.loads( + HelpersManagement.get_backup_config(backup_config["backup_id"])["status"] + )["status"] + if backup_status == "Failed": for user in server_users: WebSocketManager().broadcast_user( user, @@ -1528,12 +1506,6 @@ class ServerInstance: WebSocketManager().broadcast_user_page( user, "/panel/dashboard", "send_start_reload", {} ) - WebSocketManager().broadcast_user( - user, - "notification", - "Executable update finished for " + self.name, - ) - self.management_helper.add_to_audit_log_raw( "Alert", "-1", @@ -1656,6 +1628,14 @@ class ServerInstance: except: Console.critical("Can't broadcast server status to websocket") + def check_backup_by_id(self, backup_id: str) -> bool: + # Check to see if we're already backing up + for thread in threading.enumerate(): + if thread.getName() == f"backup_{backup_id}": + Console.debug(f"Backup with id {backup_id} already running!") + return True + return False + def get_servers_stats(self): server_stats = {} diff --git a/app/classes/shared/tasks.py b/app/classes/shared/tasks.py index b9513441..80efa87d 100644 --- a/app/classes/shared/tasks.py +++ b/app/classes/shared/tasks.py @@ -140,7 +140,7 @@ class TasksManager: ) elif command == "backup_server": - svr.a_backup_server() + svr.server_backup_threader(cmd["action_id"]) elif command == "update_executable": svr.jar_update() @@ -240,6 +240,7 @@ class TasksManager: "system" ), "command": schedule.command, + "action_id": schedule.action_id, } ], ) @@ -268,6 +269,7 @@ class TasksManager: "system" ), "command": schedule.command, + "action_id": schedule.action_id, } ], ) @@ -284,6 +286,7 @@ class TasksManager: "system" ), "command": schedule.command, + "action_id": schedule.action_id, } ], ) @@ -303,6 +306,7 @@ class TasksManager: "system" ), "command": schedule.command, + "action_id": schedule.action_id, } ], ) @@ -337,6 +341,7 @@ class TasksManager: job_data["cron_string"], job_data["parent"], job_data["delay"], + job_data["action_id"], ) # Checks to make sure some doofus didn't actually make the newly @@ -367,6 +372,7 @@ class TasksManager: "system" ), "command": job_data["command"], + "action_id": job_data["action_id"], } ], ) @@ -393,6 +399,7 @@ class TasksManager: "system" ), "command": job_data["command"], + "action_id": job_data["action_id"], } ], ) @@ -409,6 +416,7 @@ class TasksManager: "system" ), "command": job_data["command"], + "action_id": job_data["action_id"], } ], ) @@ -428,6 +436,7 @@ class TasksManager: "system" ), "command": job_data["command"], + "action_id": job_data["action_id"], } ], ) @@ -520,6 +529,7 @@ class TasksManager: "system" ), "command": job_data["command"], + "action_id": job_data["action_id"], } ], ) @@ -543,6 +553,7 @@ class TasksManager: "system" ), "command": job_data["command"], + "action_id": job_data["action_id"], } ], ) @@ -559,6 +570,7 @@ class TasksManager: "system" ), "command": job_data["command"], + "action_id": job_data["action_id"], } ], ) @@ -578,6 +590,7 @@ class TasksManager: "system" ), "command": job_data["command"], + "action_id": job_data["action_id"], } ], ) @@ -653,6 +666,7 @@ class TasksManager: "system" ), "command": schedule.command, + "action_id": schedule.action_id, } ], ) diff --git a/app/classes/web/panel_handler.py b/app/classes/web/panel_handler.py index 6e122b2d..8df48431 100644 --- a/app/classes/web/panel_handler.py +++ b/app/classes/web/panel_handler.py @@ -41,6 +41,8 @@ SUBPAGE_PERMS = { "webhooks": EnumPermissionsServer.CONFIG, } +SCHEDULE_AUTH_ERROR_URL = "/panel/error?error=Unauthorized access To Schedules" + class PanelHandler(BaseHandler): def get_user_roles(self) -> t.Dict[str, list]: @@ -677,36 +679,18 @@ class PanelHandler(BaseHandler): page_data["java_versions"] = page_java if subpage == "backup": server_info = self.controller.servers.get_server_data_by_id(server_id) - page_data["backup_config"] = ( - self.controller.management.get_backup_config(server_id) - ) - exclusions = [] - page_data["exclusions"] = ( - self.controller.management.get_excluded_backup_dirs(server_id) + + page_data["backups"] = self.controller.management.get_backups_by_server( + server_id, model=True ) page_data["backing_up"] = ( self.controller.servers.get_server_instance_by_id( server_id ).is_backingup ) - page_data["backup_stats"] = ( - self.controller.servers.get_server_instance_by_id( - server_id - ).send_backup_status() - ) # makes it so relative path is the only thing shown - for file in page_data["exclusions"]: - if Helpers.is_os_windows(): - exclusions.append(file.replace(server_info["path"] + "\\", "")) - else: - exclusions.append(file.replace(server_info["path"] + "/", "")) - page_data["exclusions"] = exclusions + self.controller.servers.refresh_server_settings(server_id) - try: - page_data["backup_list"] = server.list_backups() - except: - page_data["backup_list"] = [] - page_data["backup_path"] = Helpers.wtol_path(server_info["backup_path"]) if subpage == "metrics": try: @@ -780,20 +764,23 @@ class PanelHandler(BaseHandler): elif page == "download_backup": file = self.get_argument("file", "") + backup_id = self.get_argument("backup_id", "") server_id = self.check_server_id() if server_id is None: return - + backup_config = self.controller.management.get_backup_config(backup_id) server_info = self.controller.servers.get_server_data_by_id(server_id) + backup_location = os.path.join(backup_config["backup_location"], backup_id) backup_file = os.path.abspath( os.path.join( - Helpers.get_os_understandable_path(server_info["backup_path"]), file + Helpers.get_os_understandable_path(backup_location), + file, ) ) if not self.helper.is_subdir( backup_file, - Helpers.get_os_understandable_path(server_info["backup_path"]), + Helpers.get_os_understandable_path(backup_location), ) or not os.path.isfile(backup_file): self.redirect("/panel/error?error=Invalid path detected") return @@ -1132,6 +1119,9 @@ class PanelHandler(BaseHandler): page_data["server_data"] = self.controller.servers.get_server_data_by_id( server_id ) + page_data["backups"] = self.controller.management.get_backups_by_server( + server_id, True + ) page_data["server_stats"] = self.controller.servers.get_server_stats_by_id( server_id ) @@ -1152,6 +1142,7 @@ class PanelHandler(BaseHandler): page_data["schedule"]["delay"] = 0 page_data["schedule"]["time"] = "" page_data["schedule"]["interval"] = 1 + page_data["schedule"]["action_id"] = "" # we don't need to check difficulty here. # We'll just default to basic for new schedules page_data["schedule"]["difficulty"] = "basic" @@ -1160,7 +1151,7 @@ class PanelHandler(BaseHandler): if not EnumPermissionsServer.SCHEDULE in page_data["user_permissions"]: if not superuser: - self.redirect("/panel/error?error=Unauthorized access To Schedules") + self.redirect(SCHEDULE_AUTH_ERROR_URL) return template = "panel/server_schedule_edit.html" @@ -1197,6 +1188,9 @@ class PanelHandler(BaseHandler): exec_user["user_id"], server_id ) ) + page_data["backups"] = self.controller.management.get_backups_by_server( + server_id, True + ) page_data["server_data"] = self.controller.servers.get_server_data_by_id( server_id ) @@ -1211,6 +1205,7 @@ class PanelHandler(BaseHandler): page_data["schedule"]["server_id"] = server_id page_data["schedule"]["schedule_id"] = schedule.schedule_id page_data["schedule"]["action"] = schedule.action + page_data["schedule"]["action_id"] = schedule.action_id if schedule.name: page_data["schedule"]["name"] = schedule.name else: @@ -1254,11 +1249,141 @@ class PanelHandler(BaseHandler): if not EnumPermissionsServer.SCHEDULE in page_data["user_permissions"]: if not superuser: - self.redirect("/panel/error?error=Unauthorized access To Schedules") + self.redirect(SCHEDULE_AUTH_ERROR_URL) return template = "panel/server_schedule_edit.html" + elif page == "edit_backup": + server_id = self.get_argument("id", None) + backup_id = self.get_argument("backup_id", None) + page_data["active_link"] = "backups" + page_data["permissions"] = { + "Commands": EnumPermissionsServer.COMMANDS, + "Terminal": EnumPermissionsServer.TERMINAL, + "Logs": EnumPermissionsServer.LOGS, + "Schedule": EnumPermissionsServer.SCHEDULE, + "Backup": EnumPermissionsServer.BACKUP, + "Files": EnumPermissionsServer.FILES, + "Config": EnumPermissionsServer.CONFIG, + "Players": EnumPermissionsServer.PLAYERS, + } + if not self.failed_server: + server_obj = self.controller.servers.get_server_instance_by_id( + server_id + ) + page_data["backup_failed"] = server_obj.last_backup_status() + page_data["user_permissions"] = ( + self.controller.server_perms.get_user_id_permissions_list( + exec_user["user_id"], server_id + ) + ) + server_info = self.controller.servers.get_server_data_by_id(server_id) + page_data["backup_config"] = self.controller.management.get_backup_config( + backup_id + ) + page_data["backups"] = self.controller.management.get_backups_by_server( + server_id, model=True + ) + exclusions = [] + page_data["backing_up"] = self.controller.servers.get_server_instance_by_id( + server_id + ).is_backingup + self.controller.servers.refresh_server_settings(server_id) + try: + page_data["backup_list"] = server.list_backups( + page_data["backup_config"] + ) + except: + page_data["backup_list"] = [] + page_data["backup_path"] = Helpers.wtol_path( + page_data["backup_config"]["backup_location"] + ) + page_data["server_data"] = self.controller.servers.get_server_data_by_id( + server_id + ) + page_data["server_stats"] = self.controller.servers.get_server_stats_by_id( + server_id + ) + page_data["server_stats"]["server_type"] = ( + self.controller.servers.get_server_type_by_id(server_id) + ) + page_data["exclusions"] = ( + self.controller.management.get_excluded_backup_dirs(backup_id) + ) + # Make exclusion paths relative for page + for file in page_data["exclusions"]: + if Helpers.is_os_windows(): + exclusions.append(file.replace(server_info["path"] + "\\", "")) + else: + exclusions.append(file.replace(server_info["path"] + "/", "")) + page_data["exclusions"] = exclusions + + if EnumPermissionsServer.BACKUP not in page_data["user_permissions"]: + if not superuser: + self.redirect(SCHEDULE_AUTH_ERROR_URL) + return + template = "panel/server_backup_edit.html" + + elif page == "add_backup": + server_id = self.get_argument("id", None) + backup_id = self.get_argument("backup_id", None) + page_data["active_link"] = "backups" + page_data["permissions"] = { + "Commands": EnumPermissionsServer.COMMANDS, + "Terminal": EnumPermissionsServer.TERMINAL, + "Logs": EnumPermissionsServer.LOGS, + "Schedule": EnumPermissionsServer.SCHEDULE, + "Backup": EnumPermissionsServer.BACKUP, + "Files": EnumPermissionsServer.FILES, + "Config": EnumPermissionsServer.CONFIG, + "Players": EnumPermissionsServer.PLAYERS, + } + if not self.failed_server: + server_obj = self.controller.servers.get_server_instance_by_id( + server_id + ) + page_data["backup_failed"] = server_obj.last_backup_status() + page_data["user_permissions"] = ( + self.controller.server_perms.get_user_id_permissions_list( + exec_user["user_id"], server_id + ) + ) + server_info = self.controller.servers.get_server_data_by_id(server_id) + page_data["backup_config"] = { + "excluded_dirs": [], + "max_backups": 0, + "server_id": server_id, + "backup_location": os.path.join(self.helper.backup_path, server_id), + "compress": False, + "shutdown": False, + "before": "", + "after": "", + } + page_data["backing_up"] = False + self.controller.servers.refresh_server_settings(server_id) + + page_data["backup_list"] = [] + page_data["backup_path"] = Helpers.wtol_path( + page_data["backup_config"]["backup_location"] + ) + page_data["server_data"] = self.controller.servers.get_server_data_by_id( + server_id + ) + page_data["server_stats"] = self.controller.servers.get_server_stats_by_id( + server_id + ) + page_data["server_stats"]["server_type"] = ( + self.controller.servers.get_server_type_by_id(server_id) + ) + page_data["exclusions"] = [] + + if EnumPermissionsServer.BACKUP not in page_data["user_permissions"]: + if not superuser: + self.redirect(SCHEDULE_AUTH_ERROR_URL) + return + template = "panel/server_backup_edit.html" + elif page == "edit_user": user_id = self.get_argument("id", None) role_servers = self.controller.servers.get_authorized_servers(user_id) diff --git a/app/classes/web/routes/api/api_handlers.py b/app/classes/web/routes/api/api_handlers.py index a30350a5..b6911661 100644 --- a/app/classes/web/routes/api/api_handlers.py +++ b/app/classes/web/routes/api/api_handlers.py @@ -38,6 +38,7 @@ from app.classes.web.routes.api.servers.server.backups.index import ( ) from app.classes.web.routes.api.servers.server.backups.backup.index import ( ApiServersServerBackupsBackupIndexHandler, + ApiServersServerBackupsBackupFilesIndexHandler, ) from app.classes.web.routes.api.servers.server.files import ( ApiServersServerFilesIndexHandler, @@ -218,13 +219,13 @@ def api_handlers(handler_args): handler_args, ), ( - r"/api/v2/servers/([a-z0-9-]+)/backups/backup/?", + r"/api/v2/servers/([a-z0-9-]+)/backups/backup/([a-z0-9-]+)/?", ApiServersServerBackupsBackupIndexHandler, handler_args, ), ( - r"/api/v2/servers/([a-z0-9-]+)/files/?", - ApiServersServerFilesIndexHandler, + r"/api/v2/servers/([a-z0-9-]+)/backups/backup/([a-z0-9-]+)/files/?", + ApiServersServerBackupsBackupFilesIndexHandler, handler_args, ), ( @@ -237,6 +238,11 @@ def api_handlers(handler_args): ApiServersServerFilesZipHandler, handler_args, ), + ( + r"/api/v2/servers/([a-z0-9-]+)/files(?:/([a-zA-Z0-9-]+))?/?", + ApiServersServerFilesIndexHandler, + handler_args, + ), ( r"/api/v2/servers/([a-z0-9-]+)/tasks/?", ApiServersServerTasksIndexHandler, @@ -273,7 +279,8 @@ def api_handlers(handler_args): handler_args, ), ( - r"/api/v2/servers/([a-z0-9-]+)/action/([a-z_]+)/?", + # optional third argument when we need a action ID + r"/api/v2/servers/([a-z0-9-]+)/action/([a-z_]+)(?:/([a-z0-9-]+))?/?", ApiServersServerActionHandler, handler_args, ), diff --git a/app/classes/web/routes/api/servers/server/action.py b/app/classes/web/routes/api/servers/server/action.py index aba06da3..d8e58b2f 100644 --- a/app/classes/web/routes/api/servers/server/action.py +++ b/app/classes/web/routes/api/servers/server/action.py @@ -1,5 +1,6 @@ import logging import os +import json from app.classes.models.server_permissions import EnumPermissionsServer from app.classes.models.servers import Servers from app.classes.shared.file_helpers import FileHelpers @@ -10,7 +11,7 @@ logger = logging.getLogger(__name__) class ApiServersServerActionHandler(BaseApiHandler): - def post(self, server_id: str, action: str): + def post(self, server_id: str, action: str, action_id=None): auth_data = self.authenticate_user() if not auth_data: return @@ -54,7 +55,7 @@ class ApiServersServerActionHandler(BaseApiHandler): return self._agree_eula(server_id, auth_data[4]["user_id"]) self.controller.management.send_command( - auth_data[4]["user_id"], server_id, self.get_remote_ip(), action + auth_data[4]["user_id"], server_id, self.get_remote_ip(), action, action_id ) self.finish_json( @@ -82,6 +83,20 @@ class ApiServersServerActionHandler(BaseApiHandler): new_server_id = self.helper.create_uuid() new_server_path = os.path.join(self.helper.servers_dir, new_server_id) new_backup_path = os.path.join(self.helper.backup_path, new_server_id) + backup_data = { + "backup_name": f"{new_server_name} Backup", + "backup_location": new_backup_path, + "excluded_dirs": "", + "max_backups": 0, + "server_id": new_server_id, + "compress": False, + "shutdown": False, + "before": "", + "after": "", + "default": True, + "status": json.dumps({"status": "Standby", "message": ""}), + "enabled": True, + } new_server_command = str(server_data.get("execution_command")).replace( server_id, new_server_id ) @@ -93,7 +108,6 @@ class ApiServersServerActionHandler(BaseApiHandler): new_server_name, new_server_id, new_server_path, - new_backup_path, new_server_command, server_data.get("executable"), new_server_log_path, @@ -103,6 +117,8 @@ class ApiServersServerActionHandler(BaseApiHandler): server_data.get("type"), ) + self.controller.management.add_backup_config(backup_data) + self.controller.management.add_to_audit_log( user_id, f"is cloning server {server_id} named {server_data.get('server_name')}", diff --git a/app/classes/web/routes/api/servers/server/backups/backup/index.py b/app/classes/web/routes/api/servers/server/backups/backup/index.py index 1b9ff915..5d8fd2b5 100644 --- a/app/classes/web/routes/api/servers/server/backups/backup/index.py +++ b/app/classes/web/routes/api/servers/server/backups/backup/index.py @@ -11,7 +11,7 @@ from app.classes.shared.helpers import Helpers logger = logging.getLogger(__name__) -backup_schema = { +BACKUP_SCHEMA = { "type": "object", "properties": { "filename": {"type": "string", "minLength": 5}, @@ -19,11 +19,44 @@ backup_schema = { "additionalProperties": False, "minProperties": 1, } +BACKUP_PATCH_SCHEMA = { + "type": "object", + "properties": { + "backup_name": {"type": "string", "minLength": 3}, + "backup_location": {"type": "string", "minLength": 1}, + "max_backups": {"type": "integer"}, + "compress": {"type": "boolean"}, + "shutdown": {"type": "boolean"}, + "before": {"type": "string"}, + "after": {"type": "string"}, + "excluded_dirs": {"type": "array"}, + }, + "additionalProperties": False, + "minProperties": 1, +} + +BASIC_BACKUP_PATCH_SCHEMA = { + "type": "object", + "properties": { + "backup_name": {"type": "string", "minLength": 3}, + "max_backups": {"type": "integer"}, + "compress": {"type": "boolean"}, + "shutdown": {"type": "boolean"}, + "before": {"type": "string"}, + "after": {"type": "string"}, + "excluded_dirs": {"type": "array"}, + }, + "additionalProperties": False, + "minProperties": 1, +} +ID_MISMATCH = "Server ID backup server ID different" +GENERAL_AUTH_ERROR = "Authorization Error" class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler): - def get(self, server_id: str): + def get(self, server_id: str, backup_id: str): auth_data = self.authenticate_user() + backup_conf = self.controller.management.get_backup_config(backup_id) if not auth_data: return mask = self.controller.server_perms.get_lowest_api_perm_mask( @@ -32,15 +65,40 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler): ), auth_data[5], ) + if backup_conf["server_id"]["server_id"] != server_id: + return self.finish_json( + 400, + { + "status": "error", + "error": "ID_MISMATCH", + "error_data": ID_MISMATCH, + }, + ) server_permissions = self.controller.server_perms.get_permissions(mask) if EnumPermissionsServer.BACKUP not in server_permissions: # if the user doesn't have Schedule permission, return an error - return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) - self.finish_json(200, self.controller.management.get_backup_config(server_id)) + return self.finish_json( + 400, + { + "status": "error", + "error": "NOT_AUTHORIZED", + "error_data": GENERAL_AUTH_ERROR, + }, + ) + self.finish_json(200, backup_conf) - def delete(self, server_id: str): + def delete(self, server_id: str, backup_id: str): auth_data = self.authenticate_user() - backup_conf = self.controller.management.get_backup_config(server_id) + backup_conf = self.controller.management.get_backup_config(backup_id) + if backup_conf["server_id"]["server_id"] != server_id: + return self.finish_json( + 400, + { + "status": "error", + "error": "ID_MISMATCH", + "error_data": ID_MISMATCH, + }, + ) if not auth_data: return mask = self.controller.server_perms.get_lowest_api_perm_mask( @@ -52,7 +110,66 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler): server_permissions = self.controller.server_perms.get_permissions(mask) if EnumPermissionsServer.BACKUP not in server_permissions: # if the user doesn't have Schedule permission, return an error - return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) + return self.finish_json( + 400, + { + "status": "error", + "error": "NOT_AUTHORIZED", + "error_data": GENERAL_AUTH_ERROR, + }, + ) + + self.controller.management.add_to_audit_log( + auth_data[4]["user_id"], + f"Edited server {server_id}: removed backup config" + f" {backup_conf['backup_name']}", + server_id, + self.get_remote_ip(), + ) + if backup_conf["default"]: + return self.finish_json( + 405, + { + "status": "error", + "error": "NOT_ALLOWED", + "error_data": "Cannot delete default backup", + }, + ) + self.controller.management.delete_backup_config(backup_id) + + return self.finish_json(200, {"status": "ok"}) + + def post(self, server_id: str, backup_id: str): + auth_data = self.authenticate_user() + if not auth_data: + return + mask = self.controller.server_perms.get_lowest_api_perm_mask( + self.controller.server_perms.get_user_permissions_mask( + auth_data[4]["user_id"], server_id + ), + auth_data[5], + ) + server_permissions = self.controller.server_perms.get_permissions(mask) + if EnumPermissionsServer.BACKUP not in server_permissions: + # if the user doesn't have Schedule permission, return an error + return self.finish_json( + 400, + { + "status": "error", + "error": "NOT_AUTHORIZED", + "error_data": GENERAL_AUTH_ERROR, + }, + ) + backup_config = self.controller.management.get_backup_config(backup_id) + if backup_config["server_id"]["server_id"] != server_id: + return self.finish_json( + 400, + { + "status": "error", + "error": "ID_MISMATCH", + "error_data": ID_MISMATCH, + }, + ) try: data = json.loads(self.request.body) @@ -61,7 +178,7 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler): 400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)} ) try: - validate(data, backup_schema) + validate(data, BACKUP_SCHEMA) except ValidationError as e: return self.finish_json( 400, @@ -72,9 +189,246 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler): }, ) + svr_obj = self.controller.servers.get_server_obj(server_id) + server_data = self.controller.servers.get_server_data_by_id(server_id) + zip_name = data["filename"] + # import the server again based on zipfile + backup_config = self.controller.management.get_backup_config(backup_id) + backup_location = os.path.join( + backup_config["backup_location"], backup_config["backup_id"] + ) + if Helpers.validate_traversal(backup_location, zip_name): + try: + temp_dir = Helpers.unzip_backup_archive(backup_location, zip_name) + except (FileNotFoundError, NotADirectoryError) as e: + return self.finish_json( + 400, {"status": "error", "error": f"NO BACKUP FOUND {e}"} + ) + if server_data["type"] == "minecraft-java": + new_server = self.controller.restore_java_zip_server( + svr_obj.server_name, + temp_dir, + server_data["executable"], + "1", + "2", + server_data["server_port"], + server_data["created_by"], + ) + elif server_data["type"] == "minecraft-bedrock": + new_server = self.controller.restore_bedrock_zip_server( + svr_obj.server_name, + temp_dir, + server_data["executable"], + server_data["server_port"], + server_data["created_by"], + ) + new_server_id = new_server + new_server = self.controller.servers.get_server_data(new_server) + self.controller.rename_backup_dir( + server_id, + new_server_id, + new_server["server_id"], + ) + # preserve current schedules + for schedule in self.controller.management.get_schedules_by_server( + server_id + ): + job_data = self.controller.management.get_scheduled_task( + schedule.schedule_id + ) + job_data["server_id"] = new_server_id + del job_data["schedule_id"] + self.tasks_manager.update_job(schedule.schedule_id, job_data) + # preserve execution command + new_server_obj = self.controller.servers.get_server_obj(new_server_id) + new_server_obj.execution_command = server_data["execution_command"] + # reset executable path + if svr_obj.path in svr_obj.executable: + new_server_obj.executable = str(svr_obj.executable).replace( + svr_obj.path, new_server_obj.path + ) + # reset run command path + if svr_obj.path in svr_obj.execution_command: + new_server_obj.execution_command = str( + svr_obj.execution_command + ).replace(svr_obj.path, new_server_obj.path) + # reset log path + if svr_obj.path in svr_obj.log_path: + new_server_obj.log_path = str(svr_obj.log_path).replace( + svr_obj.path, new_server_obj.path + ) + self.controller.servers.update_server(new_server_obj) + + # preserve backup config + server_backups = self.controller.management.get_backups_by_server(server_id) + for backup in server_backups: + old_backup_id = server_backups[backup]["backup_id"] + del server_backups[backup]["backup_id"] + server_backups[backup]["server_id"] = new_server_id + if str(server_id) in (server_backups[backup]["backup_location"]): + server_backups[backup]["backup_location"] = str( + server_backups[backup]["backup_location"] + ).replace(str(server_id), str(new_server_id)) + new_backup_id = self.controller.management.add_backup_config( + server_backups[backup] + ) + os.listdir(server_backups[backup]["backup_location"]) + FileHelpers.move_dir( + os.path.join( + server_backups[backup]["backup_location"], old_backup_id + ), + os.path.join( + server_backups[backup]["backup_location"], new_backup_id + ), + ) + # remove old server's tasks + try: + self.tasks_manager.remove_all_server_tasks(server_id) + except JobLookupError as e: + logger.info("No active tasks found for server: {e}") + self.controller.remove_server(server_id, True) + + self.controller.management.add_to_audit_log( + auth_data[4]["user_id"], + f"Restored server {server_id} backup {data['filename']}", + server_id, + self.get_remote_ip(), + ) + + return self.finish_json(200, {"status": "ok"}) + + def patch(self, server_id: str, backup_id: str): + auth_data = self.authenticate_user() + if not auth_data: + return + + try: + data = json.loads(self.request.body) + except json.decoder.JSONDecodeError as e: + return self.finish_json( + 400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)} + ) + + try: + if auth_data[4]["superuser"]: + validate(data, BACKUP_PATCH_SCHEMA) + else: + validate(data, BASIC_BACKUP_PATCH_SCHEMA) + except ValidationError as e: + return self.finish_json( + 400, + { + "status": "error", + "error": "INVALID_JSON_SCHEMA", + "error_data": str(e), + }, + ) + backup_conf = self.controller.management.get_backup_config(backup_id) + if server_id not in [str(x["server_id"]) for x in auth_data[0]]: + # if the user doesn't have access to the server, return an error + return self.finish_json( + 400, + { + "status": "error", + "error": "NOT_AUTHORIZED", + "error_data": GENERAL_AUTH_ERROR, + }, + ) + if backup_conf["server_id"]["server_id"] != server_id: + return self.finish_json( + 400, + { + "status": "error", + "error": "ID_MISMATCH", + "error_data": ID_MISMATCH, + }, + ) + mask = self.controller.server_perms.get_lowest_api_perm_mask( + self.controller.server_perms.get_user_permissions_mask( + auth_data[4]["user_id"], server_id + ), + auth_data[5], + ) + server_permissions = self.controller.server_perms.get_permissions(mask) + if EnumPermissionsServer.BACKUP not in server_permissions: + # if the user doesn't have Schedule permission, return an error + return self.finish_json( + 400, + { + "status": "error", + "error": "NOT_AUTHORIZED", + "error_data": GENERAL_AUTH_ERROR, + }, + ) + self.controller.management.update_backup_config(backup_id, data) + return self.finish_json(200, {"status": "ok"}) + + +class ApiServersServerBackupsBackupFilesIndexHandler(BaseApiHandler): + def delete(self, server_id: str, backup_id: str): + auth_data = self.authenticate_user() + backup_conf = self.controller.management.get_backup_config(backup_id) + if backup_conf["server_id"]["server_id"] != server_id: + return self.finish_json( + 400, + { + "status": "error", + "error": "ID_MISMATCH", + "error_data": ID_MISMATCH, + }, + ) + if not auth_data: + return + mask = self.controller.server_perms.get_lowest_api_perm_mask( + self.controller.server_perms.get_user_permissions_mask( + auth_data[4]["user_id"], server_id + ), + auth_data[5], + ) + server_permissions = self.controller.server_perms.get_permissions(mask) + if EnumPermissionsServer.BACKUP not in server_permissions: + # if the user doesn't have Schedule permission, return an error + return self.finish_json( + 400, + { + "status": "error", + "error": "NOT_AUTHORIZED", + "error_data": GENERAL_AUTH_ERROR, + }, + ) + + try: + data = json.loads(self.request.body) + except json.decoder.JSONDecodeError as e: + return self.finish_json( + 400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)} + ) + try: + validate(data, BACKUP_SCHEMA) + except ValidationError as e: + return self.finish_json( + 400, + { + "status": "error", + "error": "INVALID_JSON_SCHEMA", + "error_data": str(e), + }, + ) + self.helper.validate_traversal( + os.path.join(backup_conf["backup_location"], backup_conf["backup_id"]), + os.path.join( + backup_conf["backup_location"], + backup_conf["backup_id"], + data["filename"], + ), + ) try: FileHelpers.del_file( - os.path.join(backup_conf["backup_path"], data["filename"]) + os.path.join( + backup_conf["backup_location"], + backup_conf["backup_id"], + data["filename"], + ) ) except Exception as e: return self.finish_json( @@ -88,136 +442,3 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler): ) return self.finish_json(200, {"status": "ok"}) - - def post(self, server_id: str): - auth_data = self.authenticate_user() - if not auth_data: - return - mask = self.controller.server_perms.get_lowest_api_perm_mask( - self.controller.server_perms.get_user_permissions_mask( - auth_data[4]["user_id"], server_id - ), - auth_data[5], - ) - server_permissions = self.controller.server_perms.get_permissions(mask) - if EnumPermissionsServer.BACKUP not in server_permissions: - # if the user doesn't have Schedule permission, return an error - return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) - - try: - data = json.loads(self.request.body) - except json.decoder.JSONDecodeError as e: - return self.finish_json( - 400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)} - ) - try: - validate(data, backup_schema) - except ValidationError as e: - return self.finish_json( - 400, - { - "status": "error", - "error": "INVALID_JSON_SCHEMA", - "error_data": str(e), - }, - ) - - try: - svr_obj = self.controller.servers.get_server_obj(server_id) - server_data = self.controller.servers.get_server_data_by_id(server_id) - zip_name = data["filename"] - # import the server again based on zipfile - backup_path = svr_obj.backup_path - if Helpers.validate_traversal(backup_path, zip_name): - temp_dir = Helpers.unzip_backup_archive(backup_path, zip_name) - if server_data["type"] == "minecraft-java": - new_server = self.controller.restore_java_zip_server( - svr_obj.server_name, - temp_dir, - server_data["executable"], - "1", - "2", - server_data["server_port"], - server_data["created_by"], - ) - elif server_data["type"] == "minecraft-bedrock": - new_server = self.controller.restore_bedrock_zip_server( - svr_obj.server_name, - temp_dir, - server_data["executable"], - server_data["server_port"], - server_data["created_by"], - ) - new_server_id = new_server - new_server = self.controller.servers.get_server_data(new_server) - self.controller.rename_backup_dir( - server_id, new_server_id, new_server["server_id"] - ) - # preserve current schedules - for schedule in self.controller.management.get_schedules_by_server( - server_id - ): - job_data = self.controller.management.get_scheduled_task( - schedule.schedule_id - ) - job_data["server_id"] = new_server_id - del job_data["schedule_id"] - self.tasks_manager.update_job(schedule.schedule_id, job_data) - # preserve execution command - new_server_obj = self.controller.servers.get_server_obj(new_server_id) - new_server_obj.execution_command = server_data["execution_command"] - # reset executable path - if svr_obj.path in svr_obj.executable: - new_server_obj.executable = str(svr_obj.executable).replace( - svr_obj.path, new_server_obj.path - ) - # reset run command path - if svr_obj.path in svr_obj.execution_command: - new_server_obj.execution_command = str( - svr_obj.execution_command - ).replace(svr_obj.path, new_server_obj.path) - # reset log path - if svr_obj.path in svr_obj.log_path: - new_server_obj.log_path = str(svr_obj.log_path).replace( - svr_obj.path, new_server_obj.path - ) - self.controller.servers.update_server(new_server_obj) - - # preserve backup config - backup_config = self.controller.management.get_backup_config(server_id) - excluded_dirs = [] - server_obj = self.controller.servers.get_server_obj(server_id) - loop_backup_path = self.helper.wtol_path(server_obj.path) - for item in self.controller.management.get_excluded_backup_dirs( - server_id - ): - item_path = self.helper.wtol_path(item) - bu_path = os.path.relpath(item_path, loop_backup_path) - bu_path = os.path.join(new_server_obj.path, bu_path) - excluded_dirs.append(bu_path) - self.controller.management.set_backup_config( - new_server_id, - new_server_obj.backup_path, - backup_config["max_backups"], - excluded_dirs, - backup_config["compress"], - backup_config["shutdown"], - ) - # remove old server's tasks - try: - self.tasks_manager.remove_all_server_tasks(server_id) - except JobLookupError as e: - logger.info("No active tasks found for server: {e}") - self.controller.remove_server(server_id, True) - except (FileNotFoundError, NotADirectoryError) as e: - return self.finish_json( - 400, {"status": "error", "error": f"NO BACKUP FOUND {e}"} - ) - self.controller.management.add_to_audit_log( - auth_data[4]["user_id"], - f"Restored server {server_id} backup {data['filename']}", - server_id, - self.get_remote_ip(), - ) - - return self.finish_json(200, {"status": "ok"}) diff --git a/app/classes/web/routes/api/servers/server/backups/index.py b/app/classes/web/routes/api/servers/server/backups/index.py index 865fe25a..a155f943 100644 --- a/app/classes/web/routes/api/servers/server/backups/index.py +++ b/app/classes/web/routes/api/servers/server/backups/index.py @@ -1,3 +1,4 @@ +import os import logging import json from jsonschema import validate @@ -10,13 +11,14 @@ logger = logging.getLogger(__name__) backup_patch_schema = { "type": "object", "properties": { - "backup_path": {"type": "string", "minLength": 1}, + "backup_name": {"type": "string", "minLength": 3}, + "backup_location": {"type": "string", "minLength": 1}, "max_backups": {"type": "integer"}, "compress": {"type": "boolean"}, "shutdown": {"type": "boolean"}, - "backup_before": {"type": "string"}, - "backup_after": {"type": "string"}, - "exclusions": {"type": "array"}, + "before": {"type": "string"}, + "after": {"type": "string"}, + "excluded_dirs": {"type": "array"}, }, "additionalProperties": False, "minProperties": 1, @@ -25,12 +27,13 @@ backup_patch_schema = { basic_backup_patch_schema = { "type": "object", "properties": { + "backup_name": {"type": "string", "minLength": 3}, "max_backups": {"type": "integer"}, "compress": {"type": "boolean"}, "shutdown": {"type": "boolean"}, - "backup_before": {"type": "string"}, - "backup_after": {"type": "string"}, - "exclusions": {"type": "array"}, + "before": {"type": "string"}, + "after": {"type": "string"}, + "excluded_dirs": {"type": "array"}, }, "additionalProperties": False, "minProperties": 1, @@ -52,9 +55,11 @@ class ApiServersServerBackupsIndexHandler(BaseApiHandler): if EnumPermissionsServer.BACKUP not in server_permissions: # if the user doesn't have Schedule permission, return an error return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) - self.finish_json(200, self.controller.management.get_backup_config(server_id)) + self.finish_json( + 200, self.controller.management.get_backups_by_server(server_id) + ) - def patch(self, server_id: str): + def post(self, server_id: str): auth_data = self.authenticate_user() if not auth_data: return @@ -80,7 +85,6 @@ class ApiServersServerBackupsIndexHandler(BaseApiHandler): "error_data": str(e), }, ) - if server_id not in [str(x["server_id"]) for x in auth_data[0]]: # if the user doesn't have access to the server, return an error return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) @@ -94,33 +98,12 @@ class ApiServersServerBackupsIndexHandler(BaseApiHandler): if EnumPermissionsServer.BACKUP not in server_permissions: # if the user doesn't have Schedule permission, return an error return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"}) - - self.controller.management.set_backup_config( - server_id, - data.get( - "backup_path", - self.controller.management.get_backup_config(server_id)["backup_path"], - ), - data.get( - "max_backups", - self.controller.management.get_backup_config(server_id)["max_backups"], - ), - data.get("exclusions"), - data.get( - "compress", - self.controller.management.get_backup_config(server_id)["compress"], - ), - data.get( - "shutdown", - self.controller.management.get_backup_config(server_id)["shutdown"], - ), - data.get( - "backup_before", - self.controller.management.get_backup_config(server_id)["before"], - ), - data.get( - "backup_after", - self.controller.management.get_backup_config(server_id)["after"], - ), - ) + # Set the backup location automatically for non-super users. We should probably + # make the default location configurable for SU eventually + if not auth_data[4]["superuser"]: + data["backup_location"] = os.path.join(self.helper.backup_path, server_id) + data["server_id"] = server_id + if not data.get("excluded_dirs", None): + data["excluded_dirs"] = [] + self.controller.management.add_backup_config(data) return self.finish_json(200, {"status": "ok"}) diff --git a/app/classes/web/routes/api/servers/server/files.py b/app/classes/web/routes/api/servers/server/files.py index 2951ff25..2699ae0c 100644 --- a/app/classes/web/routes/api/servers/server/files.py +++ b/app/classes/web/routes/api/servers/server/files.py @@ -72,7 +72,7 @@ file_delete_schema = { class ApiServersServerFilesIndexHandler(BaseApiHandler): - def post(self, server_id: str): + def post(self, server_id: str, backup_id=None): auth_data = self.authenticate_user() if not auth_data: return @@ -149,21 +149,35 @@ class ApiServersServerFilesIndexHandler(BaseApiHandler): filename = html.escape(raw_filename) rel = os.path.join(folder, raw_filename) dpath = os.path.join(folder, filename) - if str(dpath) in self.controller.management.get_excluded_backup_dirs( - server_id - ): - if os.path.isdir(rel): - return_json[filename] = { - "path": dpath, - "dir": True, - "excluded": True, - } + if backup_id: + if str( + dpath + ) in self.controller.management.get_excluded_backup_dirs(backup_id): + if os.path.isdir(rel): + return_json[filename] = { + "path": dpath, + "dir": True, + "excluded": True, + } + else: + return_json[filename] = { + "path": dpath, + "dir": False, + "excluded": True, + } else: - return_json[filename] = { - "path": dpath, - "dir": False, - "excluded": True, - } + if os.path.isdir(rel): + return_json[filename] = { + "path": dpath, + "dir": True, + "excluded": False, + } + else: + return_json[filename] = { + "path": dpath, + "dir": False, + "excluded": False, + } else: if os.path.isdir(rel): return_json[filename] = { @@ -189,7 +203,7 @@ class ApiServersServerFilesIndexHandler(BaseApiHandler): ) self.finish_json(200, {"status": "ok", "data": file_contents}) - def delete(self, server_id: str): + def delete(self, server_id: str, _backup_id=None): auth_data = self.authenticate_user() if not auth_data: return @@ -247,7 +261,7 @@ class ApiServersServerFilesIndexHandler(BaseApiHandler): return self.finish_json(200, {"status": "ok"}) return self.finish_json(500, {"status": "error", "error": str(proc)}) - def patch(self, server_id: str): + def patch(self, server_id: str, _backup_id): auth_data = self.authenticate_user() if not auth_data: return @@ -301,7 +315,7 @@ class ApiServersServerFilesIndexHandler(BaseApiHandler): file_object.write(file_contents) return self.finish_json(200, {"status": "ok"}) - def put(self, server_id: str): + def put(self, server_id: str, _backup_id): auth_data = self.authenticate_user() if not auth_data: return diff --git a/app/classes/web/routes/api/servers/server/tasks/index.py b/app/classes/web/routes/api/servers/server/tasks/index.py index 0c03319c..ed8b9df9 100644 --- a/app/classes/web/routes/api/servers/server/tasks/index.py +++ b/app/classes/web/routes/api/servers/server/tasks/index.py @@ -21,6 +21,9 @@ new_task_schema = { "action": { "type": "string", }, + "action_id": { + "type": "string", + }, "interval": {"type": "integer"}, "interval_type": { "type": "string", @@ -110,6 +113,18 @@ class ApiServersServerTasksIndexHandler(BaseApiHandler): ) if "parent" not in data: data["parent"] = None + if data.get("action_id"): + backup_config = self.controller.management.get_backup_config( + data["action_id"] + ) + if backup_config["server_id"]["server_id"] != server_id: + return self.finish_json( + 405, + { + "status": "error", + "error": "Server ID Mismatch", + }, + ) task_id = self.tasks_manager.schedule_job(data) self.controller.management.add_to_audit_log( diff --git a/app/classes/web/routes/api/servers/server/tasks/task/index.py b/app/classes/web/routes/api/servers/server/tasks/task/index.py index dac60762..05c8cee9 100644 --- a/app/classes/web/routes/api/servers/server/tasks/task/index.py +++ b/app/classes/web/routes/api/servers/server/tasks/task/index.py @@ -22,6 +22,9 @@ task_patch_schema = { "action": { "type": "string", }, + "action_id": { + "type": "string", + }, "interval": {"type": "integer"}, "interval_type": { "type": "string", diff --git a/app/frontend/static/assets/css/crafty.css b/app/frontend/static/assets/css/crafty.css index 43dd2e6a..ce28bab3 100644 --- a/app/frontend/static/assets/css/crafty.css +++ b/app/frontend/static/assets/css/crafty.css @@ -12,6 +12,16 @@ nav.sidebar { position: fixed; } +td { + -ms-overflow-style: none; + /* IE and Edge */ + scrollbar-width: none; + /* Firefox */ +} + +td::-webkit-scrollbar { + display: none; +} @media (min-width: 992px) { nav.sidebar { @@ -267,4 +277,8 @@ div.warnings div.wssError a:hover { font-family: 'Sarabun', 'roboto', sans-serif; } -/**************************************************************/ \ No newline at end of file +/**************************************************************/ + +.hidden-input { + margin-left: -40px; +} \ No newline at end of file diff --git a/app/frontend/templates/panel/server_backup.html b/app/frontend/templates/panel/server_backup.html index 2a9263ba..86cd9415 100644 --- a/app/frontend/templates/panel/server_backup.html +++ b/app/frontend/templates/panel/server_backup.html @@ -39,208 +39,152 @@ {% include "parts/m_server_controls_list.html %} -
-
-
-
- {% if data['backing_up'] %} -
-
{{ - data['backup_stats']['percent'] }}%
-
-

Backing up {{data['server_stats']['world_size']}}

- {% end %} - -
- {% if not data['backing_up'] %} -
- -
- {% end %} -
-
- {% if data['super_user'] %} - - +
+
+
+

{{ translate('serverBackups', 'backups', + data['lang']) }}

+ {% if data['user_data']['hints'] %} + {% end %} -
- -
- - -
-
- - {% if data['backup_config']['compress'] %} - {{ translate('serverBackups', 'compress', data['lang']) }} - {% else %} - {{ - translate('serverBackups', 'compress', data['lang']) }} - {% end %} -
-
- - {% if data['backup_config']['shutdown'] %} - {{ translate('serverBackups', 'shutdown', data['lang']) }} - {% else %} - {{ - translate('serverBackups', 'shutdown', data['lang']) }} - {% end %} -
-
- - {% if data['backup_config']['before'] %} - {{ - translate('serverBackups', 'before', data['lang']) }} -
- - {% else %} - {{ - translate('serverBackups', 'before', data['lang']) }} -
- - {% end %} -
-
- - {% if data['backup_config']['after'] %} - {{ - translate('serverBackups', 'after', data['lang']) }} -
- - {% else %} - {{ - translate('serverBackups', 'after', data['lang']) }} -
- - {% end %} -
-
- -
- -
- - -
-
- - -

{{ translate('serverBackups', 'currentBackups', data['lang']) }}

- - - - - - - - - {% for backup in data['backup_list'] %} - - - - - - {% end %} - - -
{{ translate('serverBackups', 'options', data['lang']) }}{{ translate('serverBackups', 'path', data['lang']) }}{{ translate('serverBackups', 'size', data['lang']) }}
- - - {{ translate('serverBackups', 'download', data['lang']) }} - -
-
- - -
{{ backup['path'] }}{{ backup['size'] }}
- +
+ {% if len(data['backups']) == 0 %} +
+ {{ translate('serverBackups', 'no-backup', data['lang']) }} {{ + translate('serverBackups', 'newBackup',data['lang']) }}. +
+ {% end %} + {% if len(data['backups']) > 0 %} +
+ + + + + + + + + + + + {% for backup in data['backups'] %} + + + + + + + + {% end %} + +
{{ translate('serverBackups', 'name', + data['lang']) }} {{ translate('serverBackups', 'status', + data['lang']) }} {{ translate('serverBackups', + 'storageLocation', data['lang']) }}{{ translate('serverBackups', + 'maxBackups', data['lang']) }}{{ translate('serverBackups', 'actions', + data['lang']) }}
+

{{backup.backup_name}}

+
+ {% if backup.default %} + {{ translate('serverBackups', 'default', + data['lang']) }} + {% end %} +
+
+ +
+
+

{{backup.backup_location}}

+
+

{{backup.max_backups}}

+
+ + {% if not backup.default %} + + {% end %} + +
+
+
+ + + + + + + + + {% for backup in data['backups'] %} + + + + + {% end %} + +
Name + {{ translate('serverBackups', 'edit', data['lang']) + }}
+

{{backup.backup_name}}

+
+
+ +
+
+ {% if backup.default %} + {{ translate('serverBackups', 'default', + data['lang']) }} + {% end %} +
+ + {% if not backup.default %} + + {% end %} + +
+
+ {% end %} +
-
-
-
-
-

{{ translate('serverBackups', 'excludedBackups', - data['lang']) }}

-
-
-
    - {% for item in data['exclusions'] %} -
  • {{item}}
  • -
    - {% end %} -
-
@@ -298,7 +242,7 @@ {% block js %} + +{% end %} \ No newline at end of file diff --git a/app/frontend/templates/panel/server_schedule_edit.html b/app/frontend/templates/panel/server_schedule_edit.html index 7b116f7f..50b48b10 100644 --- a/app/frontend/templates/panel/server_schedule_edit.html +++ b/app/frontend/templates/panel/server_schedule_edit.html @@ -79,6 +79,24 @@ +
@@ -232,7 +250,7 @@ } function replacer(key, value) { - if (key != "start_time" && key != "cron_string" && key != "interval_type") { + if (key != "start_time" && key != "cron_string" && key != "interval_type" && key != "action_id") { if (typeof value == "boolean") { return value } @@ -247,7 +265,7 @@ } } else if (value === "" && key == "start_time"){ return "00:00"; - }else{ + }else { return value; } } @@ -281,6 +299,11 @@ // Format the plain form data as JSON let formDataJsonString = JSON.stringify(formDataObject, replacer); + let data = JSON.parse(formDataJsonString) + if (data["action"] === "backup" && !data["action_id"]){ + return bootbox.alert("Validation Failed") + } + let res = await fetch(`/api/v2/servers/${serverId}/tasks/`, { method: 'POST', headers: { @@ -358,6 +381,14 @@ document.getElementById("ifYes").style.display = "none"; document.getElementById("command_input").required = false; } + if (document.getElementById('action').value == "backup"){ + document.getElementById("ifBackup").style.display = "block"; + document.getElementById("action_id").required = true; + } else { + document.getElementById("ifBackup").style.display = "none"; + document.getElementById("action_id").required = false; + $("#action_id").val(null); + } } function basicAdvanced() { if (document.getElementById('difficulty').value == "advanced") { diff --git a/app/migrations/20240217_rework_servers_uuid_part2.py b/app/migrations/20240217_rework_servers_uuid_part2.py index a4b01c45..010da2e6 100644 --- a/app/migrations/20240217_rework_servers_uuid_part2.py +++ b/app/migrations/20240217_rework_servers_uuid_part2.py @@ -5,13 +5,7 @@ import logging from app.classes.shared.console import Console from app.classes.shared.migration import Migrator, MigrateHistory -from app.classes.models.management import ( - Webhooks, - Schedules, - Backups, -) -from app.classes.models.server_permissions import RoleServers -from app.classes.models.base_model import BaseModel +from app.classes.models.roles import Roles logger = logging.getLogger(__name__) @@ -53,6 +47,78 @@ def migrate(migrator: Migrator, database, **kwargs): table_name = "servers" database = db + # ********************************************************************************** + # Role Servers Class + # ********************************************************************************** + class RoleServers(peewee.Model): + role_id = peewee.ForeignKeyField(Roles, backref="role_server") + server_id = peewee.ForeignKeyField(Servers, backref="role_server") + permissions = peewee.CharField(default="00000000") + + class Meta: + table_name = "role_servers" + primary_key = peewee.CompositeKey("role_id", "server_id") + database = db + + # ********************************************************************************** + # Webhooks Class + # ********************************************************************************** + class Webhooks(peewee.Model): + id = peewee.AutoField() + server_id = peewee.ForeignKeyField(Servers, backref="webhook_server", null=True) + name = peewee.CharField(default="Custom Webhook", max_length=64) + url = peewee.CharField(default="") + webhook_type = peewee.CharField(default="Custom") + bot_name = peewee.CharField(default="Crafty Controller") + trigger = peewee.CharField(default="server_start,server_stop") + body = peewee.CharField(default="") + color = peewee.CharField(default="#005cd1") + enabled = peewee.BooleanField(default=True) + + class Meta: + table_name = "webhooks" + database = db + + # ********************************************************************************** + # Schedules Class + # ********************************************************************************** + class Schedules(peewee.Model): + schedule_id = peewee.IntegerField(unique=True, primary_key=True) + server_id = peewee.ForeignKeyField(Servers, backref="schedule_server") + enabled = peewee.BooleanField() + action = peewee.CharField() + interval = peewee.IntegerField() + interval_type = peewee.CharField() + start_time = peewee.CharField(null=True) + command = peewee.CharField(null=True) + name = peewee.CharField() + one_time = peewee.BooleanField(default=False) + cron_string = peewee.CharField(default="") + parent = peewee.IntegerField(null=True) + delay = peewee.IntegerField(default=0) + next_run = peewee.CharField(default="") + + class Meta: + table_name = "schedules" + database = db + + # ********************************************************************************** + # Backups Class + # ********************************************************************************** + class Backups(peewee.Model): + excluded_dirs = peewee.CharField(null=True) + max_backups = peewee.IntegerField() + max_backups = peewee.IntegerField() + server_id = peewee.ForeignKeyField(Servers, backref="backups_server") + compress = peewee.BooleanField(default=False) + shutdown = peewee.BooleanField(default=False) + before = peewee.CharField(default="") + after = peewee.CharField(default="") + + class Meta: + table_name = "backups" + database = db + this_migration = MigrateHistory.get_or_none( MigrateHistory.name == "20240217_rework_servers_uuid_part2" ) @@ -70,8 +136,8 @@ def migrate(migrator: Migrator, database, **kwargs): return try: - logger.info("Migrating Data from Int to UUID (Foreign Keys)") - Console.info("Migrating Data from Int to UUID (Foreign Keys)") + logger.debug("Migrating Data from Int to UUID (Foreign Keys)") + Console.debug("Migrating Data from Int to UUID (Foreign Keys)") # Changes on Webhooks Log Table for webhook in Webhooks.select(): @@ -122,8 +188,8 @@ def migrate(migrator: Migrator, database, **kwargs): and RoleServers.server_id == old_server_id ).execute() - logger.info("Migrating Data from Int to UUID (Foreign Keys) : SUCCESS") - Console.info("Migrating Data from Int to UUID (Foreign Keys) : SUCCESS") + logger.debug("Migrating Data from Int to UUID (Foreign Keys) : SUCCESS") + Console.debug("Migrating Data from Int to UUID (Foreign Keys) : SUCCESS") except Exception as ex: logger.error("Error while migrating Data from Int to UUID (Foreign Keys)") @@ -135,16 +201,16 @@ def migrate(migrator: Migrator, database, **kwargs): return try: - logger.info("Migrating Data from Int to UUID (Primary Keys)") - Console.info("Migrating Data from Int to UUID (Primary Keys)") + logger.debug("Migrating Data from Int to UUID (Primary Keys)") + Console.debug("Migrating Data from Int to UUID (Primary Keys)") # Migrating servers from the old id type to the new one for server in Servers.select(): Servers.update(server_id=server.server_uuid).where( Servers.server_id == server.server_id ).execute() - logger.info("Migrating Data from Int to UUID (Primary Keys) : SUCCESS") - Console.info("Migrating Data from Int to UUID (Primary Keys) : SUCCESS") + logger.debug("Migrating Data from Int to UUID (Primary Keys) : SUCCESS") + Console.debug("Migrating Data from Int to UUID (Primary Keys) : SUCCESS") except Exception as ex: logger.error("Error while migrating Data from Int to UUID (Primary Keys)") @@ -203,9 +269,81 @@ def rollback(migrator: Migrator, database, **kwargs): table_name = "servers" database = db + # ********************************************************************************** + # Role Servers Class + # ********************************************************************************** + class RoleServers(peewee.Model): + role_id = peewee.ForeignKeyField(Roles, backref="role_server") + server_id = peewee.ForeignKeyField(Servers, backref="role_server") + permissions = peewee.CharField(default="00000000") + + class Meta: + table_name = "role_servers" + primary_key = peewee.CompositeKey("role_id", "server_id") + database = db + + # ********************************************************************************** + # Webhooks Class + # ********************************************************************************** + class Webhooks(peewee.Model): + id = peewee.AutoField() + server_id = peewee.ForeignKeyField(Servers, backref="webhook_server", null=True) + name = peewee.CharField(default="Custom Webhook", max_length=64) + url = peewee.CharField(default="") + webhook_type = peewee.CharField(default="Custom") + bot_name = peewee.CharField(default="Crafty Controller") + trigger = peewee.CharField(default="server_start,server_stop") + body = peewee.CharField(default="") + color = peewee.CharField(default="#005cd1") + enabled = peewee.BooleanField(default=True) + + class Meta: + table_name = "webhooks" + database = db + + # ********************************************************************************** + # Schedules Class + # ********************************************************************************** + class Schedules(peewee.Model): + schedule_id = peewee.IntegerField(unique=True, primary_key=True) + server_id = peewee.ForeignKeyField(Servers, backref="schedule_server") + enabled = peewee.BooleanField() + action = peewee.CharField() + interval = peewee.IntegerField() + interval_type = peewee.CharField() + start_time = peewee.CharField(null=True) + command = peewee.CharField(null=True) + name = peewee.CharField() + one_time = peewee.BooleanField(default=False) + cron_string = peewee.CharField(default="") + parent = peewee.IntegerField(null=True) + delay = peewee.IntegerField(default=0) + next_run = peewee.CharField(default="") + + class Meta: + table_name = "schedules" + database = db + + # ********************************************************************************** + # Backups Class + # ********************************************************************************** + class Backups(peewee.Model): + excluded_dirs = peewee.CharField(null=True) + max_backups = peewee.IntegerField() + max_backups = peewee.IntegerField() + server_id = peewee.ForeignKeyField(Servers, backref="backups_server") + compress = peewee.BooleanField(default=False) + shutdown = peewee.BooleanField(default=False) + before = peewee.CharField(default="") + after = peewee.CharField(default="") + + class Meta: + table_name = "backups" + database = db + try: - logger.info("Migrating Data from UUID to Int (Primary Keys)") - Console.info("Migrating Data from UUID to Int (Primary Keys)") + logger.debug("Migrating Data from UUID to Int (Primary Keys)") + Console.debug("Migrating Data from UUID to Int (Primary Keys)") # Migrating servers from the old id type to the new one new_id = 0 for server in Servers.select(): @@ -217,8 +355,8 @@ def rollback(migrator: Migrator, database, **kwargs): Servers.server_id == server.server_id ).execute() - logger.info("Migrating Data from UUID to Int (Primary Keys) : SUCCESS") - Console.info("Migrating Data from UUID to Int (Primary Keys) : SUCCESS") + logger.debug("Migrating Data from UUID to Int (Primary Keys) : SUCCESS") + Console.debug("Migrating Data from UUID to Int (Primary Keys) : SUCCESS") except Exception as ex: logger.error("Error while migrating Data from UUID to Int (Primary Keys)") @@ -230,8 +368,8 @@ def rollback(migrator: Migrator, database, **kwargs): return try: - logger.info("Migrating Data from UUID to Int (Foreign Keys)") - Console.info("Migrating Data from UUID to Int (Foreign Keys)") + logger.debug("Migrating Data from UUID to Int (Foreign Keys)") + Console.debug("Migrating Data from UUID to Int (Foreign Keys)") # Changes on Webhooks Log Table for webhook in Webhooks.select(): old_server_id = webhook.server_id_id @@ -281,8 +419,8 @@ def rollback(migrator: Migrator, database, **kwargs): and RoleServers.server_id == old_server_id ).execute() - logger.info("Migrating Data from UUID to Int (Foreign Keys) : SUCCESS") - Console.info("Migrating Data from UUID to Int (Foreign Keys) : SUCCESS") + logger.debug("Migrating Data from UUID to Int (Foreign Keys) : SUCCESS") + Console.debug("Migrating Data from UUID to Int (Foreign Keys) : SUCCESS") except Exception as ex: logger.error("Error while migrating Data from UUID to Int (Foreign Keys)") diff --git a/app/migrations/20240308_multi-backup.py b/app/migrations/20240308_multi-backup.py new file mode 100644 index 00000000..db154ec8 --- /dev/null +++ b/app/migrations/20240308_multi-backup.py @@ -0,0 +1,238 @@ +import os +import datetime +import uuid +import peewee +import logging + + +from app.classes.shared.helpers import Helpers +from app.classes.shared.console import Console +from app.classes.shared.migration import Migrator +from app.classes.shared.file_helpers import FileHelpers + +logger = logging.getLogger(__name__) + + +def migrate(migrator: Migrator, database, **kwargs): + """ + Write your migrations here. + """ + db = database + Console.info("Starting Backups migrations") + Console.info( + "Migrations: Adding columns [backup_id, " + "backup_name, backup_location, enabled, default, action_id, backup_status]" + ) + migrator.add_columns( + "backups", + backup_id=peewee.CharField(default=Helpers.create_uuid), + ) + migrator.add_columns("backups", backup_name=peewee.CharField(default="Default")) + migrator.add_columns("backups", backup_location=peewee.CharField(default="")) + migrator.add_columns("backups", enabled=peewee.BooleanField(default=True)) + migrator.add_columns("backups", default=peewee.BooleanField(default=False)) + migrator.add_columns( + "backups", + status=peewee.CharField(default='{"status": "Standby", "message": ""}'), + ) + migrator.add_columns( + "schedules", action_id=peewee.CharField(null=True, default=None) + ) + + class Servers(peewee.Model): + server_id = peewee.CharField(primary_key=True, default=str(uuid.uuid4())) + created = peewee.DateTimeField(default=datetime.datetime.now) + server_name = peewee.CharField(default="Server", index=True) + path = peewee.CharField(default="") + backup_path = peewee.CharField(default="") + executable = peewee.CharField(default="") + log_path = peewee.CharField(default="") + execution_command = peewee.CharField(default="") + auto_start = peewee.BooleanField(default=0) + auto_start_delay = peewee.IntegerField(default=10) + crash_detection = peewee.BooleanField(default=0) + stop_command = peewee.CharField(default="stop") + executable_update_url = peewee.CharField(default="") + server_ip = peewee.CharField(default="127.0.0.1") + server_port = peewee.IntegerField(default=25565) + logs_delete_after = peewee.IntegerField(default=0) + type = peewee.CharField(default="minecraft-java") + show_status = peewee.BooleanField(default=1) + created_by = peewee.IntegerField(default=-100) + shutdown_timeout = peewee.IntegerField(default=60) + ignored_exits = peewee.CharField(default="0") + + class Meta: + table_name = "servers" + database = db + + class Backups(peewee.Model): + backup_id = peewee.CharField(primary_key=True, default=Helpers.create_uuid) + backup_name = peewee.CharField(default="New Backup") + backup_location = peewee.CharField(default="") + excluded_dirs = peewee.CharField(null=True) + max_backups = peewee.IntegerField() + server_id = peewee.ForeignKeyField(Servers, backref="backups_server") + compress = peewee.BooleanField(default=False) + shutdown = peewee.BooleanField(default=False) + before = peewee.CharField(default="") + after = peewee.CharField(default="") + default = peewee.BooleanField(default=False) + status = peewee.CharField(default='{"status": "Standby", "message": ""}') + enabled = peewee.BooleanField(default=True) + + class Meta: + table_name = "backups" + database = db + + class NewBackups(peewee.Model): + backup_id = peewee.CharField(primary_key=True, default=Helpers.create_uuid) + backup_name = peewee.CharField(default="New Backup") + backup_location = peewee.CharField(default="") + excluded_dirs = peewee.CharField(null=True) + max_backups = peewee.IntegerField() + server_id = peewee.ForeignKeyField(Servers, backref="backups_server") + compress = peewee.BooleanField(default=False) + shutdown = peewee.BooleanField(default=False) + before = peewee.CharField(default="") + after = peewee.CharField(default="") + default = peewee.BooleanField(default=False) + status = peewee.CharField(default='{"status": "Standby", "message": ""}') + enabled = peewee.BooleanField(default=True) + + class Meta: + table_name = "new_backups" + database = db + + class Schedules(peewee.Model): + schedule_id = peewee.IntegerField(unique=True, primary_key=True) + server_id = peewee.ForeignKeyField(Servers, backref="schedule_server") + enabled = peewee.BooleanField() + action = peewee.CharField() + interval = peewee.IntegerField() + interval_type = peewee.CharField() + start_time = peewee.CharField(null=True) + command = peewee.CharField(null=True) + action_id = peewee.CharField(null=True) + name = peewee.CharField() + one_time = peewee.BooleanField(default=False) + cron_string = peewee.CharField(default="") + parent = peewee.IntegerField(null=True) + delay = peewee.IntegerField(default=0) + next_run = peewee.CharField(default="") + + class Meta: + table_name = "schedules" + database = db + + class NewSchedules(peewee.Model): + schedule_id = peewee.IntegerField(unique=True, primary_key=True) + server_id = peewee.ForeignKeyField(Servers, backref="schedule_server") + enabled = peewee.BooleanField() + action = peewee.CharField() + interval = peewee.IntegerField() + interval_type = peewee.CharField() + start_time = peewee.CharField(null=True) + command = peewee.CharField(null=True) + action_id = peewee.CharField(null=True) + name = peewee.CharField() + one_time = peewee.BooleanField(default=False) + cron_string = peewee.CharField(default="") + parent = peewee.IntegerField(null=True) + delay = peewee.IntegerField(default=0) + next_run = peewee.CharField(default="") + + class Meta: + table_name = "new_schedules" + database = db + + migrator.create_table(NewBackups) + migrator.create_table(NewSchedules) + + migrator.run() + + # Copy data from the existing backups table to the new one + for backup in Backups.select(): + # Fetch the related server entry from the Servers table + server = Servers.get(Servers.server_id == backup.server_id) + Console.info(f"Migrations: Migrating backup for server {server.server_name}") + # Create a new backup entry with data from the + # old backup entry and related server + new_backup = NewBackups.create( + backup_name=f"{server.server_name} Backup", + # Set backup_location equal to backup_path + backup_location=server.backup_path, + excluded_dirs=backup.excluded_dirs, + max_backups=backup.max_backups, + server_id=server.server_id, + compress=backup.compress, + shutdown=backup.shutdown, + before=backup.before, + after=backup.after, + default=True, + enabled=True, + ) + Helpers.ensure_dir_exists( + os.path.join(server.backup_path, new_backup.backup_id) + ) + for file in os.listdir(server.backup_path): + if not os.path.isdir(os.path.join(os.path.join(server.backup_path, file))): + FileHelpers.move_file( + os.path.join(server.backup_path, file), + os.path.join(server.backup_path, new_backup.backup_id, file), + ) + + Console.debug("Migrations: Dropping old backup table") + # Drop the existing backups table + migrator.drop_table("backups") + + Console.debug("Migrations: Renaming new_backups to backups") + # Rename the new table to backups + migrator.rename_table("new_backups", "backups") + + Console.debug("Migrations: Dropping backup_path from servers table") + migrator.drop_columns("servers", ["backup_path"]) + + for schedule in Schedules.select(): + action_id = None + if schedule.command == "backup_server": + Console.info( + f"Migrations: Adding backup ID to task with name {schedule.name}" + ) + backup = NewBackups.get(NewBackups.server_id == schedule.server_id) + action_id = backup.backup_id + NewSchedules.create( + schedule_id=schedule.schedule_id, + server_id=schedule.server_id, + enabled=schedule.enabled, + action=schedule.action, + interval=schedule.interval, + interval_type=schedule.interval_type, + start_time=schedule.start_time, + command=schedule.command, + action_id=action_id, + name=schedule.name, + one_time=schedule.one_time, + cron_string=schedule.cron_string, + parent=schedule.parent, + delay=schedule.delay, + next_run=schedule.next_run, + ) + + Console.debug("Migrations: dropping old schedules table") + # Drop the existing backups table + migrator.drop_table("schedules") + + Console.debug("Migrations: renaming new_schedules to schedules") + # Rename the new table to backups + migrator.rename_table("new_schedules", "schedules") + + +def rollback(migrator: Migrator, database, **kwargs): + """ + Write your rollback migrations here. + """ + db = database + + migrator.drop_columns("backups", ["name", "backup_id", "backup_location"]) + migrator.add_columns("servers", backup_path=peewee.CharField(default="")) diff --git a/app/translations/cs_CS.json b/app/translations/cs_CS.json index 142593df..0ce6687d 100644 --- a/app/translations/cs_CS.json +++ b/app/translations/cs_CS.json @@ -321,10 +321,12 @@ "serversDesc": "servery, ke kterým má tato role přístup" }, "serverBackups": { + "actions": "Akce", "after": "Spustit příkaz po záloze", "backupAtMidnight": "Automatické zálohování o půlnoci?", "backupNow": "Zálohovat nyní!", "backupTask": "Bylo spuštěno zálohování.", + "backups": "Zálohy serverů", "before": "Spustit příkaz před zálohou", "cancel": "Zrušit", "clickExclude": "Kliknutím vyberete výjimku", @@ -333,21 +335,34 @@ "confirmDelete": "Chcete tuto zálohu odstranit? Tuto akci nelze vrátit zpět.", "confirmRestore": "Jste si jisti, že chcete provést obnovu z této zálohy. Všechny aktuální soubory serveru se změní na stav zálohy a nebude možné je obnovit.", "currentBackups": "Aktuální zálohy", + "default": "Defaultní záloha", + "defaultExplain": "Tuto zálohu Crafty používalo před aktualizací. Nemůžete ji změnit nebo smazat", "delete": "Smazat", "destroyBackup": "Zničit zálohu \" + file_to_del + \"?", "download": "Stáhnout", + "edit": "upravit", + "enabled": "Povoleno", "excludedBackups": "Vyloučené cesty: ", "excludedChoose": "Vyberte cesty, které chcete ze zálohování vyloučit.", "exclusionsTitle": "Vyloučení ze zálohování", + "failed": "Selhalo", "maxBackups": "Maximální počet záloh", "maxBackupsDesc": "Crafty neuloží více než N záloh a odstraní nejstarší (zadejte 0 pro zachování všech).", + "myBackup": "Moje nová záloha", + "name": "Jméno", + "newBackup": "Vytvořit novou zálohu", + "no-backup": "Žádné zálohy. Pro vytvoření nové zálohy zmáčkněte prosím. Vytvořit novou zálohu", "options": "Nastavení", "path": "Cesta", "restore": "Obnovit", "restoring": "Obnovení zálohy. To může chvíli trvat. Buďte prosím trpěliví.", + "run": "Nastartovat zálohu", "save": "Uložit", "shutdown": "Vypnout server po dobu zálohování", "size": "Velikost", + "standby": "V pohotovosti", + "status": "Stav", + "storage": "Lokace uložiště", "storageLocation": "Umístění úložiště", "storageLocationDesc": "Kam chcete ukládat zálohy?" }, @@ -512,6 +527,7 @@ }, "serverSchedules": { "action": "Akce", + "actionId": "Vyberte zálohu na které se to má potvrdit!", "areYouSure": "Odstranění naplánované úlohy?", "cancel": "Zrušit", "cannotSee": "Nevidíte všechno?", diff --git a/app/translations/de_DE.json b/app/translations/de_DE.json index 41242532..88c8002e 100644 --- a/app/translations/de_DE.json +++ b/app/translations/de_DE.json @@ -301,10 +301,12 @@ "serversDesc": "Server, auf die Nutzer mit dieser Rolle zugreifen darf" }, "serverBackups": { + "actions": "Aktionen", "after": "Befehl nach dem Backup ausführen", "backupAtMidnight": "Automatisches Backup um 24:00 Uhr?", "backupNow": "Jetzt sichern!", "backupTask": "Ein Backup-Auftrag wurde gestartet.", + "backups": "Server-Backups", "before": "Befehl vor dem Backup ausführen", "cancel": "Abbrechen", "clickExclude": "Auswählen, um Ausnahmen zu markieren", @@ -313,21 +315,34 @@ "confirmDelete": "Möchten Sie diese Backup-Datei löschen? Dies kann nicht rückgängig gemacht werden.", "confirmRestore": "Sicher, dass dieses Backup wiederherstellgestellt werden soll? Alle aktuellen Serverdateien werden in den Zustand von diesem Backup versetzt und können nicht wiederhergestellt werden.", "currentBackups": "Aktuelle Backups", + "default": "Standard-Backup", + "defaultExplain": "Das Backup, welches Crafty vor Updates verwendet. Dies kann nicht geändert oder gelöscht werden.", "delete": "Löschen", "destroyBackup": "Backup löschen \" + file_to_del + \"?", "download": "Herunterladen", + "edit": "Bearbeiten", + "enabled": "Aktiviert", "excludedBackups": "Ausgeschlossene Verzeichnisse: ", "excludedChoose": "Verzeichnisse auswählen, die nicht gesichert werden sollen", "exclusionsTitle": "Backup Ausnahmen", + "failed": "Fehlgeschlagen", "maxBackups": "Maximale Backups", "maxBackupsDesc": "Crafty speichert nicht mehr als N Backups, wodurch das älteste gelöscht wird (geben Sie 0 ein, um alle zu behalten)", + "myBackup": "Mein Neues Backup", + "name": "Name", + "newBackup": "Neues Backup erstellen", + "no-backup": "Keine Backups. Um eine neue Backup-Konfiguration zu erstellen, bitte auf 'Neues Backup erstellen' klicken.", "options": "Optionen", "path": "Pfad", "restore": "Wiederherstellen", "restoring": "Backup wiederherstellen. Dies kann eine Weile dauern.", + "run": "Backup erstellen", "save": "Speichern", "shutdown": "Server für die Dauer des Backups stoppen", "size": "Größe", + "standby": "Bereitschaft", + "status": "Status", + "storage": "Speicherort", "storageLocation": "Speicherort", "storageLocationDesc": "Wo wollen Sie die Backups speichern?" }, @@ -492,6 +507,7 @@ }, "serverSchedules": { "action": "Aktion", + "actionId": "Aktion auswählen", "areYouSure": "Geplante Aufgabe löschen?", "cancel": "Abbrechen", "cannotSee": "Nicht alles sichtbar?", diff --git a/app/translations/en_EN.json b/app/translations/en_EN.json index 5a82408a..7cfe3c3a 100644 --- a/app/translations/en_EN.json +++ b/app/translations/en_EN.json @@ -298,10 +298,12 @@ "serversDesc": "servers this role is allowed to access" }, "serverBackups": { + "actions": "Actions", "after": "Run command after backup", "backupAtMidnight": "Auto-backup at midnight?", "backupNow": "Backup Now!", "backupTask": "A backup task has been started.", + "backups": "Server Backups", "before": "Run command before backup", "cancel": "Cancel", "clickExclude": "Click to select Exclusions", @@ -310,21 +312,34 @@ "confirmDelete": "Do you want to delete this backup? This cannot be undone.", "confirmRestore": "Are you sure you want to restore from this backup. All current server files will changed to backup state and will be unrecoverable.", "currentBackups": "Current Backups", + "default": "Default Backup", + "defaultExplain": "The backup that Crafty will use before updates. This cannot be changed or deleted.", "delete": "Delete", "destroyBackup": "Destroy backup \" + file_to_del + \"?", "download": "Download", + "edit": "Edit", + "enabled": "Enabled", "excludedBackups": "Excluded Paths: ", "excludedChoose": "Choose the paths you wish to exclude from your backups", "exclusionsTitle": "Backup Exclusions", + "failed": "Failed", "maxBackups": "Max Backups", "maxBackupsDesc": "Crafty will not store more than N backups, deleting the oldest (enter 0 to keep all)", + "myBackup": "My New Backup", + "name": "Name", + "newBackup": "Create New Backup", + "no-backup": "No Backups. To make a new backup configuration please press. New Backup", "options": "Options", "path": "Path", "restore": "Restore", "restoring": "Restoring Backup. This may take a while. Please be patient.", + "run": "Run Backup", "save": "Save", "shutdown": "Shutdown server for duration of backup", "size": "Size", + "standby": "Standby", + "status": "Status", + "storage": "Storage Location", "storageLocation": "Storage Location", "storageLocationDesc": "Where do you want to store backups?" }, @@ -489,6 +504,7 @@ }, "serverSchedules": { "action": "Action", + "actionId": "Select Action Child", "areYouSure": "Delete Scheduled Task?", "cancel": "Cancel", "cannotSee": "Not seeing everything?", diff --git a/app/translations/es_ES.json b/app/translations/es_ES.json index c098dff1..e0f57869 100644 --- a/app/translations/es_ES.json +++ b/app/translations/es_ES.json @@ -228,7 +228,7 @@ "login": "Iniciar Sesión", "password": "Contraseña", "username": "Usuario", - "viewStatus": "View Public Status Page" + "viewStatus": "Ver página de estado público" }, "notify": { "activityLog": "Registros de actividad", @@ -301,10 +301,12 @@ "serversDesc": "Servidores a los que este grupo puede acceder" }, "serverBackups": { + "actions": "Acciones", "after": "Comando ejecutado después del respaldo", "backupAtMidnight": "¿Copia de seguridad automática a medianoche?", "backupNow": "¡Respalde ahora!", "backupTask": "Se ha iniciado una tarea de copia de seguridad.", + "backups": "Copias de seguridad del servidor", "before": "Comando ejecutado antes del respaldo", "cancel": "Cancelar", "clickExclude": "Click para seleccionar las Exclusiones", @@ -313,21 +315,34 @@ "confirmDelete": "¿Quieres eliminar esta copia de seguridad? Esto no se puede deshacer.", "confirmRestore": "¿Seguro que quiere restaurar desde este respaldo?. Todos los archivos del servidor actuales serán cambiados al estado del respaldo y serán irrecuperables.", "currentBackups": "Copias de seguridad actuales", + "default": "Copia de seguridad predeterminada", + "defaultExplain": "La copia de seguridad que Crafty usará antes de actualizar. No se puede cambiar ni eliminar.", "delete": "Eliminar", "destroyBackup": "¿Destruir copia de seguridad \" + file_to_del + \"?", "download": "Descargar", + "edit": "Editar", + "enabled": "Habilitado", "excludedBackups": "Rutas Excluidas: ", "excludedChoose": "Elige las rutas que desea excluir de los respaldos", "exclusionsTitle": "Exclusiones en respaldos.", + "failed": "Fallido", "maxBackups": "Cantidad máxima de respaldos", "maxBackupsDesc": "Crafty no almacenará más de N copias de seguridad, eliminando la más antigua. (Sin límite: 0)", + "myBackup": "Mi Nueva Copia", + "name": "Nombre", + "newBackup": "Crear Nueva Copia de Seguridad", + "no-backup": "No hay copias de seguridad. Para crear una nueva configuración de copias de seguridad, presiona Crear nueva copia", "options": "Opciones", "path": "Ruta", "restore": "Restaurar", "restoring": "Restaurando copia de seguridad. Esto puede tomar un tiempo. Sea paciente.", + "run": "Ejecutar Copia de seguridad", "save": "Guardar", "shutdown": "Apagar el servidor durante la duración de la copia del respaldo.", "size": "Tamaño", + "standby": "En espera", + "status": "Estado", + "storage": "Ubicación del almacenamiento", "storageLocation": "Ubicación de almacenamiento", "storageLocationDesc": "¿Dónde quieres almacenar las copias de seguridad?" }, @@ -492,6 +507,7 @@ }, "serverSchedules": { "action": "Acción", + "actionId": "Seleccionar acción secundaria", "areYouSure": "¿Borrar tarea programada?", "cancel": "Cancelar", "cannotSee": "¿No puede ver todo?", diff --git a/app/translations/fr_FR.json b/app/translations/fr_FR.json index 07074ea3..db838d2d 100644 --- a/app/translations/fr_FR.json +++ b/app/translations/fr_FR.json @@ -301,10 +301,12 @@ "serversDesc": "Les serveurs auquels ce rôle a accès" }, "serverBackups": { + "actions": "Actions", "after": "Exécuter une commande après la sauvegarde", "backupAtMidnight": "Sauvegarde Automatique à minuit ?", "backupNow": "Sauvegarder Maintenant !", "backupTask": "Une sauvegarde vient de démarrer.", + "backups": "Sauvegarde de Serveur", "before": "Exécuter une commande avant la sauvegarde", "cancel": "Annuler", "clickExclude": "Cliquer pour sélectionner les Exclusions", @@ -313,21 +315,34 @@ "confirmDelete": "Es-tu sûr de vouloir supprimer cette sauvegarde ? Tu ne pourras pas revenir en arrière.", "confirmRestore": "Êtes-vous sûr de vouloir restaurer à partir de cette sauvegarde. Tous les fichiers du serveur actuel passeront à l'état de sauvegarde et seront irrécupérables.", "currentBackups": "Sauvegardes Actuelles", + "default": "Sauvegarde par Défaut", + "defaultExplain": "La sauvegarde que Crafty utilisera avant la mise à jour. Cela ne peut être changé ou modifié.", "delete": "Supprimer", "destroyBackup": "Supprimer la sauvegarde \" + file_to_del + \" ?", "download": "Télécharger", + "edit": "Modifier", + "enabled": "Activé", "excludedBackups": "Dossiers Exclus : ", "excludedChoose": "Choisir les dossiers à exclure de la sauvegarde", "exclusionsTitle": "Exclusions de Sauvegarde", + "failed": "Echec", "maxBackups": "Sauvergardes Max", "maxBackupsDesc": "Crafty ne fera pas plus de N sauvegardes, supprimant les plus anciennes (entrer 0 pour toutes les garder)", + "myBackup": "Ma Nouvelle Sauvegarde", + "name": "Nom", + "newBackup": "Créer une Nouvelle Sauvegarde", + "no-backup": "Aucune Sauvegarde. Pour aouter une nouvelle configuration de sauvegarde, il faut clicker sur ", "options": "Options", "path": "Chemin", "restore": "Restaurer", "restoring": "Restauration de la sauvegarde. Cela peut prendre un peu de temps. S'il vous plaît soyez patient.", + "run": "Lancer la Sauvegarde", "save": "Sauvegarder", "shutdown": "Extinction du serveur pendant la durée de la sauvegarde", "size": "Taille", + "standby": "Attente", + "status": "Statut", + "storage": "Emplacement de la Sauvegarde", "storageLocation": "Emplacement de Sauvegarde", "storageLocationDesc": "Où veux-tu enregister tes sauvegardes ?" }, @@ -492,6 +507,7 @@ }, "serverSchedules": { "action": "Action", + "actionId": "Sélectionner une configuration de sauvegarde", "areYouSure": "Supprimer la Tâche Planifiée ?", "cancel": "Annuler", "cannotSee": "Tu ne peux pas tout voir ?", diff --git a/app/translations/he_IL_incomplete.json b/app/translations/he_IL_incomplete.json index f3c45e40..ec70de3c 100644 --- a/app/translations/he_IL_incomplete.json +++ b/app/translations/he_IL_incomplete.json @@ -301,10 +301,12 @@ "serversDesc": "לשרתים מותר לגשת לתפקיד זה" }, "serverBackups": { + "actions": "פעולות", "after": "הרץ פקודה לאחר הגיבוי", "backupAtMidnight": "גיבוי אוטומטי בחצות?", "backupNow": "!גיבוי עכשיו", "backupTask": "החלה משימת גיבוי.", + "backups": "גיבויי שרת", "before": "הרץ פקודה לפני הגיבוי", "cancel": "לבטל", "clickExclude": "לחצו כדי לבחור מה לא יהיה בגיבוי", @@ -313,21 +315,34 @@ "confirmDelete": "האם ברצונכם למחוק את הגיבוי הזה? אי אפשר לבטל את זה.", "confirmRestore": "האם אתם בטוחים שברצונכם לשחזר מגיבוי זה. כל קבצי השרת הנוכחיים ישתנו למצב גיבוי ולא יהיה אפשר לשחזר.", "currentBackups": "גיבויים נוכחיים", + "default": "גיבוי ברירת מחדל", + "defaultExplain": "הגיבוי ש-Crafty ישתמש בו לפני עדכונים. לא ניתן לשנות או למחוק.", "delete": "למחוק", "destroyBackup": "?\" + file_to_del + \" להרוס גיבוי", "download": "הורדה", + "edit": "ערוך", + "enabled": "מופעל", "excludedBackups": "נתיבים שלא נכללו: ", "excludedChoose": "בחרו את הנתיבים שברצונכם לא לכלול בגיבויים", "exclusionsTitle": "אי הכללות גיבוי", + "failed": "נכשל", "maxBackups": "מקסימום גיבויים", "maxBackupsDesc": "גיבויים, ימחק את הישן ביותר (הזן 0 כדי לשמור את כולם) N-קראפטי לא יאחסן יותר מ", + "myBackup": "הגיבוי החדש שלי", + "name": "שם", + "newBackup": "צור גיבוי חדש", + "no-backup": "אין גיבויים. כדי ליצור תצורת גיבוי חדשה אנא לחץ על גיבוי חדש", "options": "אפשרויות", "path": "נתיב", "restore": "לשחזר", "restoring": "שחזור גיבוי. זה עשוי לקחת זמן. אנא חכו בסבלנות.", + "run": "הפעל גיבוי", "save": "שמירה", "shutdown": "כיבוי שרת למשך הגיבוי", "size": "גודל", + "standby": "בהמתנה", + "status": "סטטוס", + "storage": "מיקום אחסון", "storageLocation": "מקום איחסון", "storageLocationDesc": "איפו אתם רוצים לאחסן גיבויים?" }, @@ -492,6 +507,7 @@ }, "serverSchedules": { "action": "פעולה", + "actionId": "בחר פעולה משנית", "areYouSure": "למחוק משימה מתוזמנת?", "cancel": "לבטל", "cannotSee": "לא רואים הכל?", diff --git a/app/translations/it_IT.json b/app/translations/it_IT.json index b59971b5..e2540de0 100644 --- a/app/translations/it_IT.json +++ b/app/translations/it_IT.json @@ -301,10 +301,12 @@ "serversDesc": "Server a cui questo ruolo è consentito l'accesso" }, "serverBackups": { + "actions": "Azioni", "after": "Esegui il comando prima del backup", "backupAtMidnight": "Auto-backup a mezzanotte?", "backupNow": "Effettua il Backup Ora!", "backupTask": "Un'azione di backup è cominciata.", + "backups": "Backup del server", "before": "Esegui il comando dopo il backup", "cancel": "Cancella", "clickExclude": "Clicca per selezionare le esclusioni", @@ -313,21 +315,34 @@ "confirmDelete": "Vuoi eliminare questo backup? Non puoi tornare indietro.", "confirmRestore": "Sei sicuro di voler ripristinare qeusto backup? Tutti i file correnti verranno sovrascritti allo stato di backup e saranno irrecuperabili.", "currentBackups": "Backup attuali", + "default": "Backup predefinito", + "defaultExplain": "Il backup che Crafty utilizzerà prima degli aggiornamenti. Non può essere cambiato o eliminato.", "delete": "Elimina", "destroyBackup": "Distruggere il backup \" + file_to_del + \"?", "download": "Scarica", + "edit": "Modifica", + "enabled": "Abilitato", "excludedBackups": "Percorsi esclusi: ", "excludedChoose": "Scegli i percorsi che desideri escludere dai tuoi backups", "exclusionsTitle": "Fai un backup delle esclusioni", + "failed": "Fallito", "maxBackups": "Backup massimi", "maxBackupsDesc": "Crafty non memorizzerà più di N backup, cancellando quelli più vecchi (inserisci 0 per mantenerli tutti)", + "myBackup": "Il mio nuovo backup", + "name": "Nome", + "newBackup": "Crea nuovo backup", + "no-backup": "Nessun backup. Per configurare un nuovo backup clicca Nuovo backup", "options": "Opzioni", "path": "Percorso", "restore": "Ripristina", "restoring": "Ripristinando il backup. Potrebber volerci un momento. Per favore sii paziente.", + "run": "Esegui backup", "save": "Salva", "shutdown": "Arresto del server per la durata del backup", "size": "Dimensioni", + "standby": "Sospeso", + "status": "Stato", + "storage": "Percorso archiviazione", "storageLocation": "Percorso di memorizzazione", "storageLocationDesc": "Dove vuoi memorizzare i backup?" }, @@ -492,6 +507,7 @@ }, "serverSchedules": { "action": "Azione", + "actionId": "Seleziona azione da eseguire", "areYouSure": "Eliminare l'azione programmata?", "cancel": "Cancella", "cannotSee": "Non vedi tutto?", diff --git a/app/translations/lol_EN.json b/app/translations/lol_EN.json index d6075623..862c1c68 100644 --- a/app/translations/lol_EN.json +++ b/app/translations/lol_EN.json @@ -301,10 +301,12 @@ "serversDesc": "SERVRS DIS ROLE IZ ALLOWD 2 ACCES" }, "serverBackups": { + "actions": "DO-STUFFZ", "after": "RUNZ COMMANDZ AFTUR BAKUP", "backupAtMidnight": "AUTO-BAKUP AT MIDDLENIGHTZ?", "backupNow": "BAKUP NOWZ!", "backupTask": "OKAI I GETZ FISH, BAK SOONZ", + "backups": "SERVER BACKUPS", "before": "RUNZ COMMANDZ BEFOUR BAKUP", "cancel": "STAHP", "clickExclude": "CLICK 2 MARK EXCLUSHUNS", @@ -313,21 +315,34 @@ "confirmDelete": "R U SURE U WANTZ ME TO EATZ DIS BAKUP? WIAL BEH LOZT FOREVR (LONGIR THAN KITTEHZ NAPZ)", "confirmRestore": "R U SURE U WANTZ 2 RESTORE FRUM DIS BAKUP. ALL CURRENT SERVR FISHZ WILL BE EATZ AN WILL BE UNRECOVERABLE.", "currentBackups": "CURRENT STASH OV BAKUPS", + "default": "USUAL BACKUP", + "defaultExplain": "DA BACKUP THAT CRAFTY USE BEFORE UPDATES. DIS NO CAN CHANGE OR GO AWAY.", "delete": "MAK GONE", "destroyBackup": "EAT BAKUP \" + file_to_del + \"?", "download": "DOWNLOADZ", + "edit": "MAKE BETTERS", + "enabled": "TURNED ON", "excludedBackups": "EXCLUSHUNS: ", "excludedChoose": "CHOOSE TEH PATHS U WANTS 2 EXCLUDE FRUM UR BAKUPS", "exclusionsTitle": "BAKUP EXCLUSHUNS", + "failed": "NOPE'D", "maxBackups": "MAX BAKUPS", "maxBackupsDesc": "CWAFTY WILL NOT KEEPZ MOAR THAN N BCKUPS, DELETIN TEH MOST OLDZ FURST (ENTR 0 TO BE BIG GREEDY)", + "myBackup": "MAH NEW BACKUP", + "name": "NAMZ", + "newBackup": "MAKEZ NEW BACKUP", + "no-backup": "NO BACKUPS. TO MAKE A NEW BACKUP THINGY PLEASE BOOP. NEW BACKUP", "options": "OPSHUNS", "path": "PETH", "restore": "RESTOR", "restoring": "RESTORIN BAKUP. DIS CUD TAEK WHILE. PLZ BE PATIENT.", + "run": "DO BACKUP NOWZ", "save": "DUN", "shutdown": "SLEEPY SERVR WEN MAK BAKAUPZ?", "size": "HOW BIGZ", + "standby": "WAITIN'", + "status": "WHAT'S UP", + "storage": "HIDING SPOT", "storageLocation": "SHINY STASH OV HINGZ", "storageLocationDesc": "WER DO U WANTS 2 STASH BAKUPS?" }, @@ -492,6 +507,7 @@ }, "serverSchedules": { "action": "ACTSHUN", + "actionId": "PICK ACTION KITTY", "areYouSure": "FORGET 2 DO DIS ????", "cancel": "STAHP", "cannotSee": "CANNY SEE?", diff --git a/app/translations/lv_LV.json b/app/translations/lv_LV.json index 0ac11432..0d454b8e 100644 --- a/app/translations/lv_LV.json +++ b/app/translations/lv_LV.json @@ -302,10 +302,12 @@ "serversDesc": "serveri, kuriem šai lomai ir atļauta piekļuve" }, "serverBackups": { + "actions": "Darbības", "after": "Palaist komandu pēc dublējuma", "backupAtMidnight": "Automātiski dublēt pusnaktī?", "backupNow": "Dublēt Tagad!", "backupTask": "Dublējuma uzdevums ticis startēts.", + "backups": "Servera Dublējumi", "before": "Palaist komandu pirms dublējuma", "cancel": "Atcelt", "clickExclude": "Nospied lai izvēlētos Izņēmumus", @@ -314,21 +316,34 @@ "confirmDelete": "Vai vēlaties izdzēst šo dublējumu? Šo nevar atdarīt.", "confirmRestore": "Vai tiešām vēlieties atjaunot no šī dublējuma. Visas esošās datnes tiks atgrieztas uz dublējuma stāvokli un būs neatgriežamas.", "currentBackups": "Pašreizējie Dublējumi", + "default": "Noklusētais Dublējums", + "defaultExplain": "Dublējums ko Crafty izmanto pirms atjaunināšanas. To nevar mainīt vai izdzēst.", "delete": "Dzēst", "destroyBackup": "Iznīcināt dublējumu \" + file_to_del + \"?", "download": "Lejupielādēt", + "edit": "Rediģēt", + "enabled": "Iespējots", "excludedBackups": "Izņēmuma Ceļi: ", "excludedChoose": "Izvēlies ceļus, kurus tu vēlies izņemt no saviem dublējumiem", "exclusionsTitle": "Dublējuma Izņēmumi", + "failed": "Neizdevās", "maxBackups": "Maks. Dublējumi", "maxBackupsDesc": "Crafty nesaglabās vairāk nekā N dublējumus, dzēšot vecākaos (ievadi 0 lai saglabātu visus)", + "myBackup": "Mans Jaunais Dublējums", + "name": "Nosaukums", + "newBackup": "Izveidot Jaunu Dublējumu", + "no-backup": "Nav Dublējumu. Lai izveidotu dublējuma konfigurāciju, nospied Izveidot Jaunu Dublējumu", "options": "Opcijas", "path": "Ceļš", "restore": "Atjaunot", "restoring": "Atjauno dublējumu. Tas var aizņemt kādi laiku. Esiet pacietīgs.", + "run": "Veikt Dublējumu", "save": "Saglabāt", "shutdown": "Apturēt serveri dublējumkopijas laikā", "size": "Lielums", + "standby": "Gaidstāve", + "status": "Statuss", + "storage": "Glabātavas Vieta", "storageLocation": "Krātuves Vieta", "storageLocationDesc": "Kur jūs vēlaties saglabāt dublējumus?" }, @@ -493,6 +508,7 @@ }, "serverSchedules": { "action": "Darbība", + "actionId": "Izvēlēties apakšdarbību", "areYouSure": "Dzēst Ieplānoto Uzdevumu?", "cancel": "Atcelt", "cannotSee": "Neredziet visu?", diff --git a/app/translations/nl_BE.json b/app/translations/nl_BE.json index 4d3644b8..ff531cfa 100644 --- a/app/translations/nl_BE.json +++ b/app/translations/nl_BE.json @@ -301,10 +301,12 @@ "serversDesc": "servers waar deze rol toegang toe heeft" }, "serverBackups": { + "actions": "Acties", "after": "Voer opdracht uit na back-up", "backupAtMidnight": "Automatische back-up maken om middernacht?", "backupNow": "Nu een back-up maken!", "backupTask": "Er is een back-uptaak ​​gestart.", + "backups": "Serverbackups", "before": "Voer opdracht uit vóór back-up", "cancel": "Annuleren", "clickExclude": "Klik om Uitsluitingen te selecteren", @@ -313,21 +315,34 @@ "confirmDelete": "Wil je deze back-up verwijderen? Dit kan niet ongedaan gemaakt worden.", "confirmRestore": "Bent u zeker dat u wilt herstellen vanaf deze backup. Alle huidige server bestanden zullen worden veranderd naar de backup status en zullen niet meer hersteld kunnen worden.", "currentBackups": "Huidige back-ups", + "default": "Standaardbackup", + "defaultExplain": "De backup die Crafty gebruikt vóór updates. Deze kan niet worden gewijzigd of verwijderd.", "delete": "Verwijderen", "destroyBackup": "Back-up vernietigen \" + file_to_del + \"?", "download": "Downloaden", + "edit": "Bewerken", + "enabled": "Ingeschakeld", "excludedBackups": "Uitgesloten paden: ", "excludedChoose": "Kies de paden die u wilt uitsluiten van uw back-ups", "exclusionsTitle": "Uitsluitingen voor back-ups", + "failed": "Mislukt", "maxBackups": "Max Back-ups", "maxBackupsDesc": "Crafty zal niet meer dan N back-ups opslaan, waarbij de oudste wordt verwijderd (voer 0 in om ze allemaal te bewaren)", + "myBackup": "Nieuwe backup", + "name": "Naam", + "newBackup": "Nieuwe backup maken", + "no-backup": "Geen backups. Druk op 'Nieuwe backup' om een nieuwe backupconfiguratie te maken.", "options": "Opties", "path": "Pad", "restore": "Herstellen", "restoring": "Back-up herstellen. Dit kan een tijdje duren. Even geduld alstublieft.", + "run": "Backup uitvoeren", "save": "Opslaan", "shutdown": "Sluit de server af voor de duur van de backup", "size": "Grootte", + "standby": "Standby", + "status": "Status", + "storage": "Opslaglocatie", "storageLocation": "Opslaglocatie", "storageLocationDesc": "Waar wil je back-ups opslaan?" }, @@ -492,6 +507,7 @@ }, "serverSchedules": { "action": "Actie", + "actionId": "Selecteer onderliggende actie", "areYouSure": "Verwijder Geplande Taak?", "cancel": "Annuleren", "cannotSee": "Ziet u niet alles?", diff --git a/app/translations/pl_PL.json b/app/translations/pl_PL.json index 7115b869..0e44c87f 100644 --- a/app/translations/pl_PL.json +++ b/app/translations/pl_PL.json @@ -301,10 +301,12 @@ "serversDesc": "Serwery które mają tą role mają dostęp" }, "serverBackups": { + "actions": "Akcje", "after": "Wykonaj tę komendę po backupie", "backupAtMidnight": "Auto-backup o północy?", "backupNow": "Backup Teraz!", "backupTask": "Backup został rozpoczęty.", + "backups": "Kopie zapasowe serwera", "before": "Wykonaj tę komendę przed backupem", "cancel": "Anuluj", "clickExclude": "Kliknij aby zaznaczyć wyjątki", @@ -313,21 +315,34 @@ "confirmDelete": "Czy chcesz usunąć ten backup? Nie można tego cofnąć.", "confirmRestore": "Czy jesteś pewien że chcesz przywrócić z tego backupu. Wszystkie pliki powrócą do stanu z backupu.", "currentBackups": "Backupy Teraz", + "default": "Podstawowa kopia zapasowa", + "defaultExplain": "Kopia zapasowa przed jakimikolwiek zmianami. Nie można jej usunąć ani edytować.", "delete": "Usuń", "destroyBackup": "Zniszcz Backup \" + file_to_del + \"?", "download": "Pobierz", + "edit": "Edytuj", + "enabled": "Włączony", "excludedBackups": "Wykluczone ścieżki: ", "excludedChoose": "Wybierz ścieżki do wykluczenia z backupu", "exclusionsTitle": "Wykluczenia backupu", + "failed": "Nieudany!", "maxBackups": "Maks. Backupów", "maxBackupsDesc": "Crafty nie będzie zbierał więcej niż X backupów, zacznie usuwać od nadstarszych (wpisz 0, aby zatrzymać nieskończoną ilość)", + "myBackup": "Nowa kopia zapasowa", + "name": "Nazwa", + "newBackup": "Nowa kopia zapasowa", + "no-backup": "Brak kopii zapasowych. Aby skonfigurować kopię zapasową kliknij na", "options": "Opcje", "path": "Nazwa pliku", "restore": "Przywróć", "restoring": "Przywracanie backupu. To trochę zajmie. Bądź cierpliwy.", + "run": "Wykonaj kopię zapasową", "save": "Zapisz", "shutdown": "Wyłącz serwer na czas backupu", "size": "Rozmiar", + "standby": "Gotowy", + "status": "Status", + "storage": "Lokalizacja kopii zapasowych", "storageLocation": "Ścieżka zapisywania", "storageLocationDesc": "Gdzie chcesz trzymać backupy?" }, @@ -492,6 +507,7 @@ }, "serverSchedules": { "action": "Akcja", + "actionId": "Zaznacz zadanie podwładne", "areYouSure": "Usuń zaplanowane (zadanie)?", "cancel": "Anuluj", "cannotSee": "Nie widzisz wszystkiego?", diff --git a/app/translations/th_TH.json b/app/translations/th_TH.json index 88b97435..47fa3c01 100644 --- a/app/translations/th_TH.json +++ b/app/translations/th_TH.json @@ -301,10 +301,12 @@ "serversDesc": "เซิร์ฟเวอร์ที่บทบาทนี้ได้รับอนุญาตให้เข้าถึง" }, "serverBackups": { + "actions": "คำสั่งด่วน", "after": "ส่งคำสั่งหลังการสำรองข้อมูล", "backupAtMidnight": "คุณต้องการสำรองข้อมูลอัตโนมัติตอนเที่ยงคืนหรือไม่?", "backupNow": "สำรองข้อมูลตอนนี้!", "backupTask": "เริ่มการสำรองข้อมูลแล้ว", + "backups": "ข้อมูลสำรองเซิร์ฟเวอร์", "before": "ส่งคำสั่งก่อนการสำรองข้อมูล", "cancel": "ยกเลิก", "clickExclude": "คลิกเพื่อเลือกการยกเว้น", @@ -313,21 +315,34 @@ "confirmDelete": "คุณต้องการลบข้อมูลสำรองนี้หรือไม่ สิ่งนี้ไม่สามารถยกเลิกได้", "confirmRestore": "คุณแน่ใจหรือไม่ว่าต้องการกู้คืนจากข้อมูลสำรองนี้ ไฟล์เซิร์ฟเวอร์ปัจจุบันทั้งหมดจะเปลี่ยนเป็นแบบสำรองและจะไม่สามารถกู้คืนได้", "currentBackups": "ไฟล์สำรองข้อมูลปัจจุบัน", + "default": "ข้อมูลสำรองเริ่มต้น", + "defaultExplain": "ข้อมูลสำรองที่ Crafty จะใช้ก่อนการอัพเดต สิ่งนี้ไม่สามารถเปลี่ยนแปลงหรือลบได้", "delete": "ลบ", "destroyBackup": "คุณต้องการทำลายข้อมูลสำรอง \" + file_to_del + \"หรือไม่", "download": "ดาวน์โหลด", + "edit": "แก้ไข", + "enabled": "เปิดใช้งาน", "excludedBackups": "เส้นทางที่ยกเว้น: ", "excludedChoose": "เลือกเส้นทางที่คุณต้องการยกเว้นจากการสำรองข้อมูลของคุณ", "exclusionsTitle": "ข้อยกเว้นการสำรองข้อมูล", + "failed": "ล้มเหลว", "maxBackups": "ต้องการเก็บข้อมูลสำรองกี่ครั้ง?", "maxBackupsDesc": "Crafty จะไม่เก็บข้อมูลสำรองมากกว่า N รายการ โดยจะลบข้อมูลสำรองที่เก่าที่สุด (ป้อน 0 เพื่อเก็บทั้งหมด)", + "myBackup": "ข้อมูลสำรองใหม่ของฉัน", + "name": "ชื่อ", + "newBackup": "สร้างข้อมูลสำรองใหม่", + "no-backup": "ไม่มีการสำรองข้อมูล หากต้องการตั้งค่าการสำรองข้อมูลใหม่ กรุณากด สร้างข้อมูลสำรองใหม่", "options": "ตัวเลือก", "path": "เส้นทาง", "restore": "คืนค่า", "restoring": "กำลังกู้คืนข้อมูลสำรอง การดำเนินการนี้อาจใช้เวลาสักครู่ กรุณาอดทนรออย่างใจเย็น", + "run": "เริ่มทำงานไฟล์สำรอง", "save": "บันทึก", "shutdown": "ปิดเซิร์ฟเวอร์ตามระยะเวลาของการสำรองข้อมูล", "size": "ขนาด", + "standby": "พร้อมใช้งาน", + "status": "สถานะ", + "storage": "พื้นที่จัดเก็บข้อมูล", "storageLocation": "สถานที่จัดเก็บ", "storageLocationDesc": "คุณต้องการสำรองข้อมูลไว้ที่ไหน?" }, @@ -492,6 +507,7 @@ }, "serverSchedules": { "action": "การกระทำ", + "actionId": "เลือกลูกของการกระทำ", "areYouSure": "ลบงานที่กำหนดเวลาไว้?", "cancel": "ยกเลิก", "cannotSee": "ไม่เห็นอะไรเลยใช่ใหม?", diff --git a/app/translations/tr_TR.json b/app/translations/tr_TR.json index ccdf7414..9ad1ce93 100644 --- a/app/translations/tr_TR.json +++ b/app/translations/tr_TR.json @@ -301,10 +301,12 @@ "serversDesc": "bu rolün erişmesine izin verilen sunucular" }, "serverBackups": { + "actions": "Eylemler", "after": "Yedeklemeden sonra bir komut çalıştır", "backupAtMidnight": "Gece yarısında otomatik yedekleme yapılsın mı?", "backupNow": "Backup Now!", "backupTask": "Bir yedekleme görevi başlatıldı.", + "backups": "Sunucu Yedekleri", "before": "Yedeklemeden önce bir komut çalıştır", "cancel": "İptal", "clickExclude": "İstisnaları seçmek için tıklayın", @@ -313,21 +315,34 @@ "confirmDelete": "Bu yedeği silmek istediğine emin misin? Bu geri alınamaz.", "confirmRestore": "Bu yedeği geri yüklemek istediğinizden emin misiniz? Tüm mevcut sunucu dosyaları yedeklemedeki durumuna dönecek ve kurtarılamayacaktır.", "currentBackups": "Mevcut Yedekmeler", + "default": "Varsayılan Yedek", + "defaultExplain": "Crafty'nin güncellemelerden önce kullanacağı yedek. Bu değiştirilemez ya da silinemez.", "delete": "Sil", "destroyBackup": "\" + file_to_del + \" yedeklemesi yok edilsin mi?", "download": "İndir", + "edit": "Düzenle", + "enabled": "Etkin", "excludedBackups": "Hariç Tutulan Yollar: ", "excludedChoose": "Yedeklemelerinizden hariç tutmak istediğiniz yolları seçin", "exclusionsTitle": "Yedekleme İstisnaları", + "failed": "Başarısız", "maxBackups": "Maksimum Yedekleme Sayısı", "maxBackupsDesc": "Crafty N yedeklemeden fazlasını saklamayacak, en eskisini silecektir (tümünü saklamak için 0 girin)", + "myBackup": "Benim Yeni Yedeğim", + "name": "Ad", + "newBackup": "Yeni Yedek Oluştur", + "no-backup": "Mevcut yedek bulunmuyor. Yeni bir yedek oluşturmak için lütfen Yeni Yedek Oluştur tuşuna basınız.", "options": "Seçenekler", "path": "Dosya Yolu", "restore": "Geri Yükleme", "restoring": "Yedekleme geri yükleniyor. Bu biraz zaman alabilir. Lütfen sabırlı olun.", + "run": "Yedeği Çalıştır", "save": "Kaydet", "shutdown": "Yedekleme süresince sunucuyu kapat", "size": "Boyut", + "standby": "Beklemede", + "status": "Durum", + "storage": "Depolama Konumu", "storageLocation": "Depolama Konumu", "storageLocationDesc": "Yedekmeleri nerede saklamak istiyorsunuz?" }, @@ -492,6 +507,7 @@ }, "serverSchedules": { "action": "Eylem", + "actionId": "Alt Eylem Seçiniz", "areYouSure": "Zamanlanmış Görev Silinsin mi?", "cancel": "İptal", "cannotSee": "Her şeyi göremiyor musun?", diff --git a/app/translations/uk_UA.json b/app/translations/uk_UA.json index a12e2e40..924d1050 100644 --- a/app/translations/uk_UA.json +++ b/app/translations/uk_UA.json @@ -301,10 +301,12 @@ "serversDesc": "сервери які доступні для цієї ролі" }, "serverBackups": { + "actions": "Дії", "after": "Виконати команду після завершення бекапу", "backupAtMidnight": "Авто-бекап опівночі?", "backupNow": "Запустити бекап!", "backupTask": "Бекап запущено.", + "backups": "Сервер Бекапів", "before": "Виконати команду перед початком бекапу", "cancel": "Відмінити", "clickExclude": "Додати винятки", @@ -313,21 +315,34 @@ "confirmDelete": "Ви дійсно бажаєте видати бекап? Ця дія незворотня.", "confirmRestore": "Ви впевненні що бажаєте відновити даний бекап? При відновленні сервер буде вимкнуто та відновлено за допомогою даного бекапу, минулі файли будуть втрачені!", "currentBackups": "Поточні бекапи", + "default": "Звичайний Бекап", + "defaultExplain": "Бекап цього Crafty буде створений перед оновленням. Це не можна змінити чи видалити.", "delete": "Видалити", "destroyBackup": "Видалити бекап \" + file_to_del + \"?", "download": "Завантажити", + "edit": "Редагувати", + "enabled": "Увімкненно", "excludedBackups": "Винятки: ", "excludedChoose": "Виберіть папки які бажаєте додати у винятки", "exclusionsTitle": "Бекап винятки", + "failed": "Помилка", "maxBackups": "Максимум бекапів", "maxBackupsDesc": "Crafty не зможе зберігати більше ніж N бекапів, видалятиме старі (введіть 0 для зберігання усіх бекапів)", + "myBackup": "Мій новий бекап", + "name": "Назва", + "newBackup": "Створити новий бекап", + "no-backup": "Немає бекапів. Щоб створити бекап, натисніть кнопку Мій новий Бекап", "options": "Налаштування", "path": "Шлях", "restore": "Відновити", "restoring": "Відновлення бекапу. Це може зайняти деякий час. Будь ласка будьте терплячі.", + "run": "Запустити бекап", "save": "Зберегти", "shutdown": "Вимикати сервер на час бекапу", "size": "Розмір", + "standby": "Очікування", + "status": "Статус", + "storage": "Місце збереження", "storageLocation": "Місце зберігання", "storageLocationDesc": "Де ви бажаєте зберігати бекапи?" }, @@ -492,6 +507,7 @@ }, "serverSchedules": { "action": "Дія", + "actionId": "Вибрати дочірню дію", "areYouSure": "Видалити заплановане завдання?", "cancel": "Відмінити", "cannotSee": "Нічого не бачите?", diff --git a/app/translations/zh_CN.json b/app/translations/zh_CN.json index e30ecef6..359e4e13 100644 --- a/app/translations/zh_CN.json +++ b/app/translations/zh_CN.json @@ -301,10 +301,12 @@ "serversDesc": "此角色允许访问的服务器" }, "serverBackups": { + "actions": "操作", "after": "备份后运行指令", "backupAtMidnight": "午夜自动备份?", "backupNow": "现在备份!", "backupTask": "一个备份任务已开始。", + "backups": "服务器备份", "before": "备份前运行指令", "cancel": "取消", "clickExclude": "点击来选择排除项", @@ -313,21 +315,34 @@ "confirmDelete": "您想要删除这个备份吗?此操作不能撤销。", "confirmRestore": "你确定要从此备份恢复吗?所有现存的服务器文件将更改到备份时的状态,并且无法撤销。", "currentBackups": "现有备份", + "default": "默认备份", + "defaultExplain": "Crafty 在更新前会使用的备份。此项目不能被更改或删除。", "delete": "删除", "destroyBackup": "删除备份 \" + file_to_del + \"?", "download": "下载", + "edit": "编辑", + "enabled": "已启用", "excludedBackups": "排除的路径:", "excludedChoose": "选择您希望从您的备份中排除的路径", "exclusionsTitle": "备份排除项", + "failed": "失败", "maxBackups": "最大备份数量", "maxBackupsDesc": "Crafty 不会存储多于 N 个备份,并且会删除最旧的备份(输入 0 以保留所有备份)", + "myBackup": "我的新备份", + "name": "名称", + "newBackup": "创建新备份", + "no-backup": "暂无备份。请点击“新备份”以创建一个新的备份配置。", "options": "选项", "path": "路径", "restore": "恢复", "restoring": "正在恢复备份。这需要一点时间。请耐心等待。", + "run": "运行备份", "save": "保存", "shutdown": "在备份期间停止服务器", "size": "大小", + "standby": "等候", + "status": "状态", + "storage": "存储位置", "storageLocation": "存储位置", "storageLocationDesc": "您想要在哪里存储备份?" }, @@ -492,6 +507,7 @@ }, "serverSchedules": { "action": "操作", + "actionId": "选择子操作", "areYouSure": "删除计划任务?", "cancel": "取消", "cannotSee": "什么都看不到?",