mirror of
https://gitlab.com/crafty-controller/crafty-4.git
synced 2024-08-30 18:23:09 +00:00
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:
commit
eaeda3e746
@ -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))
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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:
|
||||
|
@ -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:
|
||||
|
@ -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 = {}
|
||||
|
||||
|
@ -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,
|
||||
}
|
||||
],
|
||||
)
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
),
|
||||
|
@ -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')}",
|
||||
|
@ -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"})
|
||||
|
@ -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"})
|
||||
|
@ -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
|
||||
|
@ -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(
|
||||
|
@ -22,6 +22,9 @@ task_patch_schema = {
|
||||
"action": {
|
||||
"type": "string",
|
||||
},
|
||||
"action_id": {
|
||||
"type": "string",
|
||||
},
|
||||
"interval": {"type": "integer"},
|
||||
"interval_type": {
|
||||
"type": "string",
|
||||
|
@ -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;
|
||||
}
|
@ -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">×</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");
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
758
app/frontend/templates/panel/server_backup_edit.html
Normal file
758
app/frontend/templates/panel/server_backup_edit.html
Normal 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) %}
|
||||
<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">×</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 %}
|
@ -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") {
|
||||
|
@ -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)")
|
||||
|
238
app/migrations/20240308_multi-backup.py
Normal file
238
app/migrations/20240308_multi-backup.py
Normal 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=""))
|
@ -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?",
|
||||
|
@ -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?",
|
||||
|
@ -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?",
|
||||
|
@ -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?",
|
||||
|
@ -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 ?",
|
||||
|
@ -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": "לא רואים הכל?",
|
||||
|
@ -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?",
|
||||
|
@ -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?",
|
||||
|
@ -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?",
|
||||
|
@ -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?",
|
||||
|
@ -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?",
|
||||
|
@ -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": "ไม่เห็นอะไรเลยใช่ใหม?",
|
||||
|
@ -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?",
|
||||
|
@ -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": "Нічого не бачите?",
|
||||
|
@ -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": "什么都看不到?",
|
||||
|
Loading…
Reference in New Issue
Block a user