Merge branch 'refactor/backups' into 'dev'

Refactor Backups | Allow multiple backup configurations

See merge request crafty-controller/crafty-4!711
This commit is contained in:
Iain Powrie 2024-07-09 02:11:30 +00:00
commit eaeda3e746
39 changed files with 2631 additions and 1057 deletions

View File

@ -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))

View File

@ -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)

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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:

View File

@ -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 = {}

View File

@ -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,
}
],
)

View File

@ -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)

View File

@ -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,
),

View File

@ -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')}",

View File

@ -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"})

View File

@ -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"})

View File

@ -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

View File

@ -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(

View File

@ -22,6 +22,9 @@ task_patch_schema = {
"action": {
"type": "string",
},
"action_id": {
"type": "string",
},
"interval": {"type": "integer"},
"interval_type": {
"type": "string",

View File

@ -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;
}
/**************************************************************/
/**************************************************************/
.hidden-input {
margin-left: -40px;
}

View File

@ -39,208 +39,152 @@
<span class="d-block d-sm-none">
{% include "parts/m_server_controls_list.html %}
</span>
<div class="row">
<div class="col-md-6 col-sm-12">
<br>
<br>
{% if data['backing_up'] %}
<div class="progress" style="height: 15px;">
<div class="progress-bar progress-bar-striped progress-bar-animated" id="backup_progress_bar"
role="progressbar" style="width:{{data['backup_stats']['percent']}}%;"
aria-valuenow="{{data['backup_stats']['percent']}}" aria-valuemin="0" aria-valuemax="100">{{
data['backup_stats']['percent'] }}%</div>
</div>
<p>Backing up <i class="fas fa-spin fa-spinner"></i> <span
id="total_files">{{data['server_stats']['world_size']}}</span></p>
{% end %}
<br>
{% if not data['backing_up'] %}
<div id="backup_button" class="form-group">
<button class="btn btn-primary" id="backup_now_button">{{ translate('serverBackups', 'backupNow',
data['lang']) }}</button>
</div>
{% end %}
<form id="backup-form" class="forms-sample">
<div class="form-group">
{% if data['super_user'] %}
<label for="server_name">{{ translate('serverBackups', 'storageLocation', data['lang']) }} <small
class="text-muted ml-1"> - {{ translate('serverBackups', 'storageLocationDesc', data['lang'])
}}</small> </label>
<input type="text" class="form-control" name="backup_path" id="backup_path"
value="{{ data['server_stats']['server_id']['backup_path'] }}"
placeholder="{{ translate('serverBackups', 'storageLocation', data['lang']) }}">
<div class="col-md-12 col-sm-12" style="overflow-x:auto;">
<div class="card">
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="fa-regular fa-bell"></i> {{ translate('serverBackups', 'backups',
data['lang']) }} </h4>
{% if data['user_data']['hints'] %}
<span class="too_small" title="{{ translate('serverSchedules', 'cannotSee', data['lang']) }}" ,
data-content="{{ translate('serverSchedules', 'cannotSeeOnMobile', data['lang']) }}" ,
data-placement="bottom"></span>
{% end %}
</div>
<div class="form-group">
<label for="server_path">{{ translate('serverBackups', 'maxBackups', data['lang']) }} <small
class="text-muted ml-1"> - {{ translate('serverBackups', 'maxBackupsDesc', data['lang'])
}}</small> </label>
<input type="text" class="form-control" name="max_backups" id="max_backups"
value="{{ data['backup_config']['max_backups'] }}"
placeholder="{{ translate('serverBackups', 'maxBackups', data['lang']) }}">
</div>
<div class="form-group">
<label for="compress" class="form-check-label ml-4 mb-4"></label>
{% if data['backup_config']['compress'] %}
<input type="checkbox" class="form-check-input" id="compress" name="compress" checked=""
value="True">{{ translate('serverBackups', 'compress', data['lang']) }}
{% else %}
<input type="checkbox" class="form-check-input" id="compress" name="compress" value="True">{{
translate('serverBackups', 'compress', data['lang']) }}
{% end %}
</div>
<div class="form-group">
<label for="shutdown" class="form-check-label ml-4 mb-4"></label>
{% if data['backup_config']['shutdown'] %}
<input type="checkbox" class="form-check-input" id="shutdown" name="shutdown" checked=""
value="True">{{ translate('serverBackups', 'shutdown', data['lang']) }}
{% else %}
<input type="checkbox" class="form-check-input" id="shutdown" name="shutdown" value="True">{{
translate('serverBackups', 'shutdown', data['lang']) }}
{% end %}
</div>
<div class="form-group">
<label for="command-check" class="form-check-label ml-4 mb-4"></label>
{% if data['backup_config']['before'] %}
<input type="checkbox" class="form-check-input" id="before-check" name="before-check" checked>{{
translate('serverBackups', 'before', data['lang']) }}
<br>
<input type="text" class="form-control" name="backup_before" id="backup_before"
value="{{ data['backup_config']['before'] }}" placeholder="We enter the / for you"
style="display: inline-block;">
{% else %}
<input type="checkbox" class="form-check-input" id="before-check" name="before-check">{{
translate('serverBackups', 'before', data['lang']) }}
<br>
<input type="text" class="form-control" name="backup_before" id="backup_before" value=""
placeholder="We enter the / for you." style="display: none;">
{% end %}
</div>
<div class="form-group">
<label for="command-check" class="form-check-label ml-4 mb-4"></label>
{% if data['backup_config']['after'] %}
<input type="checkbox" class="form-check-input" id="after-check" name="after-check" checked>{{
translate('serverBackups', 'after', data['lang']) }}
<br>
<input type="text" class="form-control" name="backup_after" id="backup_after"
value="{{ data['backup_config']['after'] }}" placeholder="We enter the / for you"
style="display: inline-block;">
{% else %}
<input type="checkbox" class="form-check-input" id="after-check" name="after-check">{{
translate('serverBackups', 'after', data['lang']) }}
<br>
<input type="text" class="form-control" name="backup_after" id="backup_after" value=""
placeholder="We enter the / for you." style="display: none;">
{% end %}
</div>
<div class="form-group">
<label for="server">{{ translate('serverBackups', 'exclusionsTitle', data['lang']) }} <small> - {{
translate('serverBackups', 'excludedChoose', data['lang']) }}</small></label>
<br>
<button class="btn btn-primary mr-2" id="root_files_button"
data-server_path="{{ data['server_stats']['server_id']['path']}}" type="button">{{
translate('serverBackups', 'clickExclude', data['lang']) }}</button>
</div>
<div class="modal fade" id="dir_select" tabindex="-1" role="dialog" aria-labelledby="dir_select"
aria-hidden="true">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLongTitle">{{ translate('serverBackups',
'excludedChoose', data['lang']) }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="tree-ctx-item" id="main-tree-div" data-path=""
style="overflow: scroll; max-height:75%;">
<input type="checkbox" id="main-tree-input" name="root_path" value="" disabled>
<span id="main-tree" class="files-tree-title tree-caret-down root-dir" data-path="">
<i class="far fa-folder"></i>
<i class="far fa-folder-open"></i>
{{ translate('serverFiles', 'files', data['lang']) }}
</span>
</input>
</div>
</div>
<div class="modal-footer">
<button type="button" id="modal-cancel" class="btn btn-secondary" data-dismiss="modal"><i class="fa-solid fa-xmark"></i></button>
<button type="button" id="modal-okay" data-dismiss="modal" class="btn btn-primary"><i class="fa-solid fa-thumbs-up"></i></button>
</div>
</div>
<div><a class="btn btn-info"
href="/panel/add_backup?id={{ data['server_stats']['server_id']['server_id'] }}"><i
class="fas fa-plus-circle"></i> {{ translate('serverBackups', 'newBackup', data['lang']) }}</a>
</div>
</div>
<button type="submit" class="btn btn-success mr-2">{{ translate('serverBackups', 'save', data['lang'])
}}</button>
<button type="reset" class="btn btn-light">{{ translate('serverBackups', 'cancel', data['lang'])
}}</button>
</form>
</div>
<div class="col-md-6 col-sm-12">
<div class="text-center">
<table class="table table-responsive dataTable" id="backup_table">
<h4 class="card-title">{{ translate('serverBackups', 'currentBackups', data['lang']) }}</h4>
<thead>
<tr>
<th width="10%">{{ translate('serverBackups', 'options', data['lang']) }}</th>
<th>{{ translate('serverBackups', 'path', data['lang']) }}</th>
<th width="20%">{{ translate('serverBackups', 'size', data['lang']) }}</th>
</tr>
</thead>
<tbody>
{% for backup in data['backup_list'] %}
<tr>
<td>
<a href="/panel/download_backup?file={{ backup['path'] }}&id={{ data['server_stats']['server_id']['server_id'] }}"
class="btn btn-primary">
<i class="fas fa-download" aria-hidden="true"></i>
{{ translate('serverBackups', 'download', data['lang']) }}
</a>
<br>
<br>
<button data-file="{{ backup['path'] }}" data-backup_path="{{ data['backup_path'] }}"
class="btn btn-danger del_button">
<i class="fas fa-trash" aria-hidden="true"></i>
{{ translate('serverBackups', 'delete', data['lang']) }}
</button>
<button data-file="{{ backup['path'] }}" class="btn btn-warning restore_button">
<i class="fas fa-undo-alt" aria-hidden="true"></i>
{{ translate('serverBackups', 'restore', data['lang']) }}
</button>
</td>
<td>{{ backup['path'] }}</td>
<td>{{ backup['size'] }}</td>
</tr>
{% end %}
</tbody>
</table>
<div class="card-body">
{% if len(data['backups']) == 0 %}
<div style="text-align: center; color: grey;">
<h7>{{ translate('serverBackups', 'no-backup', data['lang']) }} <strong>{{
translate('serverBackups', 'newBackup',data['lang']) }}</strong>.</h7>
</div>
{% end %}
{% if len(data['backups']) > 0 %}
<div class="d-none d-lg-block">
<table class="table table-hover responsive-table" aria-label="backups list" id="backup_table"
style="table-layout:fixed;">
<thead>
<tr class="rounded">
<th scope="col" style="width: 15%; min-width: 10px;">{{ translate('serverBackups', 'name',
data['lang']) }} </th>
<th scope="col" style="width: 10%; min-width: 10px;">{{ translate('serverBackups', 'status',
data['lang']) }} </th>
<th scope="col" style="width: 50%; min-width: 50px;">{{ translate('serverBackups',
'storageLocation', data['lang']) }}</th>
<th scope="col" style="width: 10%; min-width: 50px;">{{ translate('serverBackups',
'maxBackups', data['lang']) }}</th>
<th scope="col" style="width: 10%; min-width: 50px;">{{ translate('serverBackups', 'actions',
data['lang']) }}</th>
</tr>
</thead>
<tbody>
{% for backup in data['backups'] %}
<tr>
<td id="{{backup.backup_name}}" class="id">
<p>{{backup.backup_name}}</p>
<br>
{% if backup.default %}
<span class="badge-pill badge-outline-warning">{{ translate('serverBackups', 'default',
data['lang']) }}</span><small><button class="badge-pill badge-outline-info backup-explain"
data-explain="{{ translate('serverBackups', 'defaultExplain', data['lang'])}}"><i
class="fa-solid fa-question"></i></button></small>
{% end %}
</td>
<td>
<div id="{{backup.backup_id}}_status">
<button class="btn btn-outline-success backup-status" data-status="{{ backup.status }}"
data-Standby="{{ translate('serverBackups', 'standby', data['lang'])}}"
data-Failed="{{ translate('serverBackups', 'failed', data['lang'])}}"></button>
</div>
</td>
<td id="{{backup.backup_location}}" class="type">
<p style="overflow: scroll;" class="no-scroll">{{backup.backup_location}}</p>
</td>
<td id="{{backup.max_backups}}" class="trigger" style="overflow: scroll; max-width: 30px;">
<p>{{backup.max_backups}}</p>
</td>
<td id="backup_edit" class="action">
<button
onclick="window.location.href=`/panel/edit_backup?id={{ data['server_stats']['server_id']['server_id'] }}&backup_id={{backup.backup_id}}`"
class="btn btn-info">
<i class="fas fa-pencil-alt"></i>
</button>
{% if not backup.default %}
<button data-backup={{ backup.backup_id }} class="btn btn-danger del_button">
<i class="fas fa-trash" aria-hidden="true"></i>
</button>
{% end %}
<button data-backup={{ backup.backup_id }} data-toggle="tooltip"
title="{{ translate('serverBackups', 'run', data['lang']) }}"
class="btn btn-outline-warning run-backup backup_now_button">
<i class="fa-solid fa-forward"></i>
</button>
</td>
</tr>
{% end %}
</tbody>
</table>
</div>
<div class="d-block d-lg-none">
<table aria-label="backups list" class="table table-hover responsive-table" id="backup_table_mini"
style="table-layout:fixed;">
<thead>
<tr class="rounded">
<th style="width: 40%; min-width: 10px;">Name
</th>
<th style="width: 40%; min-width: 50px;">{{ translate('serverBackups', 'edit', data['lang'])
}}</th>
</tr>
</thead>
<tbody>
{% for backup in data['backups'] %}
<tr>
<td id="{{backup.backup_name}}" class="id">
<p>{{backup.backup_name}}</p>
<br>
<div id="{{backup.backup_id}}_status">
<button class="btn btn-outline-success backup-status" data-status="{{ backup.status }}"
data-Standby="{{ translate('serverBackups', 'standby', data['lang'])}}"
data-Failed="{{ translate('serverBackups', 'failed', data['lang'])}}"></button>
</div>
<br>
{% if backup.default %}
<span class="badge-pill badge-outline-warning">{{ translate('serverBackups', 'default',
data['lang']) }}</span><small><button class="badge-pill badge-outline-info backup-explain"
data-explain="{{ translate('serverBackups', 'defaultExplain', data['lang'])}}"><i
class="fa-solid fa-question"></i></button></small>
{% end %}
</td>
<td id="backup_edit" class="action">
<button
onclick="window.location.href=`/panel/edit_backup?id={{ data['server_stats']['server_id']['server_id'] }}&backup_id={{backup.backup_id}}`"
class="btn btn-info">
<i class="fas fa-pencil-alt"></i>
</button>
{% if not backup.default %}
<button data-backup={{ backup.backup_id }} class="btn btn-danger del_button">
<i class="fas fa-trash" aria-hidden="true"></i>
</button>
{% end %}
<button data-backup={{ backup.backup_id }} data-toggle="tooltip"
title="{{ translate('serverBackups', 'run', data['lang']) }}"
class="btn btn-outline-warning test-socket backup_now_button">
<i class="fa-solid fa-forward"></i>
</button>
</td>
</tr>
{% end %}
</tbody>
</table>
</div>
{% end %}
</div>
</div>
</div>
</div>
<div class="col-md-12 col-sm-12">
<br>
<br>
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="fas fa-server"></i> {{ translate('serverBackups', 'excludedBackups',
data['lang']) }} <small class="text-muted ml-1"></small> </h4>
</div>
<br>
<ul>
{% for item in data['exclusions'] %}
<li>{{item}}</li>
<br>
{% end %}
</ul>
</div>
</div>
</div>
@ -298,7 +242,7 @@
{% block js %}
<script>
const server_id = new URLSearchParams(document.location.search).get('id')
const serverId = new URLSearchParams(document.location.search).get('id')
//used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security
@ -307,183 +251,105 @@
return r ? r[1] : undefined;
}
async function backup_started() {
async function backup_started(backup_id) {
const token = getCookie("_xsrf")
let res = await fetch(`/api/v2/servers/${server_id}/action/backup_server`, {
method: 'POST',
headers: {
'X-XSRFToken': token
}
});
let responseData = await res.json();
if (responseData.status === "ok") {
console.log(responseData);
$("#backup_button").html(`<div class="progress" style="height: 15px;">
<div class="progress-bar progress-bar-striped progress-bar-animated" id="backup_progress_bar"
role="progressbar" style="width:{{data['backup_stats']['percent']}}%;"
aria-valuenow="{{data['backup_stats']['percent']}}" aria-valuemin="0" aria-valuemax="100">{{
data['backup_stats']['percent'] }}%</div>
</div>
<p>Backing up <i class="fas fa-spin fa-spinner"></i> <span
id="total_files">{{data['server_stats']['world_size']}}</span></p>`);
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
console.log(backup_id)
let res = await fetch(`/api/v2/servers/${serverId}/action/backup_server/${backup_id}/`, {
method: 'POST',
headers: {
'X-XSRFToken': token
}
});
let responseData = await res.json();
if (responseData.status === "ok") {
console.log(responseData);
$("#backup_button").prop('disabled', true)
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
return;
}
async function del_backup(filename, id) {
async function del_backup(backup_id) {
const token = getCookie("_xsrf")
let contents = JSON.stringify({"filename": filename})
let res = await fetch(`/api/v2/servers/${id}/backups/backup/`, {
let res = await fetch(`/api/v2/servers/${serverId}/backups/backup/${backup_id}`, {
method: 'DELETE',
headers: {
'token': token,
},
body: contents
body: {}
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.reload();
}else{
bootbox.alert({"title": responseData.status,
"message": responseData.error})
}
}
async function restore_backup(filename, id) {
const token = getCookie("_xsrf")
let contents = JSON.stringify({"filename": filename})
var dialog = bootbox.dialog({
message: "<i class='fa fa-spin fa-spinner'></i> {{ translate('serverBackups', 'restoring', data['lang']) }}",
closeButton: false
});
let res = await fetch(`/api/v2/servers/${id}/backups/backup/`, {
method: 'POST',
headers: {
'token': token,
},
body: contents
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.href = "/panel/dashboard";
}else{
bootbox.alert({"title": responseData.status,
"message": responseData.error})
}
}
$("#before-check").on("click", function () {
if ($("#before-check:checked").val()) {
$("#backup_before").css("display", "inline-block");
} else {
$("#backup_before").css("display", "none");
$("#backup_before").val("");
}
});
$("#after-check").on("click", function () {
if ($("#after-check:checked").val()) {
$("#backup_after").css("display", "inline-block");
} else {
$("#backup_after").css("display", "none");
$("#backup_after").val("");
}
});
function replacer(key, value) {
if (key != "backup_before" && key != "backup_after") {
if (typeof value == "boolean" || key === "executable_update_url") {
return value
} else {
return (isNaN(value) ? value : +value);
}
} else {
return value;
bootbox.alert({
"title": responseData.status,
"message": responseData.error
})
}
}
$(document).ready(function () {
$("#backup-form").on("submit", async function (e) {
e.preventDefault();
const token = getCookie("_xsrf")
let backupForm = document.getElementById("backup-form");
let formData = new FormData(backupForm);
//Remove checks that we don't need in form data.
formData.delete("after-check");
formData.delete("before-check");
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
//We need to make sure these are sent regardless of whether or not they're checked
formDataObject.compress = $("#compress").prop('checked');
formDataObject.shutdown = $("#shutdown").prop('checked');
let excluded = [];
$('input.excluded:checkbox:checked').each(function () {
excluded.push($(this).val());
});
if ($("#root_files_button").hasClass("clicked")){
formDataObject.exclusions = excluded;
}
delete formDataObject.root_path
console.log(excluded);
console.log(formDataObject);
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify(formDataObject, replacer);
console.log(formDataJsonString);
let res = await fetch(`/api/v2/servers/${server_id}/backups/`, {
method: 'PATCH',
headers: {
'X-XSRFToken': token
},
body: formDataJsonString,
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.reload();
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}
});
try {
if ($('#backup_path').val() == '') {
console.log('true')
try {
document.getElementById('backup_now_button').disabled = true;
} catch {
}
} else {
document.getElementById('backup_now_button').disabled = false;
}
} catch {
try {
document.getElementById('backup_now_button').disabled = false;
} catch {
}
}
console.log("ready!");
$("#backup_config_box").hide();
$("#backup_save_note").hide();
$("#show_config").click(function () {
$("#backup_config_box").toggle();
$('#backup_button').hide();
$('#backup_save_note').show();
$('#backup_data').hide();
$(".backup-explain").on("click", function () {
bootbox.alert($(this).data("explain"));
});
$(".backup-status").on("click", function () {
if ($(this).data('message') != "") {
bootbox.alert($(this).data('message'));
}
});
$('.backup-status').each(function () {
// Get the JSON string from the element's text
var data = $(this).data('status');
try {
// Update the element's text with the status value
$(this).text($(this).data(data["status"].toLowerCase()));
// Optionally, add classes based on status to style the element
$(this).attr('data-message', data["message"]);
if (data.status === 'Active') {
$(this).removeClass();
$(this).addClass('badge-pill badge-outline-success btn');
} else if (data.status === 'Failed') {
$(this).removeClass();
$(this).addClass('badge-pill badge-outline-danger btn');
} else if (data.status === 'Standby') {
$(this).removeClass();
$(this).addClass('badge-pill badge-outline-secondary btn');
}
} catch (e) {
console.error('Invalid JSON string:', e);
}
});
if (webSocket) {
webSocket.on('backup_status', function (backup) {
text = ``;
console.log(backup)
if (backup.percent >= 100) {
$(`#${backup.backup_id}_status`).html(`<span class="badge-pill badge-outline-success backup-status"
>Completed</span>`);
setTimeout(function () {
window.location.reload(1);
}, 5000);
} else {
text = `<div class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" style="width:${backup.percent}%;"
aria-valuenow="${backup.percent}" aria-valuemin="0" aria-valuemax="100">${backup.percent}%</div>`
$(`#${backup.backup_id}_status`).html(text);
}
});
}
$('#backup_table').DataTable({
"order": [[1, "desc"]],
"paging": false,
@ -491,11 +357,12 @@
"searching": true,
"ordering": true,
"info": true,
"autoWidth": false,
"responsive": true,
"autoWidth": true,
"responsive": false,
});
$(".del_button").click(function () {
let backup = $(this).data('backup');
var file_to_del = $(this).data("file");
var backup_path = $(this).data('backup_path');
@ -515,8 +382,8 @@
callback: function (result) {
console.log(result);
if (result == true) {
var full_path = backup_path + '/' + file_to_del;
del_backup(file_to_del, server_id);
del_backup(backup);
}
}
});
@ -541,13 +408,13 @@
callback: function (result) {
console.log(result);
if (result == true) {
restore_backup(file_to_restore, server_id);
restore_backup(file_to_restore, serverId);
}
}
});
});
$("#backup_now_button").click(function () {
backup_started();
$(".backup_now_button").click(function () {
backup_started($(this).data('backup'));
});
});
@ -591,70 +458,55 @@
bootbox.alert("You must input a path before selecting this button");
}
});
if (webSocket) {
webSocket.on('backup_status', function (backup) {
if (backup.percent >= 100) {
document.getElementById('backup_progress_bar').innerHTML = '100%';
document.getElementById('backup_progress_bar').style.width = '100%';
setTimeout(function () {
window.location.reload(1);
}, 5000);
} else {
document.getElementById('backup_progress_bar').innerHTML = backup.percent + '%';
document.getElementById('backup_progress_bar').style.width = backup.percent + '%';
document.getElementById('total_files').innerHTML = backup.total_files;
}
});
}
function getDirView(event){
function getDirView(event) {
let path = event.target.parentElement.getAttribute("data-path");
if (document.getElementById(path).classList.contains('clicked')) {
return;
}else{
} else {
getTreeView(path);
}
}
async function getTreeView(path){
async function getTreeView(path) {
console.log(path)
const token = getCookie("_xsrf");
let res = await fetch(`/api/v2/servers/${server_id}/files`, {
method: 'POST',
headers: {
'X-XSRFToken': token
},
body: JSON.stringify({"page": "backups", "path": path}),
let res = await fetch(`/api/v2/servers/${serverId}/files`, {
method: 'POST',
headers: {
'X-XSRFToken': token
},
body: JSON.stringify({ "page": "backups", "path": path }),
});
let responseData = await res.json();
if (responseData.status === "ok") {
console.log(responseData);
process_tree_response(responseData);
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
let responseData = await res.json();
if (responseData.status === "ok") {
console.log(responseData);
process_tree_response(responseData);
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
}
}
function process_tree_response(response) {
let path = response.data.root_path.path;
let text = `<ul class="tree-nested d-block" id="${path}ul">`;
Object.entries(response.data).forEach(([key, value]) => {
if (key === "root_path" || key === "db_stats"){
//continue is not valid in for each. Return acts as a continue.
return;
}
Object.entries(response.data).forEach(([key, value]) => {
if (key === "root_path" || key === "db_stats") {
//continue is not valid in for each. Return acts as a continue.
return;
}
let checked = ""
let dpath = value.path;
let filename = key;
if (value.excluded){
if (value.excluded) {
checked = "checked"
}
if (value.dir){
if (value.dir) {
text += `<li class="tree-item" data-path="${dpath}">
\n<div id="${dpath}" data-path="${dpath}" data-name="${filename}" class="tree-caret tree-ctx-item tree-folder">
<input type="checkbox" class="checkBoxClass excluded" value="${dpath}" ${checked}>
@ -664,7 +516,7 @@
<strong>${filename}</strong>
</span>
</input></div><li>`
}else{
} else {
text += `<li
class="d-block tree-ctx-item tree-file"
data-path="${dpath}"
@ -674,30 +526,30 @@
}
});
text += `</ul>`;
if(response.data.root_path.top){
if (response.data.root_path.top) {
try {
document.getElementById('main-tree-div').innerHTML += text;
document.getElementById('main-tree').parentElement.classList.add("clicked");
} catch {
document.getElementById('files-tree').innerHTML = text;
}
}else{
document.getElementById('main-tree-div').innerHTML += text;
document.getElementById('main-tree').parentElement.classList.add("clicked");
} catch {
document.getElementById('files-tree').innerHTML = text;
}
} else {
try {
document.getElementById(path + "span").classList.add('tree-caret-down');
document.getElementById(path).innerHTML += text;
document.getElementById(path).classList.add("clicked");
} catch {
console.log("Bad")
}
document.getElementById(path + "span").classList.add('tree-caret-down');
document.getElementById(path).innerHTML += text;
document.getElementById(path).classList.add("clicked");
} catch {
console.log("Bad")
}
var toggler = document.getElementById(path + "span");
var toggler = document.getElementById(path + "span");
if (toggler.classList.contains('files-tree-title')) {
document.getElementById(path + "span").addEventListener("click", function caretListener() {
document.getElementById(path + "ul").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
});
}
if (toggler.classList.contains('files-tree-title')) {
document.getElementById(path + "span").addEventListener("click", function caretListener() {
document.getElementById(path + "ul").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
});
}
}
}

View File

@ -0,0 +1,758 @@
{% extends ../base.html %}
{% block meta %}
{% end %}
{% block title %}Crafty Controller - {{ translate('serverDetails', 'serverDetails', data['lang']) }}{% end %}
{% block content %}
<div class="content-wrapper">
<!-- Page Title Header Starts-->
<div class="row page-title-header">
<div class="col-12">
<div class="page-header">
<h4 class="page-title">
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{
data['server_stats']['server_id']['server_name'] }}
<br />
<small>UUID: {{ data['server_stats']['server_id']['server_id'] }}</small>
</h4>
</div>
</div>
</div>
<!-- Page Title Header Ends-->
{% include "parts/details_stats.html %}
<div class="row">
<div class="col-sm-12 grid-margin">
<div class="card">
<div class="card-body pt-0">
<span class="d-none d-sm-block">
{% include "parts/server_controls_list.html %}
</span>
<span class="d-block d-sm-none">
{% include "parts/m_server_controls_list.html %}
</span>
<div class="row">
<div class="col-md-6 col-sm-12">
<br>
<br>
<div id="{{data['backup_config'].get('backup_id', None)}}_status" class="progress"
style="height: 15px; display: none;">
</div>
{% if data['backing_up'] %}
<p>Backing up <i class="fas fa-spin fa-spinner"></i> <span
id="total_files">{{data['server_stats']['world_size']}}</span></p>
{% end %}
<br>
{% if not data['backing_up'] %}
<div id="backup_button" class="form-group">
<button class="btn btn-primary" id="backup_now_button">{{ translate('serverBackups', 'backupNow',
data['lang']) }}</button>
</div>
{% end %}
<form id="backup-form" class="forms-sample">
<div class="form-group">
<label for="backup_name">{{ translate('serverBackups', 'name', data['lang']) }}
{% if data["backup_config"].get("default", None) %}
&nbsp;&nbsp; <span class="badge-pill badge-outline-warning">{{ translate('serverBackups', 'default',
data['lang']) }}</span><small><button class="badge-pill badge-outline-info backup-explain"
data-explain="{{ translate('serverBackups', 'defaultExplain', data['lang'])}}"><i
class="fa-solid fa-question"></i></button></small>
{% end %}
</label>
{% if data["backup_config"].get("backup_id", None) %}
<input type="text" class="form-control" name="backup_name" id="backup_name"
value="{{ data['backup_config']['backup_name'] }}">
{% else %}
<input type="text" class="form-control" name="backup_name" id="backup_name"
placeholder="{{ translate('serverBackups', 'myBackup', data['lang']) }}">
{% end %}
<br>
<br>
{% if data['super_user'] %}
<label for="server_name">{{ translate('serverBackups', 'storageLocation', data['lang']) }} <small
class="text-muted ml-1"> - {{ translate('serverBackups', 'storageLocationDesc', data['lang'])
}}</small> </label>
<input type="text" class="form-control" name="backup_location" id="backup_location"
value="{{ data['backup_config']['backup_location'] }}"
placeholder="{{ translate('serverBackups', 'storageLocation', data['lang']) }}">
{% end %}
</div>
<div class="form-group">
<label for="server_path">{{ translate('serverBackups', 'maxBackups', data['lang']) }} <small
class="text-muted ml-1"> - {{ translate('serverBackups', 'maxBackupsDesc', data['lang'])
}}</small> </label>
<input type="text" class="form-control" name="max_backups" id="max_backups"
value="{{ data['backup_config']['max_backups'] }}"
placeholder="{{ translate('serverBackups', 'maxBackups', data['lang']) }}">
</div>
<div class="form-group">
<div class="custom-control custom-switch">
{% if data['backup_config']['compress'] %}
<input type="checkbox" class="custom-control-input" id="compress" name="compress" checked=""
value="True">
{% else %}
<input type="checkbox" class="custom-control-input" id="compress" name="compress" value="True">
{% end %}
<label for="compress" class="custom-control-label">{{ translate('serverBackups', 'compress',
data['lang']) }}</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
{% if data['backup_config']['shutdown']%}
<input type="checkbox" class="custom-control-input" id="shutdown" name="shutdown" checked=""
value="True">
{% else %}
<input type="checkbox" class="custom-control-input" id="shutdown" name="shutdown" value="True">
{% end %}
<label for="shutdown" class="custom-control-label">{{ translate('serverBackups', 'shutdown',
data['lang']) }}</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
{% if data['backup_config']['before'] %}
<input type="checkbox" class="custom-control-input" id="before-check" name="before-check" checked>
<input type="text" class="form-control hidden-input" name="before" id="backup_before"
value="{{ data['backup_config']['before'] }}" placeholder="We enter the / for you"
style="display: inline-block;">
{% else %}
<input type="checkbox" class="custom-control-input" id="before-check" name="before-check">
<input type="text" class="form-control hidden-input" name="before" id="backup_before" value=""
placeholder="We enter the / for you." style="display: none;">
{% end %}
<label for="before-check" class="custom-control-label">{{
translate('serverBackups', 'before', data['lang']) }}</label>
</div>
</div>
<div class="form-group">
<div class="custom-control custom-switch">
{% if data['backup_config']['after'] %}
<input type="checkbox" class="custom-control-input" id="after-check" name="after-check" checked>
<input type="text" class="form-control hidden-input" name="after" id="backup_after"
value="{{ data['backup_config']['after'] }}" placeholder="We enter the / for you"
style="display: inline-block;">
<br>
{% else %}
<input type="checkbox" class="custom-control-input" id="after-check" name="after-check">
<input type="text" class="form-control hidden-input" name="after" id="backup_after" value=""
placeholder="We enter the / for you." style="display: none;">
{% end %}
<label for="after-check" class="custom-control-label">{{
translate('serverBackups', 'after', data['lang']) }}</label>
</div>
</div>
<div class="form-group">
<label for="server">{{ translate('serverBackups', 'exclusionsTitle', data['lang']) }} <small> - {{
translate('serverBackups', 'excludedChoose', data['lang']) }}</small></label>
<br>
<button class="btn btn-primary mr-2" id="root_files_button"
data-server_path="{{ data['server_stats']['server_id']['path']}}" type="button">{{
translate('serverBackups', 'clickExclude', data['lang']) }}</button>
</div>
<div class="modal fade" id="dir_select" tabindex="-1" aria-labelledby="dir_select" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="exampleModalLongTitle">{{ translate('serverBackups',
'excludedChoose', data['lang']) }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<div class="tree-ctx-item" id="main-tree-div" data-path=""
style="overflow: scroll; max-height:75%;">
<input type="checkbox" id="main-tree-input" name="root_path" value="" disabled>
<span id="main-tree" class="files-tree-title tree-caret-down root-dir" data-path="">
<i class="far fa-folder"></i>
<i class="far fa-folder-open"></i>
{{ translate('serverFiles', 'files', data['lang']) }}
</span>
</input>
</div>
</div>
<div class="modal-footer">
<button type="button" id="modal-cancel" class="btn btn-secondary" data-dismiss="modal"><i
class="fa-solid fa-xmark"></i></button>
<button type="button" id="modal-okay" data-dismiss="modal" class="btn btn-primary"><i
class="fa-solid fa-thumbs-up"></i></button>
</div>
</div>
</div>
</div>
<button type="submit" class="btn btn-success mr-2">{{ translate('serverBackups', 'save', data['lang'])
}}</button>
<button type="reset" class="btn btn-light cancel-button">{{ translate('serverBackups', 'cancel',
data['lang'])
}}</button>
</form>
</div>
<div class="col-md-6 col-sm-12">
<div class="text-center">
<table class="table table-responsive dataTable" id="backup_table">
<h4 class="card-title">{{ translate('serverBackups', 'currentBackups', data['lang']) }}</h4>
<thead>
<tr>
<th>{{ translate('serverBackups', 'options', data['lang']) }}</th>
<th>{{ translate('serverBackups', 'path', data['lang']) }}</th>
<th>{{ translate('serverBackups', 'size', data['lang']) }}</th>
</tr>
</thead>
<tbody>
{% for backup in data['backup_list'] %}
<tr>
<td>
<a href="/panel/download_backup?file={{ backup['path'] }}&id={{ data['server_stats']['server_id']['server_id'] }}&backup_id={{ data['backup_config']['backup_id']}}"
class="btn btn-primary">
<i class="fas fa-download" aria-hidden="true"></i>
{{ translate('serverBackups', 'download', data['lang']) }}
</a>
<br>
<br>
<button data-file="{{ backup['path'] }}"
data-backup_location="{{ data['backup_config']['backup_location'] }}"
class="btn btn-danger del_button">
<i class="fas fa-trash" aria-hidden="true"></i>
{{ translate('serverBackups', 'delete', data['lang']) }}
</button>
<button data-file="{{ backup['path'] }}" class="btn btn-warning restore_button">
<i class="fas fa-undo-alt" aria-hidden="true"></i>
{{ translate('serverBackups', 'restore', data['lang']) }}
</button>
</td>
<td>{{ backup['path'] }}</td>
<td>{{ backup['size'] }}</td>
</tr>
{% end %}
</tbody>
</table>
</div>
</div>
</div>
<div class="col-md-12 col-sm-12">
<br>
<br>
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="fas fa-server"></i> {{ translate('serverBackups', 'excludedBackups',
data['lang']) }} <small class="text-muted ml-1"></small> </h4>
</div>
<br>
<ul>
{% for item in data['exclusions'] %}
<li>{{item}}</li>
<br>
{% end %}
</ul>
</div>
</div>
</div>
</div>
</div>
</div>
<style>
/* Remove default bullets */
.tree-view,
.tree-nested {
list-style-type: none;
margin: 0;
padding: 0;
margin-left: 10px;
}
/* Style the items */
.tree-item,
.files-tree-title {
cursor: pointer;
user-select: none;
/* Prevent text selection */
}
/* Create the caret/arrow with a unicode, and style it */
.tree-caret .fa-folder {
display: inline-block;
}
.tree-caret .fa-folder-open {
display: none;
}
/* Rotate the caret/arrow icon when clicked on (using JavaScript) */
.tree-caret-down .fa-folder {
display: none;
}
.tree-caret-down .fa-folder-open {
display: inline-block;
}
/* Hide the nested list */
.tree-nested {
display: none;
}
</style>
<!-- content-wrapper ends -->
{% end %}
{% block js %}
<script>
const server_id = new URLSearchParams(document.location.search).get('id')
const backup_id = new URLSearchParams(document.location.search).get('backup_id')
//used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security
function getCookie(name) {
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined;
}
async function backup_started() {
const token = getCookie("_xsrf")
let res = await fetch(`/api/v2/servers/${server_id}/action/backup_server/${backup_id}`, {
method: 'POST',
headers: {
'X-XSRFToken': token
}
});
let responseData = await res.json();
if (responseData.status === "ok") {
console.log(responseData);
$("#backup_button").prop('disabled', true)
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
return;
}
async function del_backup(filename, id) {
const token = getCookie("_xsrf")
let contents = JSON.stringify({ "filename": filename })
let res = await fetch(`/api/v2/servers/${server_id}/backups/backup/${backup_id}/files/`, {
method: 'DELETE',
headers: {
'token': token,
},
body: contents
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.reload();
} else {
bootbox.alert({
"title": responseData.status,
"message": responseData.error
})
}
}
async function restore_backup(filename, id) {
const token = getCookie("_xsrf")
let contents = JSON.stringify({ "filename": filename })
var dialog = bootbox.dialog({
message: "<i class='fa fa-spin fa-spinner'></i> {{ translate('serverBackups', 'restoring', data['lang']) }}",
closeButton: false
});
let res = await fetch(`/api/v2/servers/${server_id}/backups/backup/${backup_id}/`, {
method: 'POST',
headers: {
'token': token,
},
body: contents
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.href = "/panel/dashboard";
} else {
bootbox.alert({
"title": responseData.status,
"message": responseData.error
})
}
}
$("#before-check").on("click", function () {
if ($("#before-check:checked").val()) {
$("#backup_before").css("display", "inline-block");
} else {
$("#backup_before").css("display", "none");
$("#backup_before").val("");
}
});
$("#after-check").on("click", function () {
if ($("#after-check:checked").val()) {
$("#backup_after").css("display", "inline-block");
} else {
$("#backup_after").css("display", "none");
$("#backup_after").val("");
}
});
function replacer(key, value) {
if (key === "excluded_dirs") {
if (value == 0) {
return []
} else {
return value
}
}
if (key != "before" && key != "after") {
if (typeof value == "boolean" || key === "executable_update_url") {
return value
} else {
return (isNaN(value) ? value : +value);
}
} else {
return value;
}
}
$(document).ready(function () {
$(".backup-explain").on("click", function (e) {
e.preventDefault();
bootbox.alert($(this).data("explain"));
});
$(".cancel-button").on("click", function () {
location.href = `/panel/server_detail?id=${server_id}&subpage=backup`
});
webSocket.on('backup_status', function (backup) {
text = ``;
$(`#${backup.backup_id}_status`).show();
if (backup.percent >= 100) {
$(`#${backup.backup_id}_status`).hide()
setTimeout(function () {
window.location.reload(1);
}, 5000);
} else {
text = `<div class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar" style="width:${backup.percent}%;"
aria-valuenow="${backup.percent}" aria-valuemin="0" aria-valuemax="100">${backup.percent}%</div>`
$(`#${backup.backup_id}_status`).html(text);
}
});
$("#backup-form").on("submit", async function (e) {
e.preventDefault();
const token = getCookie("_xsrf")
let backupForm = document.getElementById("backup-form");
let formData = new FormData(backupForm);
//Remove checks that we don't need in form data.
formData.delete("after-check");
formData.delete("before-check");
//Create an object from the form data entries
let formDataObject = Object.fromEntries(formData.entries());
//We need to make sure these are sent regardless of whether or not they're checked
formDataObject.compress = $("#compress").prop('checked');
formDataObject.shutdown = $("#shutdown").prop('checked');
if ($("#root_files_button").hasClass("clicked")) {
excluded = []
$('input.excluded:checkbox:checked').each(function () {
excluded.push($(this).val());
});
formDataObject.excluded_dirs = excluded;
}
delete formDataObject.root_path
console.log(formDataObject);
// Format the plain form data as JSON
let formDataJsonString = JSON.stringify(formDataObject, replacer);
console.log(formDataJsonString);
let url = `/api/v2/servers/${server_id}/backups/backup/${backup_id}/`
let method = "PATCH"
if (!backup_id) {
url = `/api/v2/servers/${server_id}/backups/`
method = "POST";
}
let res = await fetch(url, {
method: method,
headers: {
'X-XSRFToken': token
},
body: formDataJsonString,
});
let responseData = await res.json();
if (responseData.status === "ok") {
window.location.href = `/panel/server_detail?id=${server_id}&subpage=backup`;
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}
});
try {
if ($('#backup_location').val() == '') {
console.log('true')
try {
document.getElementById('backup_now_button').disabled = true;
} catch {
}
} else {
document.getElementById('backup_now_button').disabled = false;
}
} catch {
try {
document.getElementById('backup_now_button').disabled = false;
} catch {
}
}
console.log("ready!");
$("#backup_config_box").hide();
$("#backup_save_note").hide();
$("#show_config").click(function () {
$("#backup_config_box").toggle();
$('#backup_button').hide();
$('#backup_save_note').show();
$('#backup_data').hide();
});
$('#backup_table').DataTable({
"order": [[1, "desc"]],
"paging": false,
"lengthChange": false,
"searching": true,
"ordering": true,
"info": true,
"autoWidth": false,
"responsive": true,
});
$(".del_button").click(function () {
var file_to_del = $(this).data("file");
var backup_location = $(this).data('backup_location');
console.log("file to delete is" + file_to_del);
bootbox.confirm({
title: "{% raw translate('serverBackups', 'destroyBackup', data['lang']) %}",
message: "{{ translate('serverBackups', 'confirmDelete', data['lang']) }}",
buttons: {
cancel: {
label: '<i class="fas fa-times"></i> {{ translate("serverBackups", "cancel", data['lang']) }}'
},
confirm: {
label: '<i class="fas fa-check"></i> {{ translate("serverBackups", "confirm", data['lang']) }}'
}
},
callback: function (result) {
console.log(result);
if (result == true) {
var full_path = backup_location + '/' + file_to_del;
del_backup(file_to_del, server_id);
}
}
});
});
$(".restore_button").click(function () {
var file_to_restore = $(this).data("file");
bootbox.confirm({
title: "{{ translate('serverBackups', 'restore', data['lang']) }} " + file_to_restore,
message: "{{ translate('serverBackups', 'confirmRestore', data['lang']) }}",
buttons: {
cancel: {
label: '<i class="fas fa-times"></i> {{ translate("serverBackups", "cancel", data['lang']) }}'
},
confirm: {
label: '<i class="fas fa-check"></i> {{ translate("serverBackups", "restore", data['lang']) }}',
className: 'btn-outline-danger'
}
},
callback: function (result) {
console.log(result);
if (result == true) {
restore_backup(file_to_restore, server_id);
}
}
});
});
$("#backup_now_button").click(function () {
backup_started();
});
});
document.getElementById("modal-cancel").addEventListener("click", function () {
document.getElementById("root_files_button").classList.remove('clicked');
document.getElementById("main-tree-div").innerHTML = '<input type="checkbox" id="main-tree-input" name="root_path" value="" disabled><span id="main-tree" class="files-tree-title tree-caret-down root-dir" data-path=""><i class="far fa-folder"></i><i class="far fa-folder-open"></i>{{ translate("serverFiles", "files", data["lang"]) }}</span></input>'
})
document.getElementById("root_files_button").addEventListener("click", function () {
if ($("#root_files_button").data('server_path') != "") {
if (document.getElementById('root_files_button').classList.contains('clicked')) {
show_file_tree();
return;
} else {
document.getElementById('root_files_button').classList.add('clicked');
}
path = $("#root_files_button").data('server_path')
console.log($("#root_files_button").data('server_path'))
const token = getCookie("_xsrf");
var dialog = bootbox.dialog({
message: '<p class="text-center mb-0"><i class="fa fa-spin fa-cog"></i> Please wait while we gather your files...</p>',
closeButton: false
});
setTimeout(function () {
var x = document.querySelector('.bootbox');
if (x) {
x.remove()
}
var x = document.querySelector('.modal-backdrop');
if (x) {
x.remove()
}
document.getElementById('main-tree-input').setAttribute('value', path)
getTreeView(path);
show_file_tree();
}, 5000);
} else {
bootbox.alert("You must input a path before selecting this button");
}
});
function getDirView(event) {
let path = event.target.parentElement.getAttribute("data-path");
if (document.getElementById(path).classList.contains('clicked')) {
return;
} else {
getTreeView(path);
}
}
async function getTreeView(path) {
console.log(path)
const token = getCookie("_xsrf");
let url = `/api/v2/servers/${server_id}/files/${backup_id}`
if (!backup_id) {
url = `/api/v2/servers/${server_id}/files/`
console.log("NEW URL")
}
console.log(url);
let res = await fetch(url, {
method: 'POST',
headers: {
'X-XSRFToken': token
},
body: JSON.stringify({ "page": "backups", "path": path }),
});
let responseData = await res.json();
if (responseData.status === "ok") {
console.log(responseData);
process_tree_response(responseData);
} else {
bootbox.alert({
title: responseData.status,
message: responseData.error
});
}
}
function process_tree_response(response) {
let path = response.data.root_path.path;
let text = `<ul class="tree-nested d-block" id="${path}ul">`;
Object.entries(response.data).forEach(([key, value]) => {
if (key === "root_path" || key === "db_stats") {
//continue is not valid in for each. Return acts as a continue.
return;
}
let checked = ""
let dpath = value.path;
let filename = key;
if (value.excluded) {
checked = "checked"
}
if (value.dir) {
text += `<li class="tree-item" data-path="${dpath}">
\n<div id="${dpath}" data-path="${dpath}" data-name="${filename}" class="tree-caret tree-ctx-item tree-folder">
<input type="checkbox" class="checkBoxClass excluded" value="${dpath}" ${checked}>
<span id="${dpath}span" class="files-tree-title" data-path="${dpath}" data-name="${filename}" onclick="getDirView(event)">
<i style="color: var(--info);" class="far fa-folder"></i>
<i style="color: var(--info);" class="far fa-folder-open"></i>
<strong>${filename}</strong>
</span>
</input></div><li>`
} else {
text += `<li
class="d-block tree-ctx-item tree-file"
data-path="${dpath}"
data-name="${filename}"
onclick=""><input type='checkbox' class="checkBoxClass excluded" name='root_path' value="${dpath}" ${checked}><span style="margin-right: 6px;">
<i class="far fa-file"></i></span></input>${filename}</li>`
}
});
text += `</ul>`;
if (response.data.root_path.top) {
try {
document.getElementById('main-tree-div').innerHTML += text;
document.getElementById('main-tree').parentElement.classList.add("clicked");
} catch {
document.getElementById('files-tree').innerHTML = text;
}
} else {
try {
document.getElementById(path + "span").classList.add('tree-caret-down');
document.getElementById(path).innerHTML += text;
document.getElementById(path).classList.add("clicked");
} catch {
console.log("Bad")
}
var toggler = document.getElementById(path + "span");
if (toggler.classList.contains('files-tree-title')) {
document.getElementById(path + "span").addEventListener("click", function caretListener() {
document.getElementById(path + "ul").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
});
}
}
}
function getToggleMain(event) {
path = event.target.parentElement.getAttribute('data-path');
document.getElementById("files-tree").classList.toggle("d-block");
document.getElementById(path + "span").classList.toggle("tree-caret-down");
document.getElementById(path + "span").classList.toggle("tree-caret");
}
function show_file_tree() {
$("#dir_select").modal();
}
</script>
{% end %}

View File

@ -79,6 +79,24 @@
<option id="command" value="command">{{ translate('serverScheduleConfig', 'custom' , data['lang'])
}}</option>
</select>
<div id="ifBackup" style="display: none;">
<br>
<label for="action_id">{{ translate('serverSchedules', 'actionId' , data['lang']) }}<small
class="text-muted ml-1"></small> </label><br>
<select id="action_id" name="action_id"
class="form-control form-control-lg select-css" value="{{ data['schedule']['action_id'] }}">
{% for backup in data["backups"] %}
{% if backup.backup_id == data["schedule"]["action_id"] %}
<option id="{{backup.backup_id}}" value="{{backup.backup_id}}">{{backup.backup_name}}</option>
{% end %}
{% end %}
{% for backup in data["backups"] %}
{% if backup.backup_id != data["schedule"]["action_id"] %}
<option id="{{backup.backup_id}}" value="{{backup.backup_id}}">{{backup.backup_name}}</option>
{% end %}
{% end %}
</select>
</div>
</div>
<div id="ifBasic">
<div class="form-group">
@ -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") {

View File

@ -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)")

View File

@ -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=""))

View File

@ -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?",

View File

@ -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?",

View File

@ -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?",

View File

@ -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?",

View File

@ -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 ?",

View File

@ -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": "לא רואים הכל?",

View File

@ -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?",

View File

@ -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?",

View File

@ -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?",

View File

@ -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?",

View File

@ -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?",

View File

@ -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": "ไม่เห็นอะไรเลยใช่ใหม?",

View File

@ -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?",

View File

@ -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": "Нічого не бачите?",

View File

@ -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": "什么都看不到?",