mirror of
https://gitlab.com/crafty-controller/crafty-4.git
synced 2024-08-30 18:23:09 +00:00
Merge branch 'dev' into tweak/server-create-roles
This commit is contained in:
commit
0ed296adc6
@ -56,8 +56,8 @@ get_keys "${DIR}/en_EN.json" | sort > "${ref_keys}"
|
|||||||
|
|
||||||
# Iterate over each .json file in the directory
|
# Iterate over each .json file in the directory
|
||||||
for file in "${DIR}"/*.json; do
|
for file in "${DIR}"/*.json; do
|
||||||
# Check if file is a regular file and not en_EN.json, and does not contain "_incomplete" in its name
|
# Check if file is a regular file and not en_EN.json, humanized index and does not contain "_incomplete" in its name
|
||||||
if [[ -f "${file}" && "${file}" != "${DIR}/en_EN.json" && ! "${file}" =~ _incomplete ]]; then
|
if [[ -f "${file}" && "${file}" != "${DIR}/en_EN.json" && "${file}" != "${DIR}/humanized_index.json" && ! "${file}" =~ _incomplete ]]; then
|
||||||
|
|
||||||
# Get keys and subkeys from the current file
|
# Get keys and subkeys from the current file
|
||||||
current_keys=$(mktemp)
|
current_keys=$(mktemp)
|
||||||
|
17
CHANGELOG.md
17
CHANGELOG.md
@ -2,17 +2,34 @@
|
|||||||
## --- [4.4.1] - 2024/TBD
|
## --- [4.4.1] - 2024/TBD
|
||||||
### New features
|
### New features
|
||||||
TBD
|
TBD
|
||||||
|
### Refactor
|
||||||
|
- Backups | Allow multiple backup configurations ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/711))
|
||||||
|
- UploadAPI | Use Crafty's JWT authentication for file uploads ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/762))
|
||||||
|
- UploadAPI | Splice files on the frontend to allow chunked uploads as well as bulk uploads ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/762))
|
||||||
|
- UploadAPI | Enhance upload progress feedback on all upload pages ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/762))
|
||||||
|
- UploadAPI | Consolidate and improve speed on uploads, supporting 100mb+ uploads through Cloudflare(Free) ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/762))
|
||||||
### Bug fixes
|
### 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 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))
|
- Fix bug where full access gives minimal access ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/768))
|
||||||
|
- Bump tornado & requests for sec advisories ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/774))
|
||||||
- Ensure audit.log exists or create it on Crafty startup ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/771))
|
- Ensure audit.log exists or create it on Crafty startup ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/771))
|
||||||
|
- Fix typing issue on ID comparison causing general users to not be able to delete their own API keys ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/775))
|
||||||
|
- Fix user creation bug where it would fail when a role was selected ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/763))
|
||||||
|
- Security improvements for general user creations on roles page ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/763))
|
||||||
|
- Security improvements for general user creations on user page ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/763))
|
||||||
|
- Use UTC for tokens_valid_from in user config, to resolve token invalidation on instance TZ change ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/765))
|
||||||
### Tweaks
|
### Tweaks
|
||||||
- Add info note to default creds file ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/760))
|
- Add info note to default creds file ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/760))
|
||||||
- Remove navigation label from sidebar ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/766))
|
- Remove navigation label from sidebar ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/766))
|
||||||
|
- Do not allow slashes in server names ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/767))
|
||||||
- Add a thread dump to support logs ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/769))
|
- Add a thread dump to support logs ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/769))
|
||||||
- Remove text from status page and use symbols ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/770))
|
- Remove text from status page and use symbols ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/770))
|
||||||
|
- Add better feedback on when errors appear on user creation ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/763))
|
||||||
|
- Workaround cpu_freq call catching on obscure cpu architectures ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/776))
|
||||||
### Lang
|
### Lang
|
||||||
|
- Show natural language name instead of country code in User Config Lang select list ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/773))
|
||||||
- Add remaining `he_IL`, `th_TH` translations for 4.4.0 Release ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/761))
|
- Add remaining `he_IL`, `th_TH` translations for 4.4.0 Release ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/761))
|
||||||
|
- Mark `he_IL` incomplete ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/763))
|
||||||
<br><br>
|
<br><br>
|
||||||
|
|
||||||
## --- [4.4.0] - 2024/05/11
|
## --- [4.4.0] - 2024/05/11
|
||||||
|
@ -5,6 +5,7 @@ from prometheus_client import CollectorRegistry, Gauge
|
|||||||
|
|
||||||
from app.classes.models.management import HelpersManagement, HelpersWebhooks
|
from app.classes.models.management import HelpersManagement, HelpersWebhooks
|
||||||
from app.classes.models.servers import HelperServers
|
from app.classes.models.servers import HelperServers
|
||||||
|
from app.classes.shared.helpers import Helpers
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -75,7 +76,7 @@ class ManagementController:
|
|||||||
# Commands Methods
|
# 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)
|
server_name = HelperServers.get_server_friendly_name(server_id)
|
||||||
|
|
||||||
# Example: Admin issued command start_server for server Survival
|
# Example: Admin issued command start_server for server Survival
|
||||||
@ -86,7 +87,12 @@ class ManagementController:
|
|||||||
remote_ip,
|
remote_ip,
|
||||||
)
|
)
|
||||||
self.queue_command(
|
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):
|
def queue_command(self, command_data):
|
||||||
@ -123,6 +129,7 @@ class ManagementController:
|
|||||||
cron_string="* * * * *",
|
cron_string="* * * * *",
|
||||||
parent=None,
|
parent=None,
|
||||||
delay=0,
|
delay=0,
|
||||||
|
action_id=None,
|
||||||
):
|
):
|
||||||
return HelpersManagement.create_scheduled_task(
|
return HelpersManagement.create_scheduled_task(
|
||||||
server_id,
|
server_id,
|
||||||
@ -137,6 +144,7 @@ class ManagementController:
|
|||||||
cron_string,
|
cron_string,
|
||||||
parent,
|
parent,
|
||||||
delay,
|
delay,
|
||||||
|
action_id,
|
||||||
)
|
)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
@ -175,34 +183,47 @@ class ManagementController:
|
|||||||
# Backups Methods
|
# Backups Methods
|
||||||
# **********************************************************************************
|
# **********************************************************************************
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_backup_config(server_id):
|
def get_backup_config(backup_id):
|
||||||
return HelpersManagement.get_backup_config(server_id)
|
return HelpersManagement.get_backup_config(backup_id)
|
||||||
|
|
||||||
def set_backup_config(
|
@staticmethod
|
||||||
self,
|
def get_backups_by_server(server_id, model=False):
|
||||||
server_id: int,
|
return HelpersManagement.get_backups_by_server(server_id, model)
|
||||||
backup_path: str = None,
|
|
||||||
max_backups: int = None,
|
@staticmethod
|
||||||
excluded_dirs: list = None,
|
def delete_backup_config(backup_id):
|
||||||
compress: bool = False,
|
HelpersManagement.remove_backup_config(backup_id)
|
||||||
shutdown: bool = False,
|
|
||||||
before: str = "",
|
@staticmethod
|
||||||
after: str = "",
|
def update_backup_config(backup_id, updates):
|
||||||
):
|
if "backup_location" in updates:
|
||||||
return self.management_helper.set_backup_config(
|
updates["backup_location"] = Helpers.wtol_path(updates["backup_location"])
|
||||||
server_id,
|
return HelpersManagement.update_backup_config(backup_id, updates)
|
||||||
backup_path,
|
|
||||||
max_backups,
|
def add_backup_config(self, data) -> str:
|
||||||
excluded_dirs,
|
if "backup_location" in data:
|
||||||
compress,
|
data["backup_location"] = Helpers.wtol_path(data["backup_location"])
|
||||||
shutdown,
|
return self.management_helper.add_backup_config(data)
|
||||||
before,
|
|
||||||
after,
|
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
|
@staticmethod
|
||||||
def get_excluded_backup_dirs(server_id: int):
|
def get_excluded_backup_dirs(backup_id: int):
|
||||||
return HelpersManagement.get_excluded_backup_dirs(server_id)
|
return HelpersManagement.get_excluded_backup_dirs(backup_id)
|
||||||
|
|
||||||
def add_excluded_backup_dir(self, server_id: int, dir_to_add: str):
|
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)
|
self.management_helper.add_excluded_backup_dir(server_id, dir_to_add)
|
||||||
|
@ -48,7 +48,6 @@ class ServersController(metaclass=Singleton):
|
|||||||
name: str,
|
name: str,
|
||||||
server_uuid: str,
|
server_uuid: str,
|
||||||
server_dir: str,
|
server_dir: str,
|
||||||
backup_path: str,
|
|
||||||
server_command: str,
|
server_command: str,
|
||||||
server_file: str,
|
server_file: str,
|
||||||
server_log_file: str,
|
server_log_file: str,
|
||||||
@ -83,7 +82,6 @@ class ServersController(metaclass=Singleton):
|
|||||||
server_uuid,
|
server_uuid,
|
||||||
name,
|
name,
|
||||||
server_dir,
|
server_dir,
|
||||||
backup_path,
|
|
||||||
server_command,
|
server_command,
|
||||||
server_file,
|
server_file,
|
||||||
server_log_file,
|
server_log_file,
|
||||||
@ -148,8 +146,7 @@ class ServersController(metaclass=Singleton):
|
|||||||
PermissionsServers.delete_roles_permissions(role_id, role_data["servers"])
|
PermissionsServers.delete_roles_permissions(role_id, role_data["servers"])
|
||||||
# Remove roles from server
|
# Remove roles from server
|
||||||
PermissionsServers.remove_roles_of_server(server_id)
|
PermissionsServers.remove_roles_of_server(server_id)
|
||||||
# Remove backup configs tied to server
|
self.management_helper.remove_all_server_backups(server_id)
|
||||||
self.management_helper.remove_backup_config(server_id)
|
|
||||||
# Finally remove server
|
# Finally remove server
|
||||||
self.servers_helper.remove_server(server_id)
|
self.servers_helper.remove_server(server_id)
|
||||||
|
|
||||||
|
@ -55,6 +55,7 @@ class UsersController:
|
|||||||
"minLength": self.helper.minimum_password_length,
|
"minLength": self.helper.minimum_password_length,
|
||||||
"examples": ["crafty"],
|
"examples": ["crafty"],
|
||||||
"title": "Password",
|
"title": "Password",
|
||||||
|
"error": "passLength",
|
||||||
},
|
},
|
||||||
"email": {
|
"email": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
@ -86,7 +86,7 @@ class Stats:
|
|||||||
def get_node_stats(self) -> NodeStatsReturnDict:
|
def get_node_stats(self) -> NodeStatsReturnDict:
|
||||||
try:
|
try:
|
||||||
cpu_freq = psutil.cpu_freq()
|
cpu_freq = psutil.cpu_freq()
|
||||||
except (NotImplementedError, FileNotFoundError):
|
except (NotImplementedError, AttributeError, FileNotFoundError):
|
||||||
cpu_freq = None
|
cpu_freq = None
|
||||||
if cpu_freq is None:
|
if cpu_freq is None:
|
||||||
cpu_freq = psutil._common.scpufreq(current=-1, min=-1, max=-1)
|
cpu_freq = psutil._common.scpufreq(current=-1, min=-1, max=-1)
|
||||||
|
@ -16,6 +16,7 @@ from app.classes.models.base_model import BaseModel
|
|||||||
from app.classes.models.users import HelperUsers
|
from app.classes.models.users import HelperUsers
|
||||||
from app.classes.models.servers import Servers
|
from app.classes.models.servers import Servers
|
||||||
from app.classes.models.server_permissions import PermissionsServers
|
from app.classes.models.server_permissions import PermissionsServers
|
||||||
|
from app.classes.shared.helpers import Helpers
|
||||||
from app.classes.shared.websocket_manager import WebSocketManager
|
from app.classes.shared.websocket_manager import WebSocketManager
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
@ -87,6 +88,7 @@ class Schedules(BaseModel):
|
|||||||
interval_type = CharField()
|
interval_type = CharField()
|
||||||
start_time = CharField(null=True)
|
start_time = CharField(null=True)
|
||||||
command = CharField(null=True)
|
command = CharField(null=True)
|
||||||
|
action_id = CharField(null=True)
|
||||||
name = CharField()
|
name = CharField()
|
||||||
one_time = BooleanField(default=False)
|
one_time = BooleanField(default=False)
|
||||||
cron_string = CharField(default="")
|
cron_string = CharField(default="")
|
||||||
@ -102,13 +104,19 @@ class Schedules(BaseModel):
|
|||||||
# Backups Class
|
# Backups Class
|
||||||
# **********************************************************************************
|
# **********************************************************************************
|
||||||
class Backups(BaseModel):
|
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)
|
excluded_dirs = CharField(null=True)
|
||||||
max_backups = IntegerField()
|
max_backups = IntegerField(default=0)
|
||||||
server_id = ForeignKeyField(Servers, backref="backups_server")
|
server_id = ForeignKeyField(Servers, backref="backups_server")
|
||||||
compress = BooleanField(default=False)
|
compress = BooleanField(default=False)
|
||||||
shutdown = BooleanField(default=False)
|
shutdown = BooleanField(default=False)
|
||||||
before = CharField(default="")
|
before = CharField(default="")
|
||||||
after = CharField(default="")
|
after = CharField(default="")
|
||||||
|
default = BooleanField(default=False)
|
||||||
|
status = CharField(default='{"status": "Standby", "message": ""}')
|
||||||
|
enabled = BooleanField(default=True)
|
||||||
|
|
||||||
class Meta:
|
class Meta:
|
||||||
table_name = "backups"
|
table_name = "backups"
|
||||||
@ -263,6 +271,7 @@ class HelpersManagement:
|
|||||||
cron_string="* * * * *",
|
cron_string="* * * * *",
|
||||||
parent=None,
|
parent=None,
|
||||||
delay=0,
|
delay=0,
|
||||||
|
action_id=None,
|
||||||
):
|
):
|
||||||
sch_id = Schedules.insert(
|
sch_id = Schedules.insert(
|
||||||
{
|
{
|
||||||
@ -273,6 +282,7 @@ class HelpersManagement:
|
|||||||
Schedules.interval_type: interval_type,
|
Schedules.interval_type: interval_type,
|
||||||
Schedules.start_time: start_time,
|
Schedules.start_time: start_time,
|
||||||
Schedules.command: command,
|
Schedules.command: command,
|
||||||
|
Schedules.action_id: action_id,
|
||||||
Schedules.name: name,
|
Schedules.name: name,
|
||||||
Schedules.one_time: one_time,
|
Schedules.one_time: one_time,
|
||||||
Schedules.cron_string: cron_string,
|
Schedules.cron_string: cron_string,
|
||||||
@ -335,133 +345,81 @@ class HelpersManagement:
|
|||||||
# Backups Methods
|
# Backups Methods
|
||||||
# **********************************************************************************
|
# **********************************************************************************
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_backup_config(server_id):
|
def get_backup_config(backup_id):
|
||||||
try:
|
return model_to_dict(Backups.get(Backups.backup_id == backup_id))
|
||||||
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
|
|
||||||
|
|
||||||
@staticmethod
|
@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()
|
Backups.delete().where(Backups.server_id == server_id).execute()
|
||||||
|
|
||||||
def set_backup_config(
|
@staticmethod
|
||||||
self,
|
def remove_backup_config(backup_id):
|
||||||
server_id: int,
|
Backups.delete().where(Backups.backup_id == backup_id).execute()
|
||||||
backup_path: str = None,
|
|
||||||
max_backups: int = None,
|
def add_backup_config(self, conf) -> str:
|
||||||
excluded_dirs: list = None,
|
if "excluded_dirs" in conf:
|
||||||
compress: bool = False,
|
dirs_to_exclude = ",".join(conf["excluded_dirs"])
|
||||||
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)
|
|
||||||
conf["excluded_dirs"] = dirs_to_exclude
|
conf["excluded_dirs"] = dirs_to_exclude
|
||||||
conf["compress"] = compress
|
backup = Backups.create(**conf)
|
||||||
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.")
|
logger.debug("Creating new backup record.")
|
||||||
|
return backup.backup_id
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_excluded_backup_dirs(server_id: int):
|
def update_backup_config(backup_id, data):
|
||||||
excluded_dirs = HelpersManagement.get_backup_config(server_id)["excluded_dirs"]
|
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 != "":
|
if excluded_dirs is not None and excluded_dirs != "":
|
||||||
dir_list = excluded_dirs.split(",")
|
dir_list = excluded_dirs.split(",")
|
||||||
else:
|
else:
|
||||||
dir_list = []
|
dir_list = []
|
||||||
return 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
|
# Webhooks Class
|
||||||
|
@ -26,7 +26,6 @@ class Servers(BaseModel):
|
|||||||
created = DateTimeField(default=datetime.datetime.now)
|
created = DateTimeField(default=datetime.datetime.now)
|
||||||
server_name = CharField(default="Server", index=True)
|
server_name = CharField(default="Server", index=True)
|
||||||
path = CharField(default="")
|
path = CharField(default="")
|
||||||
backup_path = CharField(default="")
|
|
||||||
executable = CharField(default="")
|
executable = CharField(default="")
|
||||||
log_path = CharField(default="")
|
log_path = CharField(default="")
|
||||||
execution_command = CharField(default="")
|
execution_command = CharField(default="")
|
||||||
@ -65,7 +64,6 @@ class HelperServers:
|
|||||||
server_id: str,
|
server_id: str,
|
||||||
name: str,
|
name: str,
|
||||||
server_dir: str,
|
server_dir: str,
|
||||||
backup_path: str,
|
|
||||||
server_command: str,
|
server_command: str,
|
||||||
server_file: str,
|
server_file: str,
|
||||||
server_log_file: str,
|
server_log_file: str,
|
||||||
@ -81,7 +79,6 @@ class HelperServers:
|
|||||||
name: The name of the server
|
name: The name of the server
|
||||||
server_uuid: This is the UUID of the server
|
server_uuid: This is the UUID of the server
|
||||||
server_dir: The directory where the server is located
|
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_command: The command to start the server
|
||||||
server_file: The name of the server file
|
server_file: The name of the server file
|
||||||
server_log_file: The path to the server log file
|
server_log_file: The path to the server log file
|
||||||
@ -111,7 +108,6 @@ class HelperServers:
|
|||||||
server_port=server_port,
|
server_port=server_port,
|
||||||
server_ip=server_host,
|
server_ip=server_host,
|
||||||
stop_command=server_stop,
|
stop_command=server_stop,
|
||||||
backup_path=backup_path,
|
|
||||||
type=server_type,
|
type=server_type,
|
||||||
created_by=created_by,
|
created_by=created_by,
|
||||||
).server_id
|
).server_id
|
||||||
|
@ -38,7 +38,7 @@ class Users(BaseModel):
|
|||||||
superuser = BooleanField(default=False)
|
superuser = BooleanField(default=False)
|
||||||
lang = CharField(default="en_EN")
|
lang = CharField(default="en_EN")
|
||||||
support_logs = CharField(default="")
|
support_logs = CharField(default="")
|
||||||
valid_tokens_from = DateTimeField(default=datetime.datetime.now)
|
valid_tokens_from = DateTimeField(default=Helpers.get_utc_now)
|
||||||
server_order = CharField(default="")
|
server_order = CharField(default="")
|
||||||
preparing = BooleanField(default=False)
|
preparing = BooleanField(default=False)
|
||||||
hints = BooleanField(default=True)
|
hints = BooleanField(default=True)
|
||||||
@ -119,7 +119,6 @@ class HelperUsers:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_user_total():
|
def get_user_total():
|
||||||
count = Users.select().where(Users.username != "system").count()
|
count = Users.select().where(Users.username != "system").count()
|
||||||
print(count)
|
|
||||||
return count
|
return count
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import time
|
import time
|
||||||
|
from datetime import datetime
|
||||||
from typing import Optional, Dict, Any, Tuple
|
from typing import Optional, Dict, Any, Tuple
|
||||||
import jwt
|
import jwt
|
||||||
from jwt import PyJWTError
|
from jwt import PyJWTError
|
||||||
@ -62,7 +63,17 @@ class Authentication:
|
|||||||
user = HelperUsers.get_user(user_id)
|
user = HelperUsers.get_user(user_id)
|
||||||
# TODO: Have a cache or something so we don't constantly
|
# TODO: Have a cache or something so we don't constantly
|
||||||
# have to query the database
|
# have to query the database
|
||||||
if int(user.get("valid_tokens_from").timestamp()) < iat:
|
valid_tokens_from_str = user.get("valid_tokens_from")
|
||||||
|
# It's possible this will be a string or a dt coming from the DB
|
||||||
|
# We need to account for that
|
||||||
|
try:
|
||||||
|
valid_tokens_from_dt = datetime.strptime(
|
||||||
|
valid_tokens_from_str, "%Y-%m-%d %H:%M:%S.%f%z"
|
||||||
|
)
|
||||||
|
except TypeError:
|
||||||
|
valid_tokens_from_dt = valid_tokens_from_str
|
||||||
|
# Convert the string to a datetime object
|
||||||
|
if int(valid_tokens_from_dt.timestamp()) < iat:
|
||||||
# Success!
|
# Success!
|
||||||
return key, data, user
|
return key, data, user
|
||||||
return None
|
return None
|
||||||
|
@ -4,7 +4,10 @@ import logging
|
|||||||
import pathlib
|
import pathlib
|
||||||
import tempfile
|
import tempfile
|
||||||
import zipfile
|
import zipfile
|
||||||
from zipfile import ZipFile, ZIP_DEFLATED
|
import hashlib
|
||||||
|
from typing import BinaryIO
|
||||||
|
import mimetypes
|
||||||
|
from zipfile import ZipFile, ZIP_DEFLATED, ZIP_STORED
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import ssl
|
import ssl
|
||||||
import time
|
import time
|
||||||
@ -22,6 +25,7 @@ class FileHelpers:
|
|||||||
|
|
||||||
def __init__(self, helper):
|
def __init__(self, helper):
|
||||||
self.helper: Helpers = helper
|
self.helper: Helpers = helper
|
||||||
|
self.mime_types = mimetypes.MimeTypes()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def ssl_get_file(
|
def ssl_get_file(
|
||||||
@ -142,6 +146,32 @@ class FileHelpers:
|
|||||||
logger.error(f"Path specified is not a file or does not exist. {path}")
|
logger.error(f"Path specified is not a file or does not exist. {path}")
|
||||||
return e
|
return e
|
||||||
|
|
||||||
|
def check_mime_types(self, file_path):
|
||||||
|
m_type, _value = self.mime_types.guess_type(file_path)
|
||||||
|
return m_type
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def calculate_file_hash(file_path: str) -> str:
|
||||||
|
"""
|
||||||
|
Takes one parameter of file path.
|
||||||
|
It will generate a SHA256 hash for the path and return it.
|
||||||
|
"""
|
||||||
|
sha256_hash = hashlib.sha256()
|
||||||
|
with open(file_path, "rb") as f:
|
||||||
|
for byte_block in iter(lambda: f.read(4096), b""):
|
||||||
|
sha256_hash.update(byte_block)
|
||||||
|
return sha256_hash.hexdigest()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def calculate_buffer_hash(buffer: BinaryIO) -> str:
|
||||||
|
"""
|
||||||
|
Takes one argument of a stream buffer. Will return a
|
||||||
|
sha256 hash of the buffer
|
||||||
|
"""
|
||||||
|
sha256_hash = hashlib.sha256()
|
||||||
|
sha256_hash.update(buffer)
|
||||||
|
return sha256_hash.hexdigest()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def copy_dir(src_path, dest_path, dirs_exist_ok=False):
|
def copy_dir(src_path, dest_path, dirs_exist_ok=False):
|
||||||
# pylint: disable=unexpected-keyword-arg
|
# pylint: disable=unexpected-keyword-arg
|
||||||
@ -229,74 +259,15 @@ class FileHelpers:
|
|||||||
|
|
||||||
return True
|
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(
|
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
|
# create a ZipFile object
|
||||||
path_to_destination += ".zip"
|
path_to_destination += ".zip"
|
||||||
@ -313,7 +284,15 @@ class FileHelpers:
|
|||||||
"backup_status",
|
"backup_status",
|
||||||
results,
|
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(
|
zip_file.comment = bytes(
|
||||||
comment, "utf-8"
|
comment, "utf-8"
|
||||||
) # comments over 65535 bytes will be truncated
|
) # comments over 65535 bytes will be truncated
|
||||||
@ -364,6 +343,7 @@ class FileHelpers:
|
|||||||
results = {
|
results = {
|
||||||
"percent": percent,
|
"percent": percent,
|
||||||
"total_files": self.helper.human_readable_file_size(dir_bytes),
|
"total_files": self.helper.human_readable_file_size(dir_bytes),
|
||||||
|
"backup_id": backup_id,
|
||||||
}
|
}
|
||||||
# send status results to page.
|
# send status results to page.
|
||||||
WebSocketManager().broadcast_page_params(
|
WebSocketManager().broadcast_page_params(
|
||||||
@ -372,6 +352,12 @@ class FileHelpers:
|
|||||||
"backup_status",
|
"backup_status",
|
||||||
results,
|
results,
|
||||||
)
|
)
|
||||||
|
WebSocketManager().broadcast_page_params(
|
||||||
|
"/panel/edit_backup",
|
||||||
|
{"id": str(server_id)},
|
||||||
|
"backup_status",
|
||||||
|
results,
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
@ -19,7 +19,7 @@ import shutil
|
|||||||
import shlex
|
import shlex
|
||||||
import subprocess
|
import subprocess
|
||||||
import itertools
|
import itertools
|
||||||
from datetime import datetime
|
from datetime import datetime, timezone
|
||||||
from socket import gethostname
|
from socket import gethostname
|
||||||
from contextlib import redirect_stderr, suppress
|
from contextlib import redirect_stderr, suppress
|
||||||
import libgravatar
|
import libgravatar
|
||||||
@ -508,7 +508,6 @@ class Helpers:
|
|||||||
"max_log_lines": 700,
|
"max_log_lines": 700,
|
||||||
"max_audit_entries": 300,
|
"max_audit_entries": 300,
|
||||||
"disabled_language_files": [],
|
"disabled_language_files": [],
|
||||||
"stream_size_GB": 1,
|
|
||||||
"keywords": ["help", "chunk"],
|
"keywords": ["help", "chunk"],
|
||||||
"allow_nsfw_profile_pictures": False,
|
"allow_nsfw_profile_pictures": False,
|
||||||
"enable_user_self_delete": False,
|
"enable_user_self_delete": False,
|
||||||
@ -640,6 +639,10 @@ class Helpers:
|
|||||||
version = f"{major}.{minor}.{sub}"
|
version = f"{major}.{minor}.{sub}"
|
||||||
return str(version)
|
return str(version)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_utc_now() -> datetime:
|
||||||
|
return datetime.fromtimestamp(time.time(), tz=timezone.utc)
|
||||||
|
|
||||||
def encode_pass(self, password):
|
def encode_pass(self, password):
|
||||||
return self.passhasher.hash(password)
|
return self.passhasher.hash(password)
|
||||||
|
|
||||||
@ -1006,6 +1009,11 @@ class Helpers:
|
|||||||
except PermissionError as e:
|
except PermissionError as e:
|
||||||
logger.critical(f"Check generated exception due to permssion error: {e}")
|
logger.critical(f"Check generated exception due to permssion error: {e}")
|
||||||
return False
|
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):
|
def create_self_signed_cert(self, cert_dir=None):
|
||||||
if cert_dir is None:
|
if cert_dir is None:
|
||||||
|
@ -566,7 +566,6 @@ class Controller:
|
|||||||
name=data["name"],
|
name=data["name"],
|
||||||
server_uuid=server_fs_uuid,
|
server_uuid=server_fs_uuid,
|
||||||
server_dir=new_server_path,
|
server_dir=new_server_path,
|
||||||
backup_path=backup_path,
|
|
||||||
server_command=server_command,
|
server_command=server_command,
|
||||||
server_file=server_file,
|
server_file=server_file,
|
||||||
server_log_file=log_location,
|
server_log_file=log_location,
|
||||||
@ -576,7 +575,7 @@ class Controller:
|
|||||||
server_host=monitoring_host,
|
server_host=monitoring_host,
|
||||||
server_type=monitoring_type,
|
server_type=monitoring_type,
|
||||||
)
|
)
|
||||||
self.management.set_backup_config(
|
self.management.add_default_backup_config(
|
||||||
new_server_id,
|
new_server_id,
|
||||||
backup_path,
|
backup_path,
|
||||||
)
|
)
|
||||||
@ -722,7 +721,6 @@ class Controller:
|
|||||||
server_name,
|
server_name,
|
||||||
server_id,
|
server_id,
|
||||||
new_server_dir,
|
new_server_dir,
|
||||||
backup_path,
|
|
||||||
server_command,
|
server_command,
|
||||||
server_jar,
|
server_jar,
|
||||||
server_log_file,
|
server_log_file,
|
||||||
@ -776,7 +774,6 @@ class Controller:
|
|||||||
server_name,
|
server_name,
|
||||||
server_id,
|
server_id,
|
||||||
new_server_dir,
|
new_server_dir,
|
||||||
backup_path,
|
|
||||||
server_command,
|
server_command,
|
||||||
server_exe,
|
server_exe,
|
||||||
server_log_file,
|
server_log_file,
|
||||||
@ -821,7 +818,6 @@ class Controller:
|
|||||||
server_name,
|
server_name,
|
||||||
server_id,
|
server_id,
|
||||||
new_server_dir,
|
new_server_dir,
|
||||||
backup_path,
|
|
||||||
server_command,
|
server_command,
|
||||||
server_exe,
|
server_exe,
|
||||||
server_log_file,
|
server_log_file,
|
||||||
@ -869,7 +865,6 @@ class Controller:
|
|||||||
server_name,
|
server_name,
|
||||||
server_id,
|
server_id,
|
||||||
new_server_dir,
|
new_server_dir,
|
||||||
backup_path,
|
|
||||||
server_command,
|
server_command,
|
||||||
server_exe,
|
server_exe,
|
||||||
server_log_file,
|
server_log_file,
|
||||||
@ -893,16 +888,13 @@ class Controller:
|
|||||||
# **********************************************************************************
|
# **********************************************************************************
|
||||||
|
|
||||||
def rename_backup_dir(self, old_server_id, new_server_id, new_uuid):
|
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)
|
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)
|
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 = Path(backup_path)
|
||||||
backup_path_components = list(backup_path.parts)
|
backup_path_components = list(backup_path.parts)
|
||||||
backup_path_components[-1] = new_uuid
|
backup_path_components[-1] = new_uuid
|
||||||
new_bu_path = pathlib.PurePath(os.path.join(*backup_path_components))
|
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)
|
default_backup_dir = os.path.join(self.helper.backup_path, new_uuid)
|
||||||
try:
|
try:
|
||||||
os.rmdir(default_backup_dir)
|
os.rmdir(default_backup_dir)
|
||||||
@ -916,7 +908,6 @@ class Controller:
|
|||||||
name: str,
|
name: str,
|
||||||
server_uuid: str,
|
server_uuid: str,
|
||||||
server_dir: str,
|
server_dir: str,
|
||||||
backup_path: str,
|
|
||||||
server_command: str,
|
server_command: str,
|
||||||
server_file: str,
|
server_file: str,
|
||||||
server_log_file: str,
|
server_log_file: str,
|
||||||
@ -931,7 +922,6 @@ class Controller:
|
|||||||
name,
|
name,
|
||||||
server_uuid,
|
server_uuid,
|
||||||
server_dir,
|
server_dir,
|
||||||
backup_path,
|
|
||||||
server_command,
|
server_command,
|
||||||
server_file,
|
server_file,
|
||||||
server_log_file,
|
server_log_file,
|
||||||
@ -996,14 +986,14 @@ class Controller:
|
|||||||
f"Unable to delete server files for server with ID: "
|
f"Unable to delete server files for server with ID: "
|
||||||
f"{server_id} with error logged: {e}"
|
f"{server_id} with error logged: {e}"
|
||||||
)
|
)
|
||||||
if Helpers.check_path_exists(
|
backup_configs = HelpersManagement.get_backups_by_server(
|
||||||
self.servers.get_server_data_by_id(server_id)["backup_path"]
|
server_id, True
|
||||||
):
|
)
|
||||||
|
for config in backup_configs:
|
||||||
|
if Helpers.check_path_exists(config.backup_location):
|
||||||
FileHelpers.del_dirs(
|
FileHelpers.del_dirs(
|
||||||
Helpers.get_os_understandable_path(
|
Helpers.get_os_understandable_path(
|
||||||
self.servers.get_server_data_by_id(server_id)[
|
config.backup_location
|
||||||
"backup_path"
|
|
||||||
]
|
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -207,9 +207,6 @@ class ServerInstance:
|
|||||||
self.server_scheduler.start()
|
self.server_scheduler.start()
|
||||||
self.dir_scheduler.start()
|
self.dir_scheduler.start()
|
||||||
self.start_dir_calc_task()
|
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
|
self.is_backingup = False
|
||||||
# Reset crash and update at initialization
|
# Reset crash and update at initialization
|
||||||
self.stats_helper.server_crash_reset()
|
self.stats_helper.server_crash_reset()
|
||||||
@ -940,8 +937,7 @@ class ServerInstance:
|
|||||||
WebSocketManager().broadcast_user(user, "send_start_reload", {})
|
WebSocketManager().broadcast_user(user, "send_start_reload", {})
|
||||||
|
|
||||||
def restart_threaded_server(self, user_id):
|
def restart_threaded_server(self, user_id):
|
||||||
bu_conf = HelpersManagement.get_backup_config(self.server_id)
|
if self.is_backingup:
|
||||||
if self.is_backingup and bu_conf["shutdown"]:
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Restart command detected. Supressing - server has"
|
"Restart command detected. Supressing - server has"
|
||||||
" backup shutdown enabled and server is currently backing up."
|
" backup shutdown enabled and server is currently backing up."
|
||||||
@ -1111,12 +1107,16 @@ class ServerInstance:
|
|||||||
f.write("eula=true")
|
f.write("eula=true")
|
||||||
self.run_threaded_server(user_id)
|
self.run_threaded_server(user_id)
|
||||||
|
|
||||||
def a_backup_server(self):
|
def server_backup_threader(self, backup_id, update=False):
|
||||||
if self.settings["backup_path"] == "":
|
# Check to see if we're already backing up
|
||||||
logger.critical("Backup path is None. Canceling Backup!")
|
if self.check_backup_by_id(backup_id):
|
||||||
return
|
return False
|
||||||
|
|
||||||
backup_thread = threading.Thread(
|
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(
|
logger.info(
|
||||||
f"Starting Backup Thread for server {self.settings['server_name']}."
|
f"Starting Backup Thread for server {self.settings['server_name']}."
|
||||||
@ -1127,27 +1127,20 @@ class ServerInstance:
|
|||||||
"Backup Thread - Local server path not defined. "
|
"Backup Thread - Local server path not defined. "
|
||||||
"Setting local server path variable."
|
"Setting local server path variable."
|
||||||
)
|
)
|
||||||
# checks if the backup thread is currently alive for this server
|
|
||||||
if not self.is_backingup:
|
|
||||||
try:
|
try:
|
||||||
backup_thread.start()
|
backup_thread.start()
|
||||||
self.is_backingup = True
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.error(f"Failed to start backup: {ex}")
|
logger.error(f"Failed to start backup: {ex}")
|
||||||
return False
|
return False
|
||||||
else:
|
|
||||||
logger.error(
|
|
||||||
f"Backup is already being processed for server "
|
|
||||||
f"{self.settings['server_name']}. Canceling backup request"
|
|
||||||
)
|
|
||||||
return False
|
|
||||||
logger.info(f"Backup Thread started for server {self.settings['server_name']}.")
|
logger.info(f"Backup Thread started for server {self.settings['server_name']}.")
|
||||||
|
|
||||||
@callback
|
@callback
|
||||||
def backup_server(self):
|
def backup_server(self, backup_id, update):
|
||||||
was_server_running = None
|
was_server_running = None
|
||||||
logger.info(f"Starting server {self.name} (ID {self.server_id}) backup")
|
logger.info(f"Starting server {self.name} (ID {self.server_id}) backup")
|
||||||
server_users = PermissionsServers.get_server_user_list(self.server_id)
|
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:
|
for user in server_users:
|
||||||
WebSocketManager().broadcast_user(
|
WebSocketManager().broadcast_user(
|
||||||
user,
|
user,
|
||||||
@ -1157,30 +1150,40 @@ class ServerInstance:
|
|||||||
).format(self.name),
|
).format(self.name),
|
||||||
)
|
)
|
||||||
time.sleep(3)
|
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 conf["before"]:
|
||||||
if self.check_running():
|
|
||||||
logger.debug(
|
logger.debug(
|
||||||
"Found running server and send command option. Sending command"
|
"Found running server and send command option. Sending command"
|
||||||
)
|
)
|
||||||
self.send_command(conf["before"])
|
self.send_command(conf["before"])
|
||||||
|
# Pause to let command run
|
||||||
|
time.sleep(5)
|
||||||
|
|
||||||
if conf["shutdown"]:
|
if conf["shutdown"]:
|
||||||
if conf["before"]:
|
|
||||||
# pause to let people read message.
|
|
||||||
time.sleep(5)
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Found shutdown preference. Delaying"
|
"Found shutdown preference. Delaying"
|
||||||
+ "backup start. Shutting down server."
|
+ "backup start. Shutting down server."
|
||||||
)
|
)
|
||||||
|
if not update:
|
||||||
|
was_server_running = False
|
||||||
if self.check_running():
|
if self.check_running():
|
||||||
self.stop_server()
|
self.stop_server()
|
||||||
was_server_running = True
|
was_server_running = True
|
||||||
|
|
||||||
self.helper.ensure_dir_exists(self.settings["backup_path"])
|
self.helper.ensure_dir_exists(backup_location)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
backup_filename = (
|
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
|
f"{datetime.datetime.now().astimezone(self.tz).strftime('%Y-%m-%d_%H-%M-%S')}" # pylint: disable=line-too-long
|
||||||
)
|
)
|
||||||
logger.info(
|
logger.info(
|
||||||
@ -1188,42 +1191,36 @@ class ServerInstance:
|
|||||||
f" (ID#{self.server_id}, path={self.server_path}) "
|
f" (ID#{self.server_id}, path={self.server_path}) "
|
||||||
f"at '{backup_filename}'"
|
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"])
|
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(
|
self.file_helper.make_backup(
|
||||||
Helpers.get_os_understandable_path(backup_filename),
|
Helpers.get_os_understandable_path(backup_filename),
|
||||||
server_dir,
|
server_dir,
|
||||||
excluded_dirs,
|
excluded_dirs,
|
||||||
self.server_id,
|
self.server_id,
|
||||||
|
backup_id,
|
||||||
|
conf["backup_name"],
|
||||||
|
conf["compress"],
|
||||||
)
|
)
|
||||||
|
|
||||||
while (
|
while (
|
||||||
len(self.list_backups()) > conf["max_backups"]
|
len(self.list_backups(conf)) > conf["max_backups"]
|
||||||
and conf["max_backups"] > 0
|
and conf["max_backups"] > 0
|
||||||
):
|
):
|
||||||
backup_list = self.list_backups()
|
backup_list = self.list_backups(conf)
|
||||||
oldfile = backup_list[0]
|
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']}'")
|
logger.info(f"Removing old backup '{oldfile['path']}'")
|
||||||
os.remove(Helpers.get_os_understandable_path(oldfile_path))
|
os.remove(Helpers.get_os_understandable_path(oldfile_path))
|
||||||
|
|
||||||
self.is_backingup = False
|
|
||||||
logger.info(f"Backup of server: {self.name} completed")
|
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:
|
if len(WebSocketManager().clients) > 0:
|
||||||
WebSocketManager().broadcast_page_params(
|
WebSocketManager().broadcast_page_params(
|
||||||
"/panel/server_detail",
|
"/panel/server_detail",
|
||||||
@ -1248,7 +1245,6 @@ class ServerInstance:
|
|||||||
)
|
)
|
||||||
self.run_threaded_server(HelperUsers.get_user_id_by_name("system"))
|
self.run_threaded_server(HelperUsers.get_user_id_by_name("system"))
|
||||||
time.sleep(3)
|
time.sleep(3)
|
||||||
self.last_backup_failed = False
|
|
||||||
if conf["after"]:
|
if conf["after"]:
|
||||||
if self.check_running():
|
if self.check_running():
|
||||||
logger.debug(
|
logger.debug(
|
||||||
@ -1256,12 +1252,21 @@ class ServerInstance:
|
|||||||
)
|
)
|
||||||
self.send_command(conf["after"])
|
self.send_command(conf["after"])
|
||||||
# pause to let people read message.
|
# pause to let people read message.
|
||||||
|
HelpersManagement.update_backup_config(
|
||||||
|
backup_id,
|
||||||
|
{"status": json.dumps({"status": "Standby", "message": ""})},
|
||||||
|
)
|
||||||
time.sleep(5)
|
time.sleep(5)
|
||||||
except:
|
except Exception as e:
|
||||||
logger.exception(
|
logger.exception(
|
||||||
f"Failed to create backup of server {self.name} (ID {self.server_id})"
|
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:
|
if len(WebSocketManager().clients) > 0:
|
||||||
WebSocketManager().broadcast_page_params(
|
WebSocketManager().broadcast_page_params(
|
||||||
"/panel/server_detail",
|
"/panel/server_detail",
|
||||||
@ -1269,56 +1274,51 @@ class ServerInstance:
|
|||||||
"backup_status",
|
"backup_status",
|
||||||
results,
|
results,
|
||||||
)
|
)
|
||||||
self.is_backingup = False
|
|
||||||
if was_server_running:
|
if was_server_running:
|
||||||
logger.info(
|
logger.info(
|
||||||
"Backup complete. User had shutdown preference. Starting server."
|
"Backup complete. User had shutdown preference. Starting server."
|
||||||
)
|
)
|
||||||
self.run_threaded_server(HelperUsers.get_user_id_by_name("system"))
|
self.run_threaded_server(HelperUsers.get_user_id_by_name("system"))
|
||||||
self.last_backup_failed = True
|
HelpersManagement.update_backup_config(
|
||||||
|
backup_id,
|
||||||
def backup_status(self, source_path, dest_path):
|
{"status": json.dumps({"status": "Failed", "message": f"{e}"})},
|
||||||
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,
|
|
||||||
)
|
)
|
||||||
|
self.set_backup_status()
|
||||||
|
|
||||||
def last_backup_status(self):
|
def last_backup_status(self):
|
||||||
return self.last_backup_failed
|
return self.last_backup_failed
|
||||||
|
|
||||||
def send_backup_status(self):
|
def set_backup_status(self):
|
||||||
try:
|
backups = HelpersManagement.get_backups_by_server(self.server_id, True)
|
||||||
return self.backup_stats
|
alert = False
|
||||||
except:
|
for backup in backups:
|
||||||
return {"percent": 0, "total_files": 0}
|
if json.loads(backup.status)["status"] == "Failed":
|
||||||
|
alert = True
|
||||||
|
self.last_backup_failed = alert
|
||||||
|
|
||||||
def list_backups(self):
|
def list_backups(self, backup_config: dict) -> list:
|
||||||
if not self.settings["backup_path"]:
|
if not backup_config:
|
||||||
logger.info(
|
logger.info(
|
||||||
f"Error putting backup file list for server with ID: {self.server_id}"
|
f"Error putting backup file list for server with ID: {self.server_id}"
|
||||||
)
|
)
|
||||||
return []
|
return []
|
||||||
|
backup_location = os.path.join(
|
||||||
|
backup_config["backup_location"], backup_config["backup_id"]
|
||||||
|
)
|
||||||
if not Helpers.check_path_exists(
|
if not Helpers.check_path_exists(
|
||||||
Helpers.get_os_understandable_path(self.settings["backup_path"])
|
Helpers.get_os_understandable_path(backup_location)
|
||||||
):
|
):
|
||||||
return []
|
return []
|
||||||
files = Helpers.get_human_readable_files_sizes(
|
files = Helpers.get_human_readable_files_sizes(
|
||||||
Helpers.list_dir_by_date(
|
Helpers.list_dir_by_date(
|
||||||
Helpers.get_os_understandable_path(self.settings["backup_path"])
|
Helpers.get_os_understandable_path(backup_location)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"path": os.path.relpath(
|
"path": os.path.relpath(
|
||||||
f["path"],
|
f["path"],
|
||||||
start=Helpers.get_os_understandable_path(
|
start=Helpers.get_os_understandable_path(backup_location),
|
||||||
self.settings["backup_path"]
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
"size": f["size"],
|
"size": f["size"],
|
||||||
}
|
}
|
||||||
@ -1330,7 +1330,7 @@ class ServerInstance:
|
|||||||
def jar_update(self):
|
def jar_update(self):
|
||||||
self.stats_helper.set_update(True)
|
self.stats_helper.set_update(True)
|
||||||
update_thread = threading.Thread(
|
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()
|
update_thread.start()
|
||||||
|
|
||||||
@ -1371,10 +1371,13 @@ class ServerInstance:
|
|||||||
def check_update(self):
|
def check_update(self):
|
||||||
return self.stats_helper.get_server_stats()["updating"]
|
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)
|
server_users = PermissionsServers.get_server_user_list(self.server_id)
|
||||||
was_started = "-1"
|
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.
|
# checks if server is running. Calls shutdown if it is running.
|
||||||
if self.check_running():
|
if self.check_running():
|
||||||
was_started = True
|
was_started = True
|
||||||
@ -1403,47 +1406,22 @@ class ServerInstance:
|
|||||||
"string": message,
|
"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(
|
current_executable = os.path.join(
|
||||||
Helpers.get_os_understandable_path(self.settings["path"]),
|
Helpers.get_os_understandable_path(self.settings["path"]),
|
||||||
self.settings["executable"],
|
self.settings["executable"],
|
||||||
)
|
)
|
||||||
|
backing_up = True
|
||||||
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.")
|
|
||||||
|
|
||||||
# wait for backup
|
# wait for backup
|
||||||
while self.is_backingup:
|
while backing_up:
|
||||||
time.sleep(10)
|
# 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
|
# 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:
|
for user in server_users:
|
||||||
WebSocketManager().broadcast_user(
|
WebSocketManager().broadcast_user(
|
||||||
user,
|
user,
|
||||||
@ -1528,12 +1506,6 @@ class ServerInstance:
|
|||||||
WebSocketManager().broadcast_user_page(
|
WebSocketManager().broadcast_user_page(
|
||||||
user, "/panel/dashboard", "send_start_reload", {}
|
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(
|
self.management_helper.add_to_audit_log_raw(
|
||||||
"Alert",
|
"Alert",
|
||||||
"-1",
|
"-1",
|
||||||
@ -1656,6 +1628,14 @@ class ServerInstance:
|
|||||||
except:
|
except:
|
||||||
Console.critical("Can't broadcast server status to websocket")
|
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):
|
def get_servers_stats(self):
|
||||||
server_stats = {}
|
server_stats = {}
|
||||||
|
|
||||||
|
@ -140,7 +140,7 @@ class TasksManager:
|
|||||||
)
|
)
|
||||||
|
|
||||||
elif command == "backup_server":
|
elif command == "backup_server":
|
||||||
svr.a_backup_server()
|
svr.server_backup_threader(cmd["action_id"])
|
||||||
|
|
||||||
elif command == "update_executable":
|
elif command == "update_executable":
|
||||||
svr.jar_update()
|
svr.jar_update()
|
||||||
@ -240,6 +240,7 @@ class TasksManager:
|
|||||||
"system"
|
"system"
|
||||||
),
|
),
|
||||||
"command": schedule.command,
|
"command": schedule.command,
|
||||||
|
"action_id": schedule.action_id,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -268,6 +269,7 @@ class TasksManager:
|
|||||||
"system"
|
"system"
|
||||||
),
|
),
|
||||||
"command": schedule.command,
|
"command": schedule.command,
|
||||||
|
"action_id": schedule.action_id,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -284,6 +286,7 @@ class TasksManager:
|
|||||||
"system"
|
"system"
|
||||||
),
|
),
|
||||||
"command": schedule.command,
|
"command": schedule.command,
|
||||||
|
"action_id": schedule.action_id,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -303,6 +306,7 @@ class TasksManager:
|
|||||||
"system"
|
"system"
|
||||||
),
|
),
|
||||||
"command": schedule.command,
|
"command": schedule.command,
|
||||||
|
"action_id": schedule.action_id,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -337,6 +341,7 @@ class TasksManager:
|
|||||||
job_data["cron_string"],
|
job_data["cron_string"],
|
||||||
job_data["parent"],
|
job_data["parent"],
|
||||||
job_data["delay"],
|
job_data["delay"],
|
||||||
|
job_data["action_id"],
|
||||||
)
|
)
|
||||||
|
|
||||||
# Checks to make sure some doofus didn't actually make the newly
|
# Checks to make sure some doofus didn't actually make the newly
|
||||||
@ -367,6 +372,7 @@ class TasksManager:
|
|||||||
"system"
|
"system"
|
||||||
),
|
),
|
||||||
"command": job_data["command"],
|
"command": job_data["command"],
|
||||||
|
"action_id": job_data["action_id"],
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -393,6 +399,7 @@ class TasksManager:
|
|||||||
"system"
|
"system"
|
||||||
),
|
),
|
||||||
"command": job_data["command"],
|
"command": job_data["command"],
|
||||||
|
"action_id": job_data["action_id"],
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -409,6 +416,7 @@ class TasksManager:
|
|||||||
"system"
|
"system"
|
||||||
),
|
),
|
||||||
"command": job_data["command"],
|
"command": job_data["command"],
|
||||||
|
"action_id": job_data["action_id"],
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -428,6 +436,7 @@ class TasksManager:
|
|||||||
"system"
|
"system"
|
||||||
),
|
),
|
||||||
"command": job_data["command"],
|
"command": job_data["command"],
|
||||||
|
"action_id": job_data["action_id"],
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -520,6 +529,7 @@ class TasksManager:
|
|||||||
"system"
|
"system"
|
||||||
),
|
),
|
||||||
"command": job_data["command"],
|
"command": job_data["command"],
|
||||||
|
"action_id": job_data["action_id"],
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -543,6 +553,7 @@ class TasksManager:
|
|||||||
"system"
|
"system"
|
||||||
),
|
),
|
||||||
"command": job_data["command"],
|
"command": job_data["command"],
|
||||||
|
"action_id": job_data["action_id"],
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -559,6 +570,7 @@ class TasksManager:
|
|||||||
"system"
|
"system"
|
||||||
),
|
),
|
||||||
"command": job_data["command"],
|
"command": job_data["command"],
|
||||||
|
"action_id": job_data["action_id"],
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -578,6 +590,7 @@ class TasksManager:
|
|||||||
"system"
|
"system"
|
||||||
),
|
),
|
||||||
"command": job_data["command"],
|
"command": job_data["command"],
|
||||||
|
"action_id": job_data["action_id"],
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -653,6 +666,7 @@ class TasksManager:
|
|||||||
"system"
|
"system"
|
||||||
),
|
),
|
||||||
"command": schedule.command,
|
"command": schedule.command,
|
||||||
|
"action_id": schedule.action_id,
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
@ -785,6 +799,18 @@ class TasksManager:
|
|||||||
self.helper.ensure_dir_exists(
|
self.helper.ensure_dir_exists(
|
||||||
os.path.join(self.controller.project_root, "import", "upload")
|
os.path.join(self.controller.project_root, "import", "upload")
|
||||||
)
|
)
|
||||||
|
self.helper.ensure_dir_exists(
|
||||||
|
os.path.join(self.controller.project_root, "temp")
|
||||||
|
)
|
||||||
|
for file in os.listdir(os.path.join(self.controller.project_root, "temp")):
|
||||||
|
if self.helper.is_file_older_than_x_days(
|
||||||
|
os.path.join(self.controller.project_root, "temp", file)
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
os.remove(os.path.join(file))
|
||||||
|
except FileNotFoundError:
|
||||||
|
logger.debug("Could not clear out file from temp directory")
|
||||||
|
|
||||||
for file in os.listdir(
|
for file in os.listdir(
|
||||||
os.path.join(self.controller.project_root, "import", "upload")
|
os.path.join(self.controller.project_root, "import", "upload")
|
||||||
):
|
):
|
||||||
@ -793,7 +819,7 @@ class TasksManager:
|
|||||||
):
|
):
|
||||||
try:
|
try:
|
||||||
os.remove(os.path.join(file))
|
os.remove(os.path.join(file))
|
||||||
except:
|
except FileNotFoundError:
|
||||||
logger.debug("Could not clear out file from import directory")
|
logger.debug("Could not clear out file from import directory")
|
||||||
|
|
||||||
def log_watcher(self):
|
def log_watcher(self):
|
||||||
|
@ -20,7 +20,7 @@ class Translation:
|
|||||||
def get_language_file(self, language: str):
|
def get_language_file(self, language: str):
|
||||||
return os.path.join(self.translations_path, str(language) + ".json")
|
return os.path.join(self.translations_path, str(language) + ".json")
|
||||||
|
|
||||||
def translate(self, page, word, language):
|
def translate(self, page, word, language, error=True):
|
||||||
fallback_language = "en_EN"
|
fallback_language = "en_EN"
|
||||||
|
|
||||||
translated_word = self.translate_inner(page, word, language)
|
translated_word = self.translate_inner(page, word, language)
|
||||||
@ -37,7 +37,9 @@ class Translation:
|
|||||||
if hasattr(translated_word, "__iter__"):
|
if hasattr(translated_word, "__iter__"):
|
||||||
# Multiline strings
|
# Multiline strings
|
||||||
return "\n".join(translated_word)
|
return "\n".join(translated_word)
|
||||||
|
if error:
|
||||||
return "Error while getting translation"
|
return "Error while getting translation"
|
||||||
|
return word
|
||||||
|
|
||||||
def translate_inner(self, page, word, language) -> t.Union[t.Any, None]:
|
def translate_inner(self, page, word, language) -> t.Union[t.Any, None]:
|
||||||
language_file = self.get_language_file(language)
|
language_file = self.get_language_file(language)
|
||||||
|
@ -41,6 +41,8 @@ SUBPAGE_PERMS = {
|
|||||||
"webhooks": EnumPermissionsServer.CONFIG,
|
"webhooks": EnumPermissionsServer.CONFIG,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SCHEDULE_AUTH_ERROR_URL = "/panel/error?error=Unauthorized access To Schedules"
|
||||||
|
|
||||||
|
|
||||||
class PanelHandler(BaseHandler):
|
class PanelHandler(BaseHandler):
|
||||||
def get_user_roles(self) -> t.Dict[str, list]:
|
def get_user_roles(self) -> t.Dict[str, list]:
|
||||||
@ -677,36 +679,18 @@ class PanelHandler(BaseHandler):
|
|||||||
page_data["java_versions"] = page_java
|
page_data["java_versions"] = page_java
|
||||||
if subpage == "backup":
|
if subpage == "backup":
|
||||||
server_info = self.controller.servers.get_server_data_by_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(server_id)
|
page_data["backups"] = self.controller.management.get_backups_by_server(
|
||||||
)
|
server_id, model=True
|
||||||
exclusions = []
|
|
||||||
page_data["exclusions"] = (
|
|
||||||
self.controller.management.get_excluded_backup_dirs(server_id)
|
|
||||||
)
|
)
|
||||||
page_data["backing_up"] = (
|
page_data["backing_up"] = (
|
||||||
self.controller.servers.get_server_instance_by_id(
|
self.controller.servers.get_server_instance_by_id(
|
||||||
server_id
|
server_id
|
||||||
).is_backingup
|
).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
|
# 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)
|
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":
|
if subpage == "metrics":
|
||||||
try:
|
try:
|
||||||
@ -780,20 +764,23 @@ class PanelHandler(BaseHandler):
|
|||||||
|
|
||||||
elif page == "download_backup":
|
elif page == "download_backup":
|
||||||
file = self.get_argument("file", "")
|
file = self.get_argument("file", "")
|
||||||
|
backup_id = self.get_argument("backup_id", "")
|
||||||
|
|
||||||
server_id = self.check_server_id()
|
server_id = self.check_server_id()
|
||||||
if server_id is None:
|
if server_id is None:
|
||||||
return
|
return
|
||||||
|
backup_config = self.controller.management.get_backup_config(backup_id)
|
||||||
server_info = self.controller.servers.get_server_data_by_id(server_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(
|
backup_file = os.path.abspath(
|
||||||
os.path.join(
|
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(
|
if not self.helper.is_subdir(
|
||||||
backup_file,
|
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):
|
) or not os.path.isfile(backup_file):
|
||||||
self.redirect("/panel/error?error=Invalid path detected")
|
self.redirect("/panel/error?error=Invalid path detected")
|
||||||
return
|
return
|
||||||
@ -892,6 +879,8 @@ class PanelHandler(BaseHandler):
|
|||||||
os.path.join(self.helper.root_dir, "app", "translations")
|
os.path.join(self.helper.root_dir, "app", "translations")
|
||||||
)
|
)
|
||||||
):
|
):
|
||||||
|
if file == "humanized_index.json":
|
||||||
|
continue
|
||||||
if file.endswith(".json"):
|
if file.endswith(".json"):
|
||||||
if file.split(".")[0] not in self.helper.get_setting(
|
if file.split(".")[0] not in self.helper.get_setting(
|
||||||
"disabled_language_files"
|
"disabled_language_files"
|
||||||
@ -1130,6 +1119,9 @@ class PanelHandler(BaseHandler):
|
|||||||
page_data["server_data"] = self.controller.servers.get_server_data_by_id(
|
page_data["server_data"] = self.controller.servers.get_server_data_by_id(
|
||||||
server_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(
|
page_data["server_stats"] = self.controller.servers.get_server_stats_by_id(
|
||||||
server_id
|
server_id
|
||||||
)
|
)
|
||||||
@ -1150,6 +1142,7 @@ class PanelHandler(BaseHandler):
|
|||||||
page_data["schedule"]["delay"] = 0
|
page_data["schedule"]["delay"] = 0
|
||||||
page_data["schedule"]["time"] = ""
|
page_data["schedule"]["time"] = ""
|
||||||
page_data["schedule"]["interval"] = 1
|
page_data["schedule"]["interval"] = 1
|
||||||
|
page_data["schedule"]["action_id"] = ""
|
||||||
# we don't need to check difficulty here.
|
# we don't need to check difficulty here.
|
||||||
# We'll just default to basic for new schedules
|
# We'll just default to basic for new schedules
|
||||||
page_data["schedule"]["difficulty"] = "basic"
|
page_data["schedule"]["difficulty"] = "basic"
|
||||||
@ -1158,7 +1151,7 @@ class PanelHandler(BaseHandler):
|
|||||||
|
|
||||||
if not EnumPermissionsServer.SCHEDULE in page_data["user_permissions"]:
|
if not EnumPermissionsServer.SCHEDULE in page_data["user_permissions"]:
|
||||||
if not superuser:
|
if not superuser:
|
||||||
self.redirect("/panel/error?error=Unauthorized access To Schedules")
|
self.redirect(SCHEDULE_AUTH_ERROR_URL)
|
||||||
return
|
return
|
||||||
|
|
||||||
template = "panel/server_schedule_edit.html"
|
template = "panel/server_schedule_edit.html"
|
||||||
@ -1195,6 +1188,9 @@ class PanelHandler(BaseHandler):
|
|||||||
exec_user["user_id"], server_id
|
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(
|
page_data["server_data"] = self.controller.servers.get_server_data_by_id(
|
||||||
server_id
|
server_id
|
||||||
)
|
)
|
||||||
@ -1209,6 +1205,7 @@ class PanelHandler(BaseHandler):
|
|||||||
page_data["schedule"]["server_id"] = server_id
|
page_data["schedule"]["server_id"] = server_id
|
||||||
page_data["schedule"]["schedule_id"] = schedule.schedule_id
|
page_data["schedule"]["schedule_id"] = schedule.schedule_id
|
||||||
page_data["schedule"]["action"] = schedule.action
|
page_data["schedule"]["action"] = schedule.action
|
||||||
|
page_data["schedule"]["action_id"] = schedule.action_id
|
||||||
if schedule.name:
|
if schedule.name:
|
||||||
page_data["schedule"]["name"] = schedule.name
|
page_data["schedule"]["name"] = schedule.name
|
||||||
else:
|
else:
|
||||||
@ -1252,11 +1249,141 @@ class PanelHandler(BaseHandler):
|
|||||||
|
|
||||||
if not EnumPermissionsServer.SCHEDULE in page_data["user_permissions"]:
|
if not EnumPermissionsServer.SCHEDULE in page_data["user_permissions"]:
|
||||||
if not superuser:
|
if not superuser:
|
||||||
self.redirect("/panel/error?error=Unauthorized access To Schedules")
|
self.redirect(SCHEDULE_AUTH_ERROR_URL)
|
||||||
return
|
return
|
||||||
|
|
||||||
template = "panel/server_schedule_edit.html"
|
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":
|
elif page == "edit_user":
|
||||||
user_id = self.get_argument("id", None)
|
user_id = self.get_argument("id", None)
|
||||||
role_servers = self.controller.servers.get_authorized_servers(user_id)
|
role_servers = self.controller.servers.get_authorized_servers(user_id)
|
||||||
@ -1307,6 +1434,8 @@ class PanelHandler(BaseHandler):
|
|||||||
for file in sorted(
|
for file in sorted(
|
||||||
os.listdir(os.path.join(self.helper.root_dir, "app", "translations"))
|
os.listdir(os.path.join(self.helper.root_dir, "app", "translations"))
|
||||||
):
|
):
|
||||||
|
if file == "humanized_index.json":
|
||||||
|
continue
|
||||||
if file.endswith(".json"):
|
if file.endswith(".json"):
|
||||||
if file.split(".")[0] not in self.helper.get_setting(
|
if file.split(".")[0] not in self.helper.get_setting(
|
||||||
"disabled_language_files"
|
"disabled_language_files"
|
||||||
|
@ -38,12 +38,14 @@ from app.classes.web.routes.api.servers.server.backups.index import (
|
|||||||
)
|
)
|
||||||
from app.classes.web.routes.api.servers.server.backups.backup.index import (
|
from app.classes.web.routes.api.servers.server.backups.backup.index import (
|
||||||
ApiServersServerBackupsBackupIndexHandler,
|
ApiServersServerBackupsBackupIndexHandler,
|
||||||
|
ApiServersServerBackupsBackupFilesIndexHandler,
|
||||||
)
|
)
|
||||||
from app.classes.web.routes.api.servers.server.files import (
|
from app.classes.web.routes.api.servers.server.files import (
|
||||||
ApiServersServerFilesIndexHandler,
|
ApiServersServerFilesIndexHandler,
|
||||||
ApiServersServerFilesCreateHandler,
|
ApiServersServerFilesCreateHandler,
|
||||||
ApiServersServerFilesZipHandler,
|
ApiServersServerFilesZipHandler,
|
||||||
)
|
)
|
||||||
|
from app.classes.web.routes.api.crafty.upload.index import ApiFilesUploadHandler
|
||||||
from app.classes.web.routes.api.servers.server.tasks.task.children import (
|
from app.classes.web.routes.api.servers.server.tasks.task.children import (
|
||||||
ApiServersServerTasksTaskChildrenHandler,
|
ApiServersServerTasksTaskChildrenHandler,
|
||||||
)
|
)
|
||||||
@ -218,13 +220,13 @@ def api_handlers(handler_args):
|
|||||||
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,
|
ApiServersServerBackupsBackupIndexHandler,
|
||||||
handler_args,
|
handler_args,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
r"/api/v2/servers/([a-z0-9-]+)/files/?",
|
r"/api/v2/servers/([a-z0-9-]+)/backups/backup/([a-z0-9-]+)/files/?",
|
||||||
ApiServersServerFilesIndexHandler,
|
ApiServersServerBackupsBackupFilesIndexHandler,
|
||||||
handler_args,
|
handler_args,
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
@ -237,6 +239,26 @@ def api_handlers(handler_args):
|
|||||||
ApiServersServerFilesZipHandler,
|
ApiServersServerFilesZipHandler,
|
||||||
handler_args,
|
handler_args,
|
||||||
),
|
),
|
||||||
|
(
|
||||||
|
r"/api/v2/crafty/admin/upload/?",
|
||||||
|
ApiFilesUploadHandler,
|
||||||
|
handler_args,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r"/api/v2/servers/import/upload/?",
|
||||||
|
ApiFilesUploadHandler,
|
||||||
|
handler_args,
|
||||||
|
),
|
||||||
|
(
|
||||||
|
r"/api/v2/servers/([a-z0-9-]+)/files/upload/?",
|
||||||
|
ApiFilesUploadHandler,
|
||||||
|
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/?",
|
r"/api/v2/servers/([a-z0-9-]+)/tasks/?",
|
||||||
ApiServersServerTasksIndexHandler,
|
ApiServersServerTasksIndexHandler,
|
||||||
@ -273,7 +295,8 @@ def api_handlers(handler_args):
|
|||||||
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,
|
ApiServersServerActionHandler,
|
||||||
handler_args,
|
handler_args,
|
||||||
),
|
),
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import datetime
|
|
||||||
import logging
|
import logging
|
||||||
from app.classes.web.base_api_handler import BaseApiHandler
|
from app.classes.web.base_api_handler import BaseApiHandler
|
||||||
|
from app.classes.shared.helpers import Helpers
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -13,7 +13,7 @@ class ApiAuthInvalidateTokensHandler(BaseApiHandler):
|
|||||||
|
|
||||||
logger.debug(f"Invalidate tokens for user {auth_data[4]['user_id']}")
|
logger.debug(f"Invalidate tokens for user {auth_data[4]['user_id']}")
|
||||||
self.controller.users.raw_update_user(
|
self.controller.users.raw_update_user(
|
||||||
auth_data[4]["user_id"], {"valid_tokens_from": datetime.datetime.now()}
|
auth_data[4]["user_id"], {"valid_tokens_from": Helpers.get_utc_now()}
|
||||||
)
|
)
|
||||||
|
|
||||||
self.finish_json(200, {"status": "ok"})
|
self.finish_json(200, {"status": "ok"})
|
||||||
|
308
app/classes/web/routes/api/crafty/upload/index.py
Normal file
308
app/classes/web/routes/api/crafty/upload/index.py
Normal file
@ -0,0 +1,308 @@
|
|||||||
|
import os
|
||||||
|
import logging
|
||||||
|
import shutil
|
||||||
|
from app.classes.models.server_permissions import EnumPermissionsServer
|
||||||
|
from app.classes.shared.helpers import Helpers
|
||||||
|
from app.classes.web.base_api_handler import BaseApiHandler
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
IMAGE_MIME_TYPES = [
|
||||||
|
"image/bmp",
|
||||||
|
"image/cis-cod",
|
||||||
|
"image/gif",
|
||||||
|
"image/ief",
|
||||||
|
"image/jpeg",
|
||||||
|
"image/pipeg",
|
||||||
|
"image/svg+xml",
|
||||||
|
"image/tiff",
|
||||||
|
"image/x-cmu-raster",
|
||||||
|
"image/x-cmx",
|
||||||
|
"image/x-icon",
|
||||||
|
"image/x-portable-anymap",
|
||||||
|
"image/x-portable-bitmap",
|
||||||
|
"image/x-portable-graymap",
|
||||||
|
"image/x-portable-pixmap",
|
||||||
|
"image/x-rgb",
|
||||||
|
"image/x-xbitmap",
|
||||||
|
"image/x-xpixmap",
|
||||||
|
"image/x-xwindowdump",
|
||||||
|
"image/png",
|
||||||
|
"image/webp",
|
||||||
|
]
|
||||||
|
|
||||||
|
ARCHIVE_MIME_TYPES = ["application/zip"]
|
||||||
|
|
||||||
|
|
||||||
|
class ApiFilesUploadHandler(BaseApiHandler):
|
||||||
|
async def post(self, server_id=None):
|
||||||
|
auth_data = self.authenticate_user()
|
||||||
|
if not auth_data:
|
||||||
|
return
|
||||||
|
|
||||||
|
upload_type = self.request.headers.get("type")
|
||||||
|
accepted_types = []
|
||||||
|
|
||||||
|
if server_id:
|
||||||
|
# Check to make sure user is authorized for the server
|
||||||
|
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"}
|
||||||
|
)
|
||||||
|
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],
|
||||||
|
)
|
||||||
|
# Make sure user has file access for the server
|
||||||
|
server_permissions = self.controller.server_perms.get_permissions(mask)
|
||||||
|
if EnumPermissionsServer.FILES not in server_permissions:
|
||||||
|
# if the user doesn't have Files permission, return an error
|
||||||
|
return self.finish_json(
|
||||||
|
400, {"status": "error", "error": "NOT_AUTHORIZED"}
|
||||||
|
)
|
||||||
|
|
||||||
|
u_type = "server_upload"
|
||||||
|
# Make sure user is a super user if they're changing panel settings
|
||||||
|
elif auth_data[4]["superuser"] and upload_type == "background":
|
||||||
|
u_type = "admin_config"
|
||||||
|
self.upload_dir = os.path.join(
|
||||||
|
self.controller.project_root,
|
||||||
|
"app/frontend/static/assets/images/auth/custom",
|
||||||
|
)
|
||||||
|
accepted_types = IMAGE_MIME_TYPES
|
||||||
|
elif upload_type == "import":
|
||||||
|
# Check that user can make servers
|
||||||
|
if (
|
||||||
|
not self.controller.crafty_perms.can_create_server(
|
||||||
|
auth_data[4]["user_id"]
|
||||||
|
)
|
||||||
|
and not auth_data[4]["superuser"]
|
||||||
|
):
|
||||||
|
return self.finish_json(
|
||||||
|
400,
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"error": "NOT_AUTHORIZED",
|
||||||
|
"data": {"message": ""},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# Set directory to upload import dir
|
||||||
|
self.upload_dir = os.path.join(
|
||||||
|
self.controller.project_root, "import", "upload"
|
||||||
|
)
|
||||||
|
u_type = "server_import"
|
||||||
|
accepted_types = ARCHIVE_MIME_TYPES
|
||||||
|
else:
|
||||||
|
return self.finish_json(
|
||||||
|
400,
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"error": "NOT_AUTHORIZED",
|
||||||
|
"data": {"message": ""},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# Get the headers from the request
|
||||||
|
self.chunk_hash = self.request.headers.get("chunkHash", 0)
|
||||||
|
self.file_id = self.request.headers.get("fileId")
|
||||||
|
self.chunked = self.request.headers.get("chunked", False)
|
||||||
|
self.filename = self.request.headers.get("fileName", None)
|
||||||
|
try:
|
||||||
|
file_size = int(self.request.headers.get("fileSize", None))
|
||||||
|
total_chunks = int(self.request.headers.get("totalChunks", 0))
|
||||||
|
except TypeError:
|
||||||
|
return self.finish_json(
|
||||||
|
400, {"status": "error", "error": "TYPE ERROR", "data": {}}
|
||||||
|
)
|
||||||
|
self.chunk_index = self.request.headers.get("chunkId")
|
||||||
|
if u_type == "server_upload":
|
||||||
|
self.upload_dir = self.request.headers.get("location", None)
|
||||||
|
self.temp_dir = os.path.join(self.controller.project_root, "temp", self.file_id)
|
||||||
|
|
||||||
|
if u_type == "server_upload":
|
||||||
|
# If this is an upload from a server the path will be what
|
||||||
|
# Is requested
|
||||||
|
full_path = os.path.join(self.upload_dir, self.filename)
|
||||||
|
|
||||||
|
# Check to make sure the requested path is inside the server's directory
|
||||||
|
if not self.helper.is_subdir(
|
||||||
|
full_path,
|
||||||
|
Helpers.get_os_understandable_path(
|
||||||
|
self.controller.servers.get_server_data_by_id(server_id)["path"]
|
||||||
|
),
|
||||||
|
):
|
||||||
|
return self.finish_json(
|
||||||
|
400,
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"error": "NOT AUTHORIZED",
|
||||||
|
"data": {"message": "Traversal detected"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# Check to make sure the file type we're being sent is what we're expecting
|
||||||
|
if (
|
||||||
|
self.file_helper.check_mime_types(self.filename) not in accepted_types
|
||||||
|
and u_type != "server_upload"
|
||||||
|
):
|
||||||
|
return self.finish_json(
|
||||||
|
422,
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"error": "INVALID FILE TYPE",
|
||||||
|
"data": {
|
||||||
|
"message": f"Invalid File Type only accepts {accepted_types}"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
_total, _used, free = shutil.disk_usage(self.upload_dir)
|
||||||
|
|
||||||
|
# Check to see if we have enough space
|
||||||
|
if free <= file_size:
|
||||||
|
return self.finish_json(
|
||||||
|
507,
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"error": "NO STORAGE SPACE",
|
||||||
|
"data": {"message": "Out Of Space!"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# If this has no chunk index we know it's the inital request
|
||||||
|
if self.chunked and not self.chunk_index:
|
||||||
|
return self.finish_json(
|
||||||
|
200, {"status": "ok", "data": {"file-id": self.file_id}}
|
||||||
|
)
|
||||||
|
# Create the upload and temp directories if they don't exist
|
||||||
|
os.makedirs(self.upload_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Check for chunked header. We will handle this request differently
|
||||||
|
# if it doesn't exist
|
||||||
|
if not self.chunked:
|
||||||
|
# Write the file directly to the upload dir
|
||||||
|
with open(os.path.join(self.upload_dir, self.filename), "wb") as file:
|
||||||
|
chunk = self.request.body
|
||||||
|
if chunk:
|
||||||
|
file.write(chunk)
|
||||||
|
# We'll check the file hash against the sent hash once the file is
|
||||||
|
# written. We cannot check this buffer.
|
||||||
|
calculated_hash = self.file_helper.calculate_file_hash(
|
||||||
|
os.path.join(self.upload_dir, self.filename)
|
||||||
|
)
|
||||||
|
logger.info(
|
||||||
|
f"File upload completed. Filename: {self.filename} Type: {u_type}"
|
||||||
|
)
|
||||||
|
return self.finish_json(
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
"status": "completed",
|
||||||
|
"data": {"message": "File uploaded successfully"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
# Since this is a chunked upload we'll create the temp dir for parts.
|
||||||
|
os.makedirs(self.temp_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# Read headers and query parameters
|
||||||
|
content_length = int(self.request.headers.get("Content-Length"))
|
||||||
|
if content_length <= 0:
|
||||||
|
logger.error(
|
||||||
|
f"File upload failed. Filename: {self.filename}"
|
||||||
|
f"Type: {u_type} Error: INVALID CONTENT LENGTH"
|
||||||
|
)
|
||||||
|
return self.finish_json(
|
||||||
|
400,
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"error": "INVALID CONTENT LENGTH",
|
||||||
|
"data": {"message": "Invalid content length"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# At this point filename, chunk index and total chunks are required
|
||||||
|
# in the request
|
||||||
|
if not self.filename or self.chunk_index is None:
|
||||||
|
logger.error(
|
||||||
|
f"File upload failed. Filename: {self.filename}"
|
||||||
|
f"Type: {u_type} Error: CHUNK INDEX NOT FOUND"
|
||||||
|
)
|
||||||
|
return self.finish_json(
|
||||||
|
400,
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"error": "INDEX ERROR",
|
||||||
|
"data": {
|
||||||
|
"message": "Filename, chunk_index,"
|
||||||
|
" and total_chunks are required"
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# Calculate the hash of the buffer and compare it against the expected hash
|
||||||
|
calculated_hash = self.file_helper.calculate_buffer_hash(self.request.body)
|
||||||
|
if str(self.chunk_hash) != str(calculated_hash):
|
||||||
|
logger.error(
|
||||||
|
f"File upload failed. Filename: {self.filename}"
|
||||||
|
f"Type: {u_type} Error: INVALID HASH"
|
||||||
|
)
|
||||||
|
return self.finish_json(
|
||||||
|
400,
|
||||||
|
{
|
||||||
|
"status": "error",
|
||||||
|
"error": "INVALID_HASH",
|
||||||
|
"data": {
|
||||||
|
"message": "Hash recieved does not match reported sent hash.",
|
||||||
|
"chunk_id": self.chunk_index,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
# File paths
|
||||||
|
file_path = os.path.join(self.upload_dir, self.filename)
|
||||||
|
chunk_path = os.path.join(
|
||||||
|
self.temp_dir, f"{self.filename}.part{self.chunk_index}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Save the chunk
|
||||||
|
with open(chunk_path, "wb") as f:
|
||||||
|
f.write(self.request.body)
|
||||||
|
|
||||||
|
# Check if all chunks are received
|
||||||
|
received_chunks = [
|
||||||
|
f
|
||||||
|
for f in os.listdir(self.temp_dir)
|
||||||
|
if f.startswith(f"{self.filename}.part")
|
||||||
|
]
|
||||||
|
# When we've reached the total chunks we'll
|
||||||
|
# Compare the hash and write the file
|
||||||
|
if len(received_chunks) == total_chunks:
|
||||||
|
with open(file_path, "wb") as outfile:
|
||||||
|
for i in range(total_chunks):
|
||||||
|
chunk_file = os.path.join(self.temp_dir, f"{self.filename}.part{i}")
|
||||||
|
with open(chunk_file, "rb") as infile:
|
||||||
|
outfile.write(infile.read())
|
||||||
|
os.remove(chunk_file)
|
||||||
|
logger.info(
|
||||||
|
f"File upload completed. Filename: {self.filename}"
|
||||||
|
f" Path: {file_path} Type: {u_type}"
|
||||||
|
)
|
||||||
|
self.controller.management.add_to_audit_log(
|
||||||
|
auth_data[4]["user_id"],
|
||||||
|
f"Uploaded file {self.filename}",
|
||||||
|
server_id,
|
||||||
|
self.request.remote_ip,
|
||||||
|
)
|
||||||
|
self.finish_json(
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
"status": "completed",
|
||||||
|
"data": {"message": "File uploaded successfully"},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
self.finish_json(
|
||||||
|
200,
|
||||||
|
{
|
||||||
|
"status": "partial",
|
||||||
|
"data": {"message": f"Chunk {self.chunk_index} received"},
|
||||||
|
},
|
||||||
|
)
|
@ -2,6 +2,7 @@ import typing as t
|
|||||||
from jsonschema import ValidationError, validate
|
from jsonschema import ValidationError, validate
|
||||||
import orjson
|
import orjson
|
||||||
from playhouse.shortcuts import model_to_dict
|
from playhouse.shortcuts import model_to_dict
|
||||||
|
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
|
||||||
from app.classes.web.base_api_handler import BaseApiHandler
|
from app.classes.web.base_api_handler import BaseApiHandler
|
||||||
|
|
||||||
create_role_schema = {
|
create_role_schema = {
|
||||||
@ -72,7 +73,7 @@ class ApiRolesIndexHandler(BaseApiHandler):
|
|||||||
return
|
return
|
||||||
(
|
(
|
||||||
_,
|
_,
|
||||||
_,
|
exec_user_permissions_crafty,
|
||||||
_,
|
_,
|
||||||
superuser,
|
superuser,
|
||||||
_,
|
_,
|
||||||
@ -82,7 +83,10 @@ class ApiRolesIndexHandler(BaseApiHandler):
|
|||||||
# GET /api/v2/roles?ids=true
|
# GET /api/v2/roles?ids=true
|
||||||
get_only_ids = self.get_query_argument("ids", None) == "true"
|
get_only_ids = self.get_query_argument("ids", None) == "true"
|
||||||
|
|
||||||
if not superuser:
|
if (
|
||||||
|
not superuser
|
||||||
|
and EnumPermissionsCrafty.ROLES_CONFIG not in exec_user_permissions_crafty
|
||||||
|
):
|
||||||
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||||
|
|
||||||
self.finish_json(
|
self.finish_json(
|
||||||
@ -105,14 +109,17 @@ class ApiRolesIndexHandler(BaseApiHandler):
|
|||||||
return
|
return
|
||||||
(
|
(
|
||||||
_,
|
_,
|
||||||
_,
|
exec_user_permissions_crafty,
|
||||||
_,
|
_,
|
||||||
superuser,
|
superuser,
|
||||||
user,
|
user,
|
||||||
_,
|
_,
|
||||||
) = auth_data
|
) = auth_data
|
||||||
|
|
||||||
if not superuser:
|
if (
|
||||||
|
not superuser
|
||||||
|
and EnumPermissionsCrafty.ROLES_CONFIG not in exec_user_permissions_crafty
|
||||||
|
):
|
||||||
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -139,6 +146,8 @@ class ApiRolesIndexHandler(BaseApiHandler):
|
|||||||
|
|
||||||
role_name = data["name"]
|
role_name = data["name"]
|
||||||
manager = data.get("manager", None)
|
manager = data.get("manager", None)
|
||||||
|
if not superuser and not manager:
|
||||||
|
manager = auth_data[4]["user_id"]
|
||||||
if manager == self.controller.users.get_id_by_name("SYSTEM") or manager == 0:
|
if manager == self.controller.users.get_id_by_name("SYSTEM") or manager == 0:
|
||||||
manager = None
|
manager = None
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
from jsonschema import ValidationError, validate
|
from jsonschema import ValidationError, validate
|
||||||
import orjson
|
import orjson
|
||||||
from peewee import DoesNotExist
|
from peewee import DoesNotExist, IntegrityError
|
||||||
|
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
|
||||||
from app.classes.web.base_api_handler import BaseApiHandler
|
from app.classes.web.base_api_handler import BaseApiHandler
|
||||||
|
|
||||||
modify_role_schema = {
|
modify_role_schema = {
|
||||||
@ -71,14 +72,17 @@ class ApiRolesRoleIndexHandler(BaseApiHandler):
|
|||||||
return
|
return
|
||||||
(
|
(
|
||||||
_,
|
_,
|
||||||
_,
|
exec_user_permissions_crafty,
|
||||||
_,
|
_,
|
||||||
superuser,
|
superuser,
|
||||||
_,
|
_,
|
||||||
_,
|
_,
|
||||||
) = auth_data
|
) = auth_data
|
||||||
|
|
||||||
if not superuser:
|
if (
|
||||||
|
not superuser
|
||||||
|
and EnumPermissionsCrafty.ROLES_CONFIG not in exec_user_permissions_crafty
|
||||||
|
):
|
||||||
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -101,8 +105,11 @@ class ApiRolesRoleIndexHandler(BaseApiHandler):
|
|||||||
user,
|
user,
|
||||||
_,
|
_,
|
||||||
) = auth_data
|
) = auth_data
|
||||||
|
role = self.controller.roles.get_role(role_id)
|
||||||
if not superuser:
|
if (
|
||||||
|
str(role.get("manager", "no manager found")) != str(auth_data[4]["user_id"])
|
||||||
|
and not superuser
|
||||||
|
):
|
||||||
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
||||||
|
|
||||||
self.controller.roles.remove_role(role_id)
|
self.controller.roles.remove_role(role_id)
|
||||||
@ -125,7 +132,7 @@ class ApiRolesRoleIndexHandler(BaseApiHandler):
|
|||||||
return
|
return
|
||||||
(
|
(
|
||||||
_,
|
_,
|
||||||
_,
|
exec_user_permissions_crafty,
|
||||||
_,
|
_,
|
||||||
superuser,
|
superuser,
|
||||||
user,
|
user,
|
||||||
@ -133,7 +140,10 @@ class ApiRolesRoleIndexHandler(BaseApiHandler):
|
|||||||
) = auth_data
|
) = auth_data
|
||||||
|
|
||||||
role = self.controller.roles.get_role(role_id)
|
role = self.controller.roles.get_role(role_id)
|
||||||
if not superuser and user["user_id"] != role["manager"]:
|
if not superuser and (
|
||||||
|
user["user_id"] != role["manager"]
|
||||||
|
or EnumPermissionsCrafty.ROLES_CONFIG not in exec_user_permissions_crafty
|
||||||
|
):
|
||||||
return self.finish_json(
|
return self.finish_json(
|
||||||
400,
|
400,
|
||||||
{
|
{
|
||||||
@ -180,7 +190,10 @@ class ApiRolesRoleIndexHandler(BaseApiHandler):
|
|||||||
)
|
)
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
return self.finish_json(404, {"status": "error", "error": "ROLE_NOT_FOUND"})
|
return self.finish_json(404, {"status": "error", "error": "ROLE_NOT_FOUND"})
|
||||||
|
except IntegrityError:
|
||||||
|
return self.finish_json(
|
||||||
|
404, {"status": "error", "error": "ROLE_NAME_EXISTS"}
|
||||||
|
)
|
||||||
self.controller.management.add_to_audit_log(
|
self.controller.management.add_to_audit_log(
|
||||||
user["user_id"],
|
user["user_id"],
|
||||||
f"modified role with ID {role_id}",
|
f"modified role with ID {role_id}",
|
||||||
|
@ -23,6 +23,7 @@ new_server_schema = {
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"examples": ["My Server"],
|
"examples": ["My Server"],
|
||||||
"minLength": 2,
|
"minLength": 2,
|
||||||
|
"pattern": "^[^/\\\\]*$",
|
||||||
},
|
},
|
||||||
"roles": {"title": "Roles to add", "type": "array", "examples": [1, 2, 3]},
|
"roles": {"title": "Roles to add", "type": "array", "examples": [1, 2, 3]},
|
||||||
"stop_command": {
|
"stop_command": {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
|
import json
|
||||||
from app.classes.models.server_permissions import EnumPermissionsServer
|
from app.classes.models.server_permissions import EnumPermissionsServer
|
||||||
from app.classes.models.servers import Servers
|
from app.classes.models.servers import Servers
|
||||||
from app.classes.shared.file_helpers import FileHelpers
|
from app.classes.shared.file_helpers import FileHelpers
|
||||||
@ -10,7 +11,7 @@ logger = logging.getLogger(__name__)
|
|||||||
|
|
||||||
|
|
||||||
class ApiServersServerActionHandler(BaseApiHandler):
|
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()
|
auth_data = self.authenticate_user()
|
||||||
if not auth_data:
|
if not auth_data:
|
||||||
return
|
return
|
||||||
@ -54,7 +55,7 @@ class ApiServersServerActionHandler(BaseApiHandler):
|
|||||||
return self._agree_eula(server_id, auth_data[4]["user_id"])
|
return self._agree_eula(server_id, auth_data[4]["user_id"])
|
||||||
|
|
||||||
self.controller.management.send_command(
|
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(
|
self.finish_json(
|
||||||
@ -82,6 +83,20 @@ class ApiServersServerActionHandler(BaseApiHandler):
|
|||||||
new_server_id = self.helper.create_uuid()
|
new_server_id = self.helper.create_uuid()
|
||||||
new_server_path = os.path.join(self.helper.servers_dir, new_server_id)
|
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)
|
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(
|
new_server_command = str(server_data.get("execution_command")).replace(
|
||||||
server_id, new_server_id
|
server_id, new_server_id
|
||||||
)
|
)
|
||||||
@ -93,7 +108,6 @@ class ApiServersServerActionHandler(BaseApiHandler):
|
|||||||
new_server_name,
|
new_server_name,
|
||||||
new_server_id,
|
new_server_id,
|
||||||
new_server_path,
|
new_server_path,
|
||||||
new_backup_path,
|
|
||||||
new_server_command,
|
new_server_command,
|
||||||
server_data.get("executable"),
|
server_data.get("executable"),
|
||||||
new_server_log_path,
|
new_server_log_path,
|
||||||
@ -103,6 +117,8 @@ class ApiServersServerActionHandler(BaseApiHandler):
|
|||||||
server_data.get("type"),
|
server_data.get("type"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self.controller.management.add_backup_config(backup_data)
|
||||||
|
|
||||||
self.controller.management.add_to_audit_log(
|
self.controller.management.add_to_audit_log(
|
||||||
user_id,
|
user_id,
|
||||||
f"is cloning server {server_id} named {server_data.get('server_name')}",
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
backup_schema = {
|
BACKUP_SCHEMA = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"filename": {"type": "string", "minLength": 5},
|
"filename": {"type": "string", "minLength": 5},
|
||||||
@ -19,11 +19,44 @@ backup_schema = {
|
|||||||
"additionalProperties": False,
|
"additionalProperties": False,
|
||||||
"minProperties": 1,
|
"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):
|
class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
|
||||||
def get(self, server_id: str):
|
def get(self, server_id: str, backup_id: str):
|
||||||
auth_data = self.authenticate_user()
|
auth_data = self.authenticate_user()
|
||||||
|
backup_conf = self.controller.management.get_backup_config(backup_id)
|
||||||
if not auth_data:
|
if not auth_data:
|
||||||
return
|
return
|
||||||
mask = self.controller.server_perms.get_lowest_api_perm_mask(
|
mask = self.controller.server_perms.get_lowest_api_perm_mask(
|
||||||
@ -32,64 +65,81 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
|
|||||||
),
|
),
|
||||||
auth_data[5],
|
auth_data[5],
|
||||||
)
|
)
|
||||||
server_permissions = self.controller.server_perms.get_permissions(mask)
|
if backup_conf["server_id"]["server_id"] != server_id:
|
||||||
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))
|
|
||||||
|
|
||||||
def delete(self, server_id: str):
|
|
||||||
auth_data = self.authenticate_user()
|
|
||||||
backup_conf = self.controller.management.get_backup_config(server_id)
|
|
||||||
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(
|
return self.finish_json(
|
||||||
400,
|
400,
|
||||||
{
|
{
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"error": "INVALID_JSON_SCHEMA",
|
"error": "ID_MISMATCH",
|
||||||
"error_data": str(e),
|
"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",
|
||||||
|
"error_data": GENERAL_AUTH_ERROR,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
self.finish_json(200, backup_conf)
|
||||||
|
|
||||||
|
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:
|
|
||||||
FileHelpers.del_file(
|
|
||||||
os.path.join(backup_conf["backup_path"], data["filename"])
|
|
||||||
)
|
|
||||||
except Exception as e:
|
|
||||||
return self.finish_json(
|
|
||||||
400, {"status": "error", "error": f"DELETE FAILED with error {e}"}
|
|
||||||
)
|
|
||||||
self.controller.management.add_to_audit_log(
|
self.controller.management.add_to_audit_log(
|
||||||
auth_data[4]["user_id"],
|
auth_data[4]["user_id"],
|
||||||
f"Edited server {server_id}: removed backup {data['filename']}",
|
f"Edited server {server_id}: removed backup config"
|
||||||
|
f" {backup_conf['backup_name']}",
|
||||||
server_id,
|
server_id,
|
||||||
self.get_remote_ip(),
|
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"})
|
return self.finish_json(200, {"status": "ok"})
|
||||||
|
|
||||||
def post(self, server_id: str):
|
def post(self, server_id: str, backup_id: str):
|
||||||
auth_data = self.authenticate_user()
|
auth_data = self.authenticate_user()
|
||||||
if not auth_data:
|
if not auth_data:
|
||||||
return
|
return
|
||||||
@ -102,7 +152,24 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
|
|||||||
server_permissions = self.controller.server_perms.get_permissions(mask)
|
server_permissions = self.controller.server_perms.get_permissions(mask)
|
||||||
if EnumPermissionsServer.BACKUP not in server_permissions:
|
if EnumPermissionsServer.BACKUP not in server_permissions:
|
||||||
# if the user doesn't have Schedule permission, return an error
|
# 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,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
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:
|
try:
|
||||||
data = json.loads(self.request.body)
|
data = json.loads(self.request.body)
|
||||||
@ -111,7 +178,7 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
|
|||||||
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
|
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
validate(data, backup_schema)
|
validate(data, BACKUP_SCHEMA)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
return self.finish_json(
|
return self.finish_json(
|
||||||
400,
|
400,
|
||||||
@ -122,14 +189,21 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
|
|||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
|
||||||
svr_obj = self.controller.servers.get_server_obj(server_id)
|
svr_obj = self.controller.servers.get_server_obj(server_id)
|
||||||
server_data = self.controller.servers.get_server_data_by_id(server_id)
|
server_data = self.controller.servers.get_server_data_by_id(server_id)
|
||||||
zip_name = data["filename"]
|
zip_name = data["filename"]
|
||||||
# import the server again based on zipfile
|
# import the server again based on zipfile
|
||||||
backup_path = svr_obj.backup_path
|
backup_config = self.controller.management.get_backup_config(backup_id)
|
||||||
if Helpers.validate_traversal(backup_path, zip_name):
|
backup_location = os.path.join(
|
||||||
temp_dir = Helpers.unzip_backup_archive(backup_path, zip_name)
|
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":
|
if server_data["type"] == "minecraft-java":
|
||||||
new_server = self.controller.restore_java_zip_server(
|
new_server = self.controller.restore_java_zip_server(
|
||||||
svr_obj.server_name,
|
svr_obj.server_name,
|
||||||
@ -151,7 +225,9 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
|
|||||||
new_server_id = new_server
|
new_server_id = new_server
|
||||||
new_server = self.controller.servers.get_server_data(new_server)
|
new_server = self.controller.servers.get_server_data(new_server)
|
||||||
self.controller.rename_backup_dir(
|
self.controller.rename_backup_dir(
|
||||||
server_id, new_server_id, new_server["server_id"]
|
server_id,
|
||||||
|
new_server_id,
|
||||||
|
new_server["server_id"],
|
||||||
)
|
)
|
||||||
# preserve current schedules
|
# preserve current schedules
|
||||||
for schedule in self.controller.management.get_schedules_by_server(
|
for schedule in self.controller.management.get_schedules_by_server(
|
||||||
@ -184,24 +260,26 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
|
|||||||
self.controller.servers.update_server(new_server_obj)
|
self.controller.servers.update_server(new_server_obj)
|
||||||
|
|
||||||
# preserve backup config
|
# preserve backup config
|
||||||
backup_config = self.controller.management.get_backup_config(server_id)
|
server_backups = self.controller.management.get_backups_by_server(server_id)
|
||||||
excluded_dirs = []
|
for backup in server_backups:
|
||||||
server_obj = self.controller.servers.get_server_obj(server_id)
|
old_backup_id = server_backups[backup]["backup_id"]
|
||||||
loop_backup_path = self.helper.wtol_path(server_obj.path)
|
del server_backups[backup]["backup_id"]
|
||||||
for item in self.controller.management.get_excluded_backup_dirs(
|
server_backups[backup]["server_id"] = new_server_id
|
||||||
server_id
|
if str(server_id) in (server_backups[backup]["backup_location"]):
|
||||||
):
|
server_backups[backup]["backup_location"] = str(
|
||||||
item_path = self.helper.wtol_path(item)
|
server_backups[backup]["backup_location"]
|
||||||
bu_path = os.path.relpath(item_path, loop_backup_path)
|
).replace(str(server_id), str(new_server_id))
|
||||||
bu_path = os.path.join(new_server_obj.path, bu_path)
|
new_backup_id = self.controller.management.add_backup_config(
|
||||||
excluded_dirs.append(bu_path)
|
server_backups[backup]
|
||||||
self.controller.management.set_backup_config(
|
)
|
||||||
new_server_id,
|
os.listdir(server_backups[backup]["backup_location"])
|
||||||
new_server_obj.backup_path,
|
FileHelpers.move_dir(
|
||||||
backup_config["max_backups"],
|
os.path.join(
|
||||||
excluded_dirs,
|
server_backups[backup]["backup_location"], old_backup_id
|
||||||
backup_config["compress"],
|
),
|
||||||
backup_config["shutdown"],
|
os.path.join(
|
||||||
|
server_backups[backup]["backup_location"], new_backup_id
|
||||||
|
),
|
||||||
)
|
)
|
||||||
# remove old server's tasks
|
# remove old server's tasks
|
||||||
try:
|
try:
|
||||||
@ -209,10 +287,7 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
|
|||||||
except JobLookupError as e:
|
except JobLookupError as e:
|
||||||
logger.info("No active tasks found for server: {e}")
|
logger.info("No active tasks found for server: {e}")
|
||||||
self.controller.remove_server(server_id, True)
|
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(
|
self.controller.management.add_to_audit_log(
|
||||||
auth_data[4]["user_id"],
|
auth_data[4]["user_id"],
|
||||||
f"Restored server {server_id} backup {data['filename']}",
|
f"Restored server {server_id} backup {data['filename']}",
|
||||||
@ -221,3 +296,149 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
return self.finish_json(200, {"status": "ok"})
|
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_location"],
|
||||||
|
backup_conf["backup_id"],
|
||||||
|
data["filename"],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
return self.finish_json(
|
||||||
|
400, {"status": "error", "error": f"DELETE FAILED with error {e}"}
|
||||||
|
)
|
||||||
|
self.controller.management.add_to_audit_log(
|
||||||
|
auth_data[4]["user_id"],
|
||||||
|
f"Edited server {server_id}: removed 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 logging
|
||||||
import json
|
import json
|
||||||
from jsonschema import validate
|
from jsonschema import validate
|
||||||
@ -10,13 +11,14 @@ logger = logging.getLogger(__name__)
|
|||||||
backup_patch_schema = {
|
backup_patch_schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"backup_path": {"type": "string", "minLength": 1},
|
"backup_name": {"type": "string", "minLength": 3},
|
||||||
|
"backup_location": {"type": "string", "minLength": 1},
|
||||||
"max_backups": {"type": "integer"},
|
"max_backups": {"type": "integer"},
|
||||||
"compress": {"type": "boolean"},
|
"compress": {"type": "boolean"},
|
||||||
"shutdown": {"type": "boolean"},
|
"shutdown": {"type": "boolean"},
|
||||||
"backup_before": {"type": "string"},
|
"before": {"type": "string"},
|
||||||
"backup_after": {"type": "string"},
|
"after": {"type": "string"},
|
||||||
"exclusions": {"type": "array"},
|
"excluded_dirs": {"type": "array"},
|
||||||
},
|
},
|
||||||
"additionalProperties": False,
|
"additionalProperties": False,
|
||||||
"minProperties": 1,
|
"minProperties": 1,
|
||||||
@ -25,12 +27,13 @@ backup_patch_schema = {
|
|||||||
basic_backup_patch_schema = {
|
basic_backup_patch_schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"backup_name": {"type": "string", "minLength": 3},
|
||||||
"max_backups": {"type": "integer"},
|
"max_backups": {"type": "integer"},
|
||||||
"compress": {"type": "boolean"},
|
"compress": {"type": "boolean"},
|
||||||
"shutdown": {"type": "boolean"},
|
"shutdown": {"type": "boolean"},
|
||||||
"backup_before": {"type": "string"},
|
"before": {"type": "string"},
|
||||||
"backup_after": {"type": "string"},
|
"after": {"type": "string"},
|
||||||
"exclusions": {"type": "array"},
|
"excluded_dirs": {"type": "array"},
|
||||||
},
|
},
|
||||||
"additionalProperties": False,
|
"additionalProperties": False,
|
||||||
"minProperties": 1,
|
"minProperties": 1,
|
||||||
@ -52,9 +55,11 @@ class ApiServersServerBackupsIndexHandler(BaseApiHandler):
|
|||||||
if EnumPermissionsServer.BACKUP not in server_permissions:
|
if EnumPermissionsServer.BACKUP not in server_permissions:
|
||||||
# if the user doesn't have Schedule permission, return an error
|
# 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"})
|
||||||
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()
|
auth_data = self.authenticate_user()
|
||||||
if not auth_data:
|
if not auth_data:
|
||||||
return
|
return
|
||||||
@ -80,7 +85,6 @@ class ApiServersServerBackupsIndexHandler(BaseApiHandler):
|
|||||||
"error_data": str(e),
|
"error_data": str(e),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
|
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
|
# if the user doesn't have access to the server, return an error
|
||||||
return self.finish_json(400, {"status": "error", "error": "NOT_AUTHORIZED"})
|
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 EnumPermissionsServer.BACKUP not in server_permissions:
|
||||||
# if the user doesn't have Schedule permission, return an error
|
# 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"})
|
||||||
|
# Set the backup location automatically for non-super users. We should probably
|
||||||
self.controller.management.set_backup_config(
|
# make the default location configurable for SU eventually
|
||||||
server_id,
|
if not auth_data[4]["superuser"]:
|
||||||
data.get(
|
data["backup_location"] = os.path.join(self.helper.backup_path, server_id)
|
||||||
"backup_path",
|
data["server_id"] = server_id
|
||||||
self.controller.management.get_backup_config(server_id)["backup_path"],
|
if not data.get("excluded_dirs", None):
|
||||||
),
|
data["excluded_dirs"] = []
|
||||||
data.get(
|
self.controller.management.add_backup_config(data)
|
||||||
"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"],
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return self.finish_json(200, {"status": "ok"})
|
return self.finish_json(200, {"status": "ok"})
|
||||||
|
@ -72,7 +72,7 @@ file_delete_schema = {
|
|||||||
|
|
||||||
|
|
||||||
class ApiServersServerFilesIndexHandler(BaseApiHandler):
|
class ApiServersServerFilesIndexHandler(BaseApiHandler):
|
||||||
def post(self, server_id: str):
|
def post(self, server_id: str, backup_id=None):
|
||||||
auth_data = self.authenticate_user()
|
auth_data = self.authenticate_user()
|
||||||
if not auth_data:
|
if not auth_data:
|
||||||
return
|
return
|
||||||
@ -149,9 +149,10 @@ class ApiServersServerFilesIndexHandler(BaseApiHandler):
|
|||||||
filename = html.escape(raw_filename)
|
filename = html.escape(raw_filename)
|
||||||
rel = os.path.join(folder, raw_filename)
|
rel = os.path.join(folder, raw_filename)
|
||||||
dpath = os.path.join(folder, filename)
|
dpath = os.path.join(folder, filename)
|
||||||
if str(dpath) in self.controller.management.get_excluded_backup_dirs(
|
if backup_id:
|
||||||
server_id
|
if str(
|
||||||
):
|
dpath
|
||||||
|
) in self.controller.management.get_excluded_backup_dirs(backup_id):
|
||||||
if os.path.isdir(rel):
|
if os.path.isdir(rel):
|
||||||
return_json[filename] = {
|
return_json[filename] = {
|
||||||
"path": dpath,
|
"path": dpath,
|
||||||
@ -177,6 +178,19 @@ class ApiServersServerFilesIndexHandler(BaseApiHandler):
|
|||||||
"dir": False,
|
"dir": False,
|
||||||
"excluded": False,
|
"excluded": False,
|
||||||
}
|
}
|
||||||
|
else:
|
||||||
|
if os.path.isdir(rel):
|
||||||
|
return_json[filename] = {
|
||||||
|
"path": dpath,
|
||||||
|
"dir": True,
|
||||||
|
"excluded": False,
|
||||||
|
}
|
||||||
|
else:
|
||||||
|
return_json[filename] = {
|
||||||
|
"path": dpath,
|
||||||
|
"dir": False,
|
||||||
|
"excluded": False,
|
||||||
|
}
|
||||||
self.finish_json(200, {"status": "ok", "data": return_json})
|
self.finish_json(200, {"status": "ok", "data": return_json})
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
@ -189,7 +203,7 @@ class ApiServersServerFilesIndexHandler(BaseApiHandler):
|
|||||||
)
|
)
|
||||||
self.finish_json(200, {"status": "ok", "data": file_contents})
|
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()
|
auth_data = self.authenticate_user()
|
||||||
if not auth_data:
|
if not auth_data:
|
||||||
return
|
return
|
||||||
@ -247,7 +261,7 @@ class ApiServersServerFilesIndexHandler(BaseApiHandler):
|
|||||||
return self.finish_json(200, {"status": "ok"})
|
return self.finish_json(200, {"status": "ok"})
|
||||||
return self.finish_json(500, {"status": "error", "error": str(proc)})
|
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()
|
auth_data = self.authenticate_user()
|
||||||
if not auth_data:
|
if not auth_data:
|
||||||
return
|
return
|
||||||
@ -301,7 +315,7 @@ class ApiServersServerFilesIndexHandler(BaseApiHandler):
|
|||||||
file_object.write(file_contents)
|
file_object.write(file_contents)
|
||||||
return self.finish_json(200, {"status": "ok"})
|
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()
|
auth_data = self.authenticate_user()
|
||||||
if not auth_data:
|
if not auth_data:
|
||||||
return
|
return
|
||||||
|
@ -12,7 +12,7 @@ logger = logging.getLogger(__name__)
|
|||||||
server_patch_schema = {
|
server_patch_schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"server_name": {"type": "string", "minLength": 1},
|
"server_name": {"type": "string", "minLength": 2, "pattern": "^[^/\\\\]*$"},
|
||||||
"backup_path": {"type": "string"},
|
"backup_path": {"type": "string"},
|
||||||
"executable": {"type": "string"},
|
"executable": {"type": "string"},
|
||||||
"log_path": {"type": "string", "minLength": 1},
|
"log_path": {"type": "string", "minLength": 1},
|
||||||
|
@ -21,6 +21,9 @@ new_task_schema = {
|
|||||||
"action": {
|
"action": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
|
"action_id": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
"interval": {"type": "integer"},
|
"interval": {"type": "integer"},
|
||||||
"interval_type": {
|
"interval_type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -110,6 +113,18 @@ class ApiServersServerTasksIndexHandler(BaseApiHandler):
|
|||||||
)
|
)
|
||||||
if "parent" not in data:
|
if "parent" not in data:
|
||||||
data["parent"] = None
|
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)
|
task_id = self.tasks_manager.schedule_job(data)
|
||||||
|
|
||||||
self.controller.management.add_to_audit_log(
|
self.controller.management.add_to_audit_log(
|
||||||
|
@ -22,6 +22,9 @@ task_patch_schema = {
|
|||||||
"action": {
|
"action": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
|
"action_id": {
|
||||||
|
"type": "string",
|
||||||
|
},
|
||||||
"interval": {"type": "integer"},
|
"interval": {"type": "integer"},
|
||||||
"interval_type": {
|
"interval_type": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
@ -2,6 +2,7 @@ import logging
|
|||||||
import json
|
import json
|
||||||
from jsonschema import validate
|
from jsonschema import validate
|
||||||
from jsonschema.exceptions import ValidationError
|
from jsonschema.exceptions import ValidationError
|
||||||
|
from app.classes.shared.translation import Translation
|
||||||
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
|
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
|
||||||
from app.classes.models.roles import Roles, HelperRoles
|
from app.classes.models.roles import Roles, HelperRoles
|
||||||
from app.classes.models.users import PUBLIC_USER_ATTRS
|
from app.classes.models.users import PUBLIC_USER_ATTRS
|
||||||
@ -54,6 +55,7 @@ class ApiUsersIndexHandler(BaseApiHandler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def post(self):
|
def post(self):
|
||||||
|
self.translator = Translation(self.helper)
|
||||||
new_user_schema = {
|
new_user_schema = {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -87,12 +89,17 @@ class ApiUsersIndexHandler(BaseApiHandler):
|
|||||||
try:
|
try:
|
||||||
validate(data, new_user_schema)
|
validate(data, new_user_schema)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
|
err = self.translator.translate(
|
||||||
|
"validators",
|
||||||
|
e.schema["error"],
|
||||||
|
self.controller.users.get_user_lang_by_id(auth_data[4]["user_id"]),
|
||||||
|
)
|
||||||
return self.finish_json(
|
return self.finish_json(
|
||||||
400,
|
400,
|
||||||
{
|
{
|
||||||
"status": "error",
|
"status": "error",
|
||||||
"error": "INVALID_JSON_SCHEMA",
|
"error": "INVALID_JSON_SCHEMA",
|
||||||
"error_data": str(e),
|
"error_data": f"{str(err)}",
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
username = data["username"]
|
username = data["username"]
|
||||||
@ -153,7 +160,11 @@ class ApiUsersIndexHandler(BaseApiHandler):
|
|||||||
|
|
||||||
for role in roles:
|
for role in roles:
|
||||||
role = self.controller.roles.get_role(role)
|
role = self.controller.roles.get_role(role)
|
||||||
if int(role["manager"]) != int(auth_data[4]["user_id"]) and not superuser:
|
if (
|
||||||
|
str(role.get("manager", "no manager found"))
|
||||||
|
!= str(auth_data[4]["user_id"])
|
||||||
|
and not superuser
|
||||||
|
):
|
||||||
return self.finish_json(
|
return self.finish_json(
|
||||||
400, {"status": "error", "error": "INVALID_ROLES_CREATE"}
|
400, {"status": "error", "error": "INVALID_ROLES_CREATE"}
|
||||||
)
|
)
|
||||||
|
@ -217,7 +217,7 @@ class ApiUsersUserKeyHandler(BaseApiHandler):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
target_key.user_id != auth_data[4]["user_id"]
|
str(target_key.user_id) != str(auth_data[4]["user_id"])
|
||||||
and not auth_data[4]["superuser"]
|
and not auth_data[4]["superuser"]
|
||||||
):
|
):
|
||||||
return self.finish_json(
|
return self.finish_json(
|
||||||
|
@ -132,7 +132,6 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
|
|||||||
return self.finish_json(
|
return self.finish_json(
|
||||||
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
|
400, {"status": "error", "error": "INVALID_JSON", "error_data": str(e)}
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
validate(data, user_patch_schema)
|
validate(data, user_patch_schema)
|
||||||
except ValidationError as e:
|
except ValidationError as e:
|
||||||
@ -144,10 +143,8 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
|
|||||||
"error_data": str(e),
|
"error_data": str(e),
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
if user_id == "@me":
|
if user_id == "@me":
|
||||||
user_id = user["user_id"]
|
user_id = user["user_id"]
|
||||||
|
|
||||||
if (
|
if (
|
||||||
EnumPermissionsCrafty.USER_CONFIG not in exec_user_crafty_permissions
|
EnumPermissionsCrafty.USER_CONFIG not in exec_user_crafty_permissions
|
||||||
and str(user["user_id"]) != str(user_id)
|
and str(user["user_id"]) != str(user_id)
|
||||||
@ -215,6 +212,25 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
|
|||||||
return self.finish_json(
|
return self.finish_json(
|
||||||
400, {"status": "error", "error": "INVALID_ROLES_MODIFY"}
|
400, {"status": "error", "error": "INVALID_ROLES_MODIFY"}
|
||||||
)
|
)
|
||||||
|
user_modify = self.controller.users.get_user_roles_id(user_id)
|
||||||
|
|
||||||
|
for role in data["roles"]:
|
||||||
|
# Check if user is not a super user and that the exec user is the role
|
||||||
|
# manager or that the role already exists in the user's list
|
||||||
|
if not superuser and (
|
||||||
|
str(
|
||||||
|
self.controller.roles.get_role(role).get(
|
||||||
|
"manager", "no manager found"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
!= str(auth_data[4]["user_id"])
|
||||||
|
and role not in user_modify
|
||||||
|
):
|
||||||
|
for item in user_modify:
|
||||||
|
print(type(role), type(item))
|
||||||
|
return self.finish_json(
|
||||||
|
400, {"status": "error", "error": "INVALID_ROLES_MODIFY"}
|
||||||
|
)
|
||||||
|
|
||||||
user_obj = HelperUsers.get_user_model(user_id)
|
user_obj = HelperUsers.get_user_model(user_id)
|
||||||
if "password" in data and str(user["user_id"]) != str(user_id):
|
if "password" in data and str(user["user_id"]) != str(user_id):
|
||||||
|
@ -24,7 +24,6 @@ from app.classes.web.routes.metrics.metrics_handlers import metrics_handlers
|
|||||||
from app.classes.web.server_handler import ServerHandler
|
from app.classes.web.server_handler import ServerHandler
|
||||||
from app.classes.web.websocket_handler import WebSocketHandler
|
from app.classes.web.websocket_handler import WebSocketHandler
|
||||||
from app.classes.web.static_handler import CustomStaticHandler
|
from app.classes.web.static_handler import CustomStaticHandler
|
||||||
from app.classes.web.upload_handler import UploadHandler
|
|
||||||
from app.classes.web.status_handler import StatusHandler
|
from app.classes.web.status_handler import StatusHandler
|
||||||
|
|
||||||
|
|
||||||
@ -142,7 +141,6 @@ class Webserver:
|
|||||||
(r"/panel/(.*)", PanelHandler, handler_args),
|
(r"/panel/(.*)", PanelHandler, handler_args),
|
||||||
(r"/server/(.*)", ServerHandler, handler_args),
|
(r"/server/(.*)", ServerHandler, handler_args),
|
||||||
(r"/ws", WebSocketHandler, handler_args),
|
(r"/ws", WebSocketHandler, handler_args),
|
||||||
(r"/upload", UploadHandler, handler_args),
|
|
||||||
(r"/status", StatusHandler, handler_args),
|
(r"/status", StatusHandler, handler_args),
|
||||||
# API Routes V2
|
# API Routes V2
|
||||||
*api_handlers(handler_args),
|
*api_handlers(handler_args),
|
||||||
|
@ -1,331 +0,0 @@
|
|||||||
import logging
|
|
||||||
import os
|
|
||||||
import time
|
|
||||||
import urllib.parse
|
|
||||||
import tornado.web
|
|
||||||
import tornado.options
|
|
||||||
import tornado.httpserver
|
|
||||||
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
|
|
||||||
|
|
||||||
from app.classes.models.server_permissions import EnumPermissionsServer
|
|
||||||
from app.classes.shared.console import Console
|
|
||||||
from app.classes.shared.helpers import Helpers
|
|
||||||
from app.classes.shared.main_controller import Controller
|
|
||||||
from app.classes.web.base_handler import BaseHandler
|
|
||||||
from app.classes.shared.websocket_manager import WebSocketManager
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
@tornado.web.stream_request_body
|
|
||||||
class UploadHandler(BaseHandler):
|
|
||||||
# noinspection PyAttributeOutsideInit
|
|
||||||
def initialize(
|
|
||||||
self,
|
|
||||||
helper: Helpers = None,
|
|
||||||
controller: Controller = None,
|
|
||||||
tasks_manager=None,
|
|
||||||
translator=None,
|
|
||||||
file_helper=None,
|
|
||||||
):
|
|
||||||
self.helper = helper
|
|
||||||
self.controller = controller
|
|
||||||
self.tasks_manager = tasks_manager
|
|
||||||
self.translator = translator
|
|
||||||
self.file_helper = file_helper
|
|
||||||
|
|
||||||
def prepare(self):
|
|
||||||
# Class & Function Defination
|
|
||||||
api_key, _token_data, exec_user = self.current_user
|
|
||||||
self.upload_type = str(self.request.headers.get("X-Content-Upload-Type"))
|
|
||||||
|
|
||||||
if self.upload_type == "server_import":
|
|
||||||
superuser = exec_user["superuser"]
|
|
||||||
if api_key is not None:
|
|
||||||
superuser = superuser and api_key.full_access
|
|
||||||
user_id = exec_user["user_id"]
|
|
||||||
stream_size_value = self.helper.get_setting("stream_size_GB")
|
|
||||||
|
|
||||||
max_streamed_size = (1024 * 1024 * 1024) * stream_size_value
|
|
||||||
|
|
||||||
self.content_len = int(self.request.headers.get("Content-Length"))
|
|
||||||
if self.content_len > max_streamed_size:
|
|
||||||
logger.error(
|
|
||||||
f"User with ID {user_id} attempted to upload a file that"
|
|
||||||
f" exceeded the max body size."
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.finish_json(
|
|
||||||
413,
|
|
||||||
{
|
|
||||||
"status": "error",
|
|
||||||
"error": "TOO LARGE",
|
|
||||||
"info": self.helper.translation.translate(
|
|
||||||
"error",
|
|
||||||
"fileTooLarge",
|
|
||||||
self.controller.users.get_user_lang_by_id(user_id),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.do_upload = True
|
|
||||||
|
|
||||||
if superuser:
|
|
||||||
exec_user_server_permissions = (
|
|
||||||
self.controller.server_perms.list_defined_permissions()
|
|
||||||
)
|
|
||||||
elif api_key is not None:
|
|
||||||
exec_user_server_permissions = (
|
|
||||||
self.controller.crafty_perms.get_api_key_permissions_list(api_key)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
exec_user_server_permissions = (
|
|
||||||
self.controller.crafty_perms.get_crafty_permissions_list(
|
|
||||||
exec_user["user_id"]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if user_id is None:
|
|
||||||
logger.warning("User ID not found in upload handler call")
|
|
||||||
Console.warning("User ID not found in upload handler call")
|
|
||||||
self.do_upload = False
|
|
||||||
|
|
||||||
if (
|
|
||||||
EnumPermissionsCrafty.SERVER_CREATION
|
|
||||||
not in exec_user_server_permissions
|
|
||||||
and not exec_user["superuser"]
|
|
||||||
):
|
|
||||||
logger.warning(
|
|
||||||
f"User {user_id} tried to upload a server" " without permissions!"
|
|
||||||
)
|
|
||||||
Console.warning(
|
|
||||||
f"User {user_id} tried to upload a server" " without permissions!"
|
|
||||||
)
|
|
||||||
self.do_upload = False
|
|
||||||
|
|
||||||
path = os.path.join(self.controller.project_root, "import", "upload")
|
|
||||||
self.helper.ensure_dir_exists(path)
|
|
||||||
# Delete existing files
|
|
||||||
if len(os.listdir(path)) > 0:
|
|
||||||
for item in os.listdir():
|
|
||||||
try:
|
|
||||||
os.remove(os.path.join(path, item))
|
|
||||||
except:
|
|
||||||
logger.debug("Could not delete file on user server upload")
|
|
||||||
|
|
||||||
self.helper.ensure_dir_exists(path)
|
|
||||||
filename = urllib.parse.unquote(
|
|
||||||
self.request.headers.get("X-FileName", None)
|
|
||||||
)
|
|
||||||
if not str(filename).endswith(".zip"):
|
|
||||||
WebSocketManager().broadcast("close_upload_box", "error")
|
|
||||||
self.finish("error")
|
|
||||||
full_path = os.path.join(path, filename)
|
|
||||||
|
|
||||||
if self.do_upload:
|
|
||||||
try:
|
|
||||||
self.f = open(full_path, "wb")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Upload failed with error: {e}")
|
|
||||||
self.do_upload = False
|
|
||||||
# If max_body_size is not set, you cannot upload files > 100MB
|
|
||||||
self.request.connection.set_max_body_size(max_streamed_size)
|
|
||||||
|
|
||||||
elif self.upload_type == "background":
|
|
||||||
superuser = exec_user["superuser"]
|
|
||||||
if api_key is not None:
|
|
||||||
superuser = superuser and api_key.full_access
|
|
||||||
user_id = exec_user["user_id"]
|
|
||||||
stream_size_value = self.helper.get_setting("stream_size_GB")
|
|
||||||
|
|
||||||
max_streamed_size = (1024 * 1024 * 1024) * stream_size_value
|
|
||||||
|
|
||||||
self.content_len = int(self.request.headers.get("Content-Length"))
|
|
||||||
if self.content_len > max_streamed_size:
|
|
||||||
logger.error(
|
|
||||||
f"User with ID {user_id} attempted to upload a file that"
|
|
||||||
f" exceeded the max body size."
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.finish_json(
|
|
||||||
413,
|
|
||||||
{
|
|
||||||
"status": "error",
|
|
||||||
"error": "TOO LARGE",
|
|
||||||
"info": self.helper.translation.translate(
|
|
||||||
"error",
|
|
||||||
"fileTooLarge",
|
|
||||||
self.controller.users.get_user_lang_by_id(user_id),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.do_upload = True
|
|
||||||
|
|
||||||
if not superuser:
|
|
||||||
return self.finish_json(
|
|
||||||
401,
|
|
||||||
{
|
|
||||||
"status": "error",
|
|
||||||
"error": "UNAUTHORIZED ACCESS",
|
|
||||||
"info": self.helper.translation.translate(
|
|
||||||
"error",
|
|
||||||
"superError",
|
|
||||||
self.controller.users.get_user_lang_by_id(user_id),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if not self.request.headers.get("X-Content-Type", None).startswith(
|
|
||||||
"image/"
|
|
||||||
):
|
|
||||||
return self.finish_json(
|
|
||||||
415,
|
|
||||||
{
|
|
||||||
"status": "error",
|
|
||||||
"error": "TYPE ERROR",
|
|
||||||
"info": self.helper.translation.translate(
|
|
||||||
"error",
|
|
||||||
"fileError",
|
|
||||||
self.controller.users.get_user_lang_by_id(user_id),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if user_id is None:
|
|
||||||
logger.warning("User ID not found in upload handler call")
|
|
||||||
Console.warning("User ID not found in upload handler call")
|
|
||||||
self.do_upload = False
|
|
||||||
|
|
||||||
path = os.path.join(
|
|
||||||
self.controller.project_root,
|
|
||||||
"app/frontend/static/assets/images/auth/custom",
|
|
||||||
)
|
|
||||||
filename = self.request.headers.get("X-FileName", None)
|
|
||||||
full_path = os.path.join(path, filename)
|
|
||||||
|
|
||||||
if self.do_upload:
|
|
||||||
try:
|
|
||||||
self.f = open(full_path, "wb")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Upload failed with error: {e}")
|
|
||||||
self.do_upload = False
|
|
||||||
# If max_body_size is not set, you cannot upload files > 100MB
|
|
||||||
self.request.connection.set_max_body_size(max_streamed_size)
|
|
||||||
else:
|
|
||||||
server_id = self.get_argument("server_id", None)
|
|
||||||
superuser = exec_user["superuser"]
|
|
||||||
if api_key is not None:
|
|
||||||
superuser = superuser and api_key.full_access
|
|
||||||
user_id = exec_user["user_id"]
|
|
||||||
stream_size_value = self.helper.get_setting("stream_size_GB")
|
|
||||||
|
|
||||||
max_streamed_size = (1024 * 1024 * 1024) * stream_size_value
|
|
||||||
|
|
||||||
self.content_len = int(self.request.headers.get("Content-Length"))
|
|
||||||
if self.content_len > max_streamed_size:
|
|
||||||
logger.error(
|
|
||||||
f"User with ID {user_id} attempted to upload a file that"
|
|
||||||
f" exceeded the max body size."
|
|
||||||
)
|
|
||||||
|
|
||||||
return self.finish_json(
|
|
||||||
413,
|
|
||||||
{
|
|
||||||
"status": "error",
|
|
||||||
"error": "TOO LARGE",
|
|
||||||
"info": self.helper.translation.translate(
|
|
||||||
"error",
|
|
||||||
"fileTooLarge",
|
|
||||||
self.controller.users.get_user_lang_by_id(user_id),
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
self.do_upload = True
|
|
||||||
|
|
||||||
if superuser:
|
|
||||||
exec_user_server_permissions = (
|
|
||||||
self.controller.server_perms.list_defined_permissions()
|
|
||||||
)
|
|
||||||
elif api_key is not None:
|
|
||||||
exec_user_server_permissions = (
|
|
||||||
self.controller.server_perms.get_api_key_permissions_list(
|
|
||||||
api_key, server_id
|
|
||||||
)
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
exec_user_server_permissions = (
|
|
||||||
self.controller.server_perms.get_user_id_permissions_list(
|
|
||||||
exec_user["user_id"], server_id
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
server_id = self.request.headers.get("X-ServerId", None)
|
|
||||||
if server_id is None:
|
|
||||||
logger.warning("Server ID not found in upload handler call")
|
|
||||||
Console.warning("Server ID not found in upload handler call")
|
|
||||||
self.do_upload = False
|
|
||||||
|
|
||||||
if user_id is None:
|
|
||||||
logger.warning("User ID not found in upload handler call")
|
|
||||||
Console.warning("User ID not found in upload handler call")
|
|
||||||
self.do_upload = False
|
|
||||||
|
|
||||||
if EnumPermissionsServer.FILES not in exec_user_server_permissions:
|
|
||||||
logger.warning(
|
|
||||||
f"User {user_id} tried to upload a file to "
|
|
||||||
f"{server_id} without permissions!"
|
|
||||||
)
|
|
||||||
Console.warning(
|
|
||||||
f"User {user_id} tried to upload a file to "
|
|
||||||
f"{server_id} without permissions!"
|
|
||||||
)
|
|
||||||
self.do_upload = False
|
|
||||||
|
|
||||||
path = self.request.headers.get("X-Path", None)
|
|
||||||
filename = self.request.headers.get("X-FileName", None)
|
|
||||||
full_path = os.path.join(path, filename)
|
|
||||||
|
|
||||||
if not self.helper.is_subdir(
|
|
||||||
full_path,
|
|
||||||
Helpers.get_os_understandable_path(
|
|
||||||
self.controller.servers.get_server_data_by_id(server_id)["path"]
|
|
||||||
),
|
|
||||||
):
|
|
||||||
logger.warning(
|
|
||||||
f"User {user_id} tried to upload a file to {server_id} "
|
|
||||||
f"but the path is not inside of the server!"
|
|
||||||
)
|
|
||||||
Console.warning(
|
|
||||||
f"User {user_id} tried to upload a file to {server_id} "
|
|
||||||
f"but the path is not inside of the server!"
|
|
||||||
)
|
|
||||||
self.do_upload = False
|
|
||||||
|
|
||||||
if self.do_upload:
|
|
||||||
try:
|
|
||||||
self.f = open(full_path, "wb")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"Upload failed with error: {e}")
|
|
||||||
self.do_upload = False
|
|
||||||
# If max_body_size is not set, you cannot upload files > 100MB
|
|
||||||
self.request.connection.set_max_body_size(max_streamed_size)
|
|
||||||
|
|
||||||
def post(self):
|
|
||||||
logger.info("Upload completed")
|
|
||||||
if self.upload_type == "server_files":
|
|
||||||
files_left = int(self.request.headers.get("X-Files-Left", None))
|
|
||||||
else:
|
|
||||||
files_left = 0
|
|
||||||
|
|
||||||
if self.do_upload:
|
|
||||||
time.sleep(5)
|
|
||||||
if files_left == 0:
|
|
||||||
WebSocketManager().broadcast("close_upload_box", "success")
|
|
||||||
self.finish("success") # Nope, I'm sending "success"
|
|
||||||
self.f.close()
|
|
||||||
else:
|
|
||||||
time.sleep(5)
|
|
||||||
if files_left == 0:
|
|
||||||
WebSocketManager().broadcast("close_upload_box", "error")
|
|
||||||
self.finish("error")
|
|
||||||
|
|
||||||
def data_received(self, chunk):
|
|
||||||
if self.do_upload:
|
|
||||||
self.f.write(chunk)
|
|
@ -12,6 +12,16 @@ nav.sidebar {
|
|||||||
position: fixed;
|
position: fixed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
-ms-overflow-style: none;
|
||||||
|
/* IE and Edge */
|
||||||
|
scrollbar-width: none;
|
||||||
|
/* Firefox */
|
||||||
|
}
|
||||||
|
|
||||||
|
td::-webkit-scrollbar {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
@media (min-width: 992px) {
|
@media (min-width: 992px) {
|
||||||
nav.sidebar {
|
nav.sidebar {
|
||||||
@ -271,3 +281,7 @@ div.warnings div.wssError a:hover {
|
|||||||
.dropdown-menu {
|
.dropdown-menu {
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.hidden-input {
|
||||||
|
margin-left: -40px;
|
||||||
|
}
|
208
app/frontend/static/assets/js/shared/upload.js
Normal file
208
app/frontend/static/assets/js/shared/upload.js
Normal file
@ -0,0 +1,208 @@
|
|||||||
|
async function uploadFile(type, file = null, path = null, file_num = 0, _onProgress = null) {
|
||||||
|
if (file == null) {
|
||||||
|
try {
|
||||||
|
file = $("#file")[0].files[0];
|
||||||
|
} catch {
|
||||||
|
bootbox.alert("Please select a file first.")
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
const fileId = uuidv4();
|
||||||
|
const token = getCookie("_xsrf");
|
||||||
|
if (type !== "server_upload") {
|
||||||
|
document.getElementById("upload_input").innerHTML = '<div class="progress" style="width: 100%;"><div id="upload-progress-bar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%"> <i class="fa-solid fa-spinner"></i></div></div>';
|
||||||
|
}
|
||||||
|
|
||||||
|
let url = ``
|
||||||
|
if (type === "server_upload") {
|
||||||
|
url = `/api/v2/servers/${serverId}/files/upload/`;
|
||||||
|
} else if (type === "background") {
|
||||||
|
url = `/api/v2/crafty/admin/upload/`
|
||||||
|
} else if (type === "import") {
|
||||||
|
url = `/api/v2/servers/import/upload/`
|
||||||
|
}
|
||||||
|
console.log(url)
|
||||||
|
const chunkSize = 1024 * 1024 * 10; // 10MB
|
||||||
|
const totalChunks = Math.ceil(file.size / chunkSize);
|
||||||
|
|
||||||
|
const uploadPromises = [];
|
||||||
|
let errors = []; // Array to store errors
|
||||||
|
try {
|
||||||
|
let res = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'X-XSRFToken': token,
|
||||||
|
'chunked': true,
|
||||||
|
'fileSize': file.size,
|
||||||
|
'type': type,
|
||||||
|
'totalChunks': totalChunks,
|
||||||
|
'fileName': file.name,
|
||||||
|
'location': path,
|
||||||
|
'fileId': fileId,
|
||||||
|
},
|
||||||
|
body: null,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!res.ok) {
|
||||||
|
let errorResponse = await res.json();
|
||||||
|
throw new Error(JSON.stringify(errorResponse));
|
||||||
|
}
|
||||||
|
|
||||||
|
let responseData = await res.json();
|
||||||
|
|
||||||
|
if (responseData.status !== "ok") {
|
||||||
|
throw new Error(JSON.stringify(responseData));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < totalChunks; i++) {
|
||||||
|
const start = i * chunkSize;
|
||||||
|
const end = Math.min(start + chunkSize, file.size);
|
||||||
|
const chunk = file.slice(start, end);
|
||||||
|
const chunk_hash = await calculateFileHash(chunk);
|
||||||
|
|
||||||
|
const uploadPromise = fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
body: chunk,
|
||||||
|
headers: {
|
||||||
|
'Content-Range': `bytes ${start}-${end - 1}/${file.size}`,
|
||||||
|
'Content-Length': chunk.size,
|
||||||
|
'fileSize': file.size,
|
||||||
|
'chunkHash': chunk_hash,
|
||||||
|
'chunked': true,
|
||||||
|
'type': type,
|
||||||
|
'totalChunks': totalChunks,
|
||||||
|
'fileName': file.name,
|
||||||
|
'location': path,
|
||||||
|
'fileId': fileId,
|
||||||
|
'chunkId': i,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then(async response => {
|
||||||
|
if (!response.ok) {
|
||||||
|
const errorData = await response.json();
|
||||||
|
throw new Error(JSON.stringify(errorData) || 'Unknown error occurred');
|
||||||
|
}
|
||||||
|
return response.json(); // Return the JSON data
|
||||||
|
})
|
||||||
|
.then(data => {
|
||||||
|
if (data.status !== "completed" && data.status !== "partial") {
|
||||||
|
throw new Error(data.message || 'Unknown error occurred');
|
||||||
|
}
|
||||||
|
// Update progress bar
|
||||||
|
const progress = (i + 1) / totalChunks * 100;
|
||||||
|
updateProgressBar(Math.round(progress), type, file_num);
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
errors.push(error); // Store the error
|
||||||
|
});
|
||||||
|
|
||||||
|
uploadPromises.push(uploadPromise);
|
||||||
|
}
|
||||||
|
|
||||||
|
await Promise.all(uploadPromises);
|
||||||
|
} catch (error) {
|
||||||
|
errors.push(error); // Store the error
|
||||||
|
}
|
||||||
|
|
||||||
|
if (errors.length > 0) {
|
||||||
|
const errorMessage = errors.map(error => JSON.parse(error.message).data.message || 'Unknown error occurred').join('<br>');
|
||||||
|
console.log(errorMessage)
|
||||||
|
bootbox.alert({
|
||||||
|
title: 'Error',
|
||||||
|
message: errorMessage,
|
||||||
|
callback: function () {
|
||||||
|
window.location.reload();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else if (type !== "server_upload") {
|
||||||
|
// All promises resolved successfully
|
||||||
|
$("#upload_input").html(`<div class="card-header header-sm d-flex justify-content-between align-items-center" style="width: 100%;"><input value="${file.name}" type="text" id="file-uploaded" disabled></input> 🔒</div>`);
|
||||||
|
if (type === "import") {
|
||||||
|
document.getElementById("lower_half").style.visibility = "visible";
|
||||||
|
document.getElementById("lower_half").hidden = false;
|
||||||
|
} else if (type === "background") {
|
||||||
|
setTimeout(function () {
|
||||||
|
location.href = `/panel/custom_login`;
|
||||||
|
}, 2000);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let caught = false;
|
||||||
|
let expanded = false;
|
||||||
|
try {
|
||||||
|
expanded = document.getElementById(path).classList.contains("clicked");
|
||||||
|
} catch { }
|
||||||
|
|
||||||
|
let par_el;
|
||||||
|
let items;
|
||||||
|
try {
|
||||||
|
par_el = document.getElementById(path + "ul");
|
||||||
|
items = par_el.children;
|
||||||
|
} catch (err) {
|
||||||
|
console.log(err);
|
||||||
|
caught = true;
|
||||||
|
par_el = document.getElementById("files-tree");
|
||||||
|
items = par_el.children;
|
||||||
|
}
|
||||||
|
|
||||||
|
let name = file.name;
|
||||||
|
let full_path = path + '/' + name;
|
||||||
|
let flag = false;
|
||||||
|
|
||||||
|
for (let item of items) {
|
||||||
|
if ($(item).attr("data-name") === name) {
|
||||||
|
flag = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!flag) {
|
||||||
|
if (caught && !expanded) {
|
||||||
|
$(par_el).append(`<li id="${full_path}li" class="d-block tree-ctx-item tree-file tree-item" data-path="${full_path}" data-name="${name}" onclick="clickOnFile(event)"><span style="margin-right: 6px;"><i class="far fa-file"></i></span>${name}</li>`);
|
||||||
|
} else if (expanded) {
|
||||||
|
$(par_el).append(`<li id="${full_path}li" class="tree-ctx-item tree-file tree-item" data-path="${full_path}" data-name="${name}" onclick="clickOnFile(event)"><span style="margin-right: 6px;"><i class="far fa-file"></i></span>${name}</li>`);
|
||||||
|
}
|
||||||
|
setTreeViewContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
$(`#upload-progress-bar-${file_num + 1}`).removeClass("progress-bar-striped");
|
||||||
|
$(`#upload-progress-bar-${file_num + 1}`).addClass("bg-success");
|
||||||
|
$(`#upload-progress-bar-${file_num + 1}`).html('<i style="color: black;" class="fas fa-box-check"></i>');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function calculateFileHash(file) {
|
||||||
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
|
||||||
|
return hashHex;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateProgressBar(progress, type, i) {
|
||||||
|
if (type !== "server_upload") {
|
||||||
|
if (progress === 100) {
|
||||||
|
$(`#upload-progress-bar`).removeClass("progress-bar-striped")
|
||||||
|
|
||||||
|
$(`#upload-progress-bar`).removeClass("progress-bar-animated")
|
||||||
|
}
|
||||||
|
$(`#upload-progress-bar`).css('width', progress + '%');
|
||||||
|
$(`#upload-progress-bar`).html(progress + '%');
|
||||||
|
} else {
|
||||||
|
if (progress === 100) {
|
||||||
|
$(`#upload-progress-bar-${i + 1}`).removeClass("progress-bar-striped")
|
||||||
|
|
||||||
|
$(`#upload-progress-bar-${i + 1}`).removeClass("progress-bar-animated")
|
||||||
|
}
|
||||||
|
$(`#upload-progress-bar-${i + 1}`).css('width', progress + '%');
|
||||||
|
$(`#upload-progress-bar-${i + 1}`).html(progress + '%');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function uuidv4() {
|
||||||
|
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
|
||||||
|
const r = Math.random() * 16 | 0,
|
||||||
|
v = c === 'x' ? r : (r & 0x3 | 0x8);
|
||||||
|
return v.toString(16);
|
||||||
|
});
|
||||||
|
}
|
@ -69,7 +69,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<button type="button" class="btn btn-info upload-button" id="upload-button"
|
<button type="button" class="btn btn-info upload-button" id="upload-button"
|
||||||
onclick="sendFile()" disabled>UPLOAD</button>
|
onclick="uploadFile('background')" disabled>UPLOAD</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -381,61 +381,6 @@
|
|||||||
}
|
}
|
||||||
img.src = src_path;
|
img.src = src_path;
|
||||||
}
|
}
|
||||||
|
|
||||||
var file;
|
|
||||||
function sendFile() {
|
|
||||||
file = $("#file")[0].files[0]
|
|
||||||
document.getElementById("upload_input").innerHTML = '<div class="progress" style="width: 100%"><div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%"> <i class="fa-solid fa-spinner"></i></div></div>';
|
|
||||||
let xmlHttpRequest = new XMLHttpRequest();
|
|
||||||
let token = getCookie("_xsrf")
|
|
||||||
let fileName = file.name
|
|
||||||
let target = '/upload'
|
|
||||||
let mimeType = file.type
|
|
||||||
let size = file.size
|
|
||||||
let type = 'background'
|
|
||||||
|
|
||||||
xmlHttpRequest.upload.addEventListener('progress', function (e) {
|
|
||||||
|
|
||||||
if (e.loaded <= size) {
|
|
||||||
var percent = Math.round(e.loaded / size * 100);
|
|
||||||
$(`#upload-progress-bar`).css('width', percent + '%');
|
|
||||||
$(`#upload-progress-bar`).html(percent + '%');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
xmlHttpRequest.open('POST', target, true);
|
|
||||||
xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType);
|
|
||||||
xmlHttpRequest.setRequestHeader('X-XSRFToken', token);
|
|
||||||
xmlHttpRequest.setRequestHeader('X-Content-Length', size);
|
|
||||||
xmlHttpRequest.setRequestHeader('X-Content-Disposition', 'attachment; filename="' + fileName + '"');
|
|
||||||
xmlHttpRequest.setRequestHeader('X-Content-Upload-Type', type);
|
|
||||||
xmlHttpRequest.setRequestHeader('X-FileName', fileName);
|
|
||||||
xmlHttpRequest.addEventListener('load', (event) => {
|
|
||||||
if (event.target.responseText == 'success') {
|
|
||||||
console.log('Upload for file', file.name, 'was successful!')
|
|
||||||
document.getElementById("upload_input").innerHTML = '<div class="card-header header-sm d-flex justify-content-between align-items-center" style="width: 100%"><span id="file-uploaded" style="color: gray;">' + fileName + '</span> 🔒</div>';
|
|
||||||
setTimeout(function () {
|
|
||||||
window.location.reload();
|
|
||||||
}, 2000);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let response_text = JSON.parse(event.target.responseText);
|
|
||||||
var x = document.querySelector('.bootbox');
|
|
||||||
console.log(JSON.parse(event.target.responseText).info)
|
|
||||||
bootbox.alert({
|
|
||||||
message: JSON.parse(event.target.responseText).info,
|
|
||||||
callback: function () {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
doUpload = false;
|
|
||||||
}
|
|
||||||
}, false);
|
|
||||||
xmlHttpRequest.addEventListener('error', (e) => {
|
|
||||||
console.error('Error while uploading file', file.name + '.', 'Event:', e)
|
|
||||||
}, false);
|
|
||||||
xmlHttpRequest.send(file);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
|
<script src="../../static/assets/js/shared/upload.js"></script>
|
||||||
{% end %}
|
{% end %}
|
@ -428,10 +428,13 @@
|
|||||||
if (responseData.status === "ok") {
|
if (responseData.status === "ok") {
|
||||||
window.location.href = "/panel/panel_config";
|
window.location.href = "/panel/panel_config";
|
||||||
} else {
|
} else {
|
||||||
|
let errordata = responseData.error;
|
||||||
|
if (responseData.error_data){
|
||||||
|
errordata = responseData.error
|
||||||
|
}
|
||||||
bootbox.alert({
|
bootbox.alert({
|
||||||
title: responseData.error,
|
title: responseData.error,
|
||||||
message: responseData.error_data
|
message: errordata
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -122,7 +122,7 @@ data['lang']) }}{% end %}
|
|||||||
name="lang" form="user_form">
|
name="lang" form="user_form">
|
||||||
{% for lang in data['languages'] %}
|
{% for lang in data['languages'] %}
|
||||||
{% if not 'incomplete' in lang %}
|
{% if not 'incomplete' in lang %}
|
||||||
<option value="{{lang}}">{{lang}}</option>
|
<option value="{{lang}}" >{{translate('language', lang, 'humanized_index')}}</option>
|
||||||
{% else %}
|
{% else %}
|
||||||
<option value="{{lang}}" disabled>{{lang}}</option>
|
<option value="{{lang}}" disabled>{{lang}}</option>
|
||||||
{% end %}
|
{% end %}
|
||||||
@ -393,6 +393,7 @@ data['lang']) }}{% end %}
|
|||||||
}
|
}
|
||||||
function replacer(key, value) {
|
function replacer(key, value) {
|
||||||
if (typeof value == "boolean" || key === "email" || key === "permissions" || key === "roles") {
|
if (typeof value == "boolean" || key === "email" || key === "permissions" || key === "roles") {
|
||||||
|
console.log(key)
|
||||||
return value
|
return value
|
||||||
} else {
|
} else {
|
||||||
console.log(key, value)
|
console.log(key, value)
|
||||||
@ -433,6 +434,7 @@ data['lang']) }}{% end %}
|
|||||||
let disabled_flag = false;
|
let disabled_flag = false;
|
||||||
let roles = null;
|
let roles = null;
|
||||||
if (superuser || userId != edit_id){
|
if (superuser || userId != edit_id){
|
||||||
|
console.log("ROLES")
|
||||||
roles = $('.role_check').map(function() {
|
roles = $('.role_check').map(function() {
|
||||||
if ($(this).attr("disabled")){
|
if ($(this).attr("disabled")){
|
||||||
disabled_flag = true;
|
disabled_flag = true;
|
||||||
@ -457,9 +459,7 @@ data['lang']) }}{% end %}
|
|||||||
delete formDataObject.username
|
delete formDataObject.username
|
||||||
}
|
}
|
||||||
if (superuser || userId != edit_id){
|
if (superuser || userId != edit_id){
|
||||||
if (!disabled_flag){
|
|
||||||
formDataObject.roles = roles;
|
formDataObject.roles = roles;
|
||||||
}
|
|
||||||
if ($("#permissions").length){
|
if ($("#permissions").length){
|
||||||
formDataObject.permissions = permissions;
|
formDataObject.permissions = permissions;
|
||||||
}
|
}
|
||||||
|
@ -39,207 +39,151 @@
|
|||||||
<span class="d-block d-sm-none">
|
<span class="d-block d-sm-none">
|
||||||
{% include "parts/m_server_controls_list.html %}
|
{% include "parts/m_server_controls_list.html %}
|
||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-md-6 col-sm-12">
|
<div class="col-md-12 col-sm-12" style="overflow-x:auto;">
|
||||||
<br>
|
<div class="card">
|
||||||
<br>
|
<div class="card-header header-sm d-flex justify-content-between align-items-center">
|
||||||
{% if data['backing_up'] %}
|
<h4 class="card-title"><i class="fa-regular fa-bell"></i> {{ translate('serverBackups', 'backups',
|
||||||
<div class="progress" style="height: 15px;">
|
data['lang']) }} </h4>
|
||||||
<div class="progress-bar progress-bar-striped progress-bar-animated" id="backup_progress_bar"
|
{% if data['user_data']['hints'] %}
|
||||||
role="progressbar" style="width:{{data['backup_stats']['percent']}}%;"
|
<span class="too_small" title="{{ translate('serverSchedules', 'cannotSee', data['lang']) }}" ,
|
||||||
aria-valuenow="{{data['backup_stats']['percent']}}" aria-valuemin="0" aria-valuemax="100">{{
|
data-content="{{ translate('serverSchedules', 'cannotSeeOnMobile', data['lang']) }}" ,
|
||||||
data['backup_stats']['percent'] }}%</div>
|
data-placement="bottom"></span>
|
||||||
</div>
|
|
||||||
<p>Backing up <i class="fas fa-spin fa-spinner"></i> <span
|
|
||||||
id="total_files">{{data['server_stats']['world_size']}}</span></p>
|
|
||||||
{% end %}
|
{% end %}
|
||||||
|
<div><a class="btn btn-info"
|
||||||
<br>
|
href="/panel/add_backup?id={{ data['server_stats']['server_id']['server_id'] }}"><i
|
||||||
{% if not data['backing_up'] %}
|
class="fas fa-plus-circle"></i> {{ translate('serverBackups', 'newBackup', data['lang']) }}</a>
|
||||||
<div id="backup_button" class="form-group">
|
</div>
|
||||||
<button class="btn btn-primary" id="backup_now_button">{{ translate('serverBackups', 'backupNow',
|
</div>
|
||||||
data['lang']) }}</button>
|
<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>
|
</div>
|
||||||
{% end %}
|
{% end %}
|
||||||
<form id="backup-form" class="forms-sample">
|
{% if len(data['backups']) > 0 %}
|
||||||
<div class="form-group">
|
<div class="d-none d-lg-block">
|
||||||
{% if data['super_user'] %}
|
<table class="table table-hover responsive-table" aria-label="backups list" id="backup_table"
|
||||||
<label for="server_name">{{ translate('serverBackups', 'storageLocation', data['lang']) }} <small
|
style="table-layout:fixed;">
|
||||||
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']) }}">
|
|
||||||
{% 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>
|
|
||||||
</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>
|
<thead>
|
||||||
<tr>
|
<tr class="rounded">
|
||||||
<th width="10%">{{ translate('serverBackups', 'options', data['lang']) }}</th>
|
<th scope="col" style="width: 15%; min-width: 10px;">{{ translate('serverBackups', 'name',
|
||||||
<th>{{ translate('serverBackups', 'path', data['lang']) }}</th>
|
data['lang']) }} </th>
|
||||||
<th width="20%">{{ translate('serverBackups', 'size', 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>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for backup in data['backup_list'] %}
|
{% for backup in data['backups'] %}
|
||||||
<tr>
|
<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>
|
<td>
|
||||||
<a href="/panel/download_backup?file={{ backup['path'] }}&id={{ data['server_stats']['server_id']['server_id'] }}"
|
<div id="{{backup.backup_id}}_status">
|
||||||
class="btn btn-primary">
|
<button class="btn btn-outline-success backup-status" data-status="{{ backup.status }}"
|
||||||
<i class="fas fa-download" aria-hidden="true"></i>
|
data-Standby="{{ translate('serverBackups', 'standby', data['lang'])}}"
|
||||||
{{ translate('serverBackups', 'download', data['lang']) }}
|
data-Failed="{{ translate('serverBackups', 'failed', data['lang'])}}"></button>
|
||||||
</a>
|
</div>
|
||||||
<br>
|
</td>
|
||||||
<br>
|
<td id="{{backup.backup_location}}" class="type">
|
||||||
<button data-file="{{ backup['path'] }}" data-backup_path="{{ data['backup_path'] }}"
|
<p style="overflow: scroll;" class="no-scroll">{{backup.backup_location}}</p>
|
||||||
class="btn btn-danger del_button">
|
</td>
|
||||||
<i class="fas fa-trash" aria-hidden="true"></i>
|
<td id="{{backup.max_backups}}" class="trigger" style="overflow: scroll; max-width: 30px;">
|
||||||
{{ translate('serverBackups', 'delete', data['lang']) }}
|
<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>
|
</button>
|
||||||
<button data-file="{{ backup['path'] }}" class="btn btn-warning restore_button">
|
{% if not backup.default %}
|
||||||
<i class="fas fa-undo-alt" aria-hidden="true"></i>
|
<button data-backup={{ backup.backup_id }} class="btn btn-danger del_button">
|
||||||
{{ translate('serverBackups', 'restore', data['lang']) }}
|
<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>
|
</button>
|
||||||
</td>
|
</td>
|
||||||
<td>{{ backup['path'] }}</td>
|
|
||||||
<td>{{ backup['size'] }}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
{% end %}
|
{% end %}
|
||||||
|
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="d-block d-lg-none">
|
||||||
</div>
|
<table aria-label="backups list" class="table table-hover responsive-table" id="backup_table_mini"
|
||||||
<div class="col-md-12 col-sm-12">
|
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>
|
<br>
|
||||||
<br>
|
<div id="{{backup.backup_id}}_status">
|
||||||
<div class="card-header header-sm d-flex justify-content-between align-items-center">
|
<button class="btn btn-outline-success backup-status" data-status="{{ backup.status }}"
|
||||||
<h4 class="card-title"><i class="fas fa-server"></i> {{ translate('serverBackups', 'excludedBackups',
|
data-Standby="{{ translate('serverBackups', 'standby', data['lang'])}}"
|
||||||
data['lang']) }} <small class="text-muted ml-1"></small> </h4>
|
data-Failed="{{ translate('serverBackups', 'failed', data['lang'])}}"></button>
|
||||||
</div>
|
</div>
|
||||||
<br>
|
<br>
|
||||||
<ul>
|
{% if backup.default %}
|
||||||
{% for item in data['exclusions'] %}
|
<span class="badge-pill badge-outline-warning">{{ translate('serverBackups', 'default',
|
||||||
<li>{{item}}</li>
|
data['lang']) }}</span><small><button class="badge-pill badge-outline-info backup-explain"
|
||||||
<br>
|
data-explain="{{ translate('serverBackups', 'defaultExplain', data['lang'])}}"><i
|
||||||
|
class="fa-solid fa-question"></i></button></small>
|
||||||
{% end %}
|
{% end %}
|
||||||
</ul>
|
</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>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -298,7 +242,7 @@
|
|||||||
{% block js %}
|
{% block js %}
|
||||||
<script>
|
<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
|
//used to get cookies from browser - this is part of tornados xsrf protection - it's for extra security
|
||||||
@ -307,9 +251,10 @@
|
|||||||
return r ? r[1] : undefined;
|
return r ? r[1] : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function backup_started() {
|
async function backup_started(backup_id) {
|
||||||
const token = getCookie("_xsrf")
|
const token = getCookie("_xsrf")
|
||||||
let res = await fetch(`/api/v2/servers/${server_id}/action/backup_server`, {
|
console.log(backup_id)
|
||||||
|
let res = await fetch(`/api/v2/servers/${serverId}/action/backup_server/${backup_id}/`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-XSRFToken': token
|
'X-XSRFToken': token
|
||||||
@ -318,14 +263,7 @@
|
|||||||
let responseData = await res.json();
|
let responseData = await res.json();
|
||||||
if (responseData.status === "ok") {
|
if (responseData.status === "ok") {
|
||||||
console.log(responseData);
|
console.log(responseData);
|
||||||
$("#backup_button").html(`<div class="progress" style="height: 15px;">
|
$("#backup_button").prop('disabled', true)
|
||||||
<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 {
|
} else {
|
||||||
|
|
||||||
bootbox.alert({
|
bootbox.alert({
|
||||||
@ -335,155 +273,83 @@
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
async function del_backup(filename, id) {
|
async function del_backup(backup_id) {
|
||||||
const token = getCookie("_xsrf")
|
const token = getCookie("_xsrf")
|
||||||
let contents = JSON.stringify({"filename": filename})
|
let res = await fetch(`/api/v2/servers/${serverId}/backups/backup/${backup_id}`, {
|
||||||
let res = await fetch(`/api/v2/servers/${id}/backups/backup/`, {
|
|
||||||
method: 'DELETE',
|
method: 'DELETE',
|
||||||
headers: {
|
headers: {
|
||||||
'token': token,
|
'token': token,
|
||||||
},
|
},
|
||||||
body: contents
|
body: {}
|
||||||
});
|
});
|
||||||
let responseData = await res.json();
|
let responseData = await res.json();
|
||||||
if (responseData.status === "ok") {
|
if (responseData.status === "ok") {
|
||||||
window.location.reload();
|
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 {
|
} else {
|
||||||
$("#backup_before").css("display", "none");
|
bootbox.alert({
|
||||||
$("#backup_before").val("");
|
"title": responseData.status,
|
||||||
}
|
"message": responseData.error
|
||||||
});
|
})
|
||||||
$("#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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function () {
|
$(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!");
|
console.log("ready!");
|
||||||
$("#backup_config_box").hide();
|
$(".backup-explain").on("click", function () {
|
||||||
$("#backup_save_note").hide();
|
bootbox.alert($(this).data("explain"));
|
||||||
|
|
||||||
$("#show_config").click(function () {
|
|
||||||
$("#backup_config_box").toggle();
|
|
||||||
$('#backup_button').hide();
|
|
||||||
$('#backup_save_note').show();
|
|
||||||
$('#backup_data').hide();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(".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({
|
$('#backup_table').DataTable({
|
||||||
"order": [[1, "desc"]],
|
"order": [[1, "desc"]],
|
||||||
"paging": false,
|
"paging": false,
|
||||||
@ -491,11 +357,12 @@
|
|||||||
"searching": true,
|
"searching": true,
|
||||||
"ordering": true,
|
"ordering": true,
|
||||||
"info": true,
|
"info": true,
|
||||||
"autoWidth": false,
|
"autoWidth": true,
|
||||||
"responsive": true,
|
"responsive": false,
|
||||||
});
|
});
|
||||||
|
|
||||||
$(".del_button").click(function () {
|
$(".del_button").click(function () {
|
||||||
|
let backup = $(this).data('backup');
|
||||||
var file_to_del = $(this).data("file");
|
var file_to_del = $(this).data("file");
|
||||||
var backup_path = $(this).data('backup_path');
|
var backup_path = $(this).data('backup_path');
|
||||||
|
|
||||||
@ -515,8 +382,8 @@
|
|||||||
callback: function (result) {
|
callback: function (result) {
|
||||||
console.log(result);
|
console.log(result);
|
||||||
if (result == true) {
|
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) {
|
callback: function (result) {
|
||||||
console.log(result);
|
console.log(result);
|
||||||
if (result == true) {
|
if (result == true) {
|
||||||
restore_backup(file_to_restore, server_id);
|
restore_backup(file_to_restore, serverId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
$("#backup_now_button").click(function () {
|
$(".backup_now_button").click(function () {
|
||||||
backup_started();
|
backup_started($(this).data('backup'));
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
@ -591,40 +458,25 @@
|
|||||||
bootbox.alert("You must input a path before selecting this button");
|
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");
|
let path = event.target.parentElement.getAttribute("data-path");
|
||||||
if (document.getElementById(path).classList.contains('clicked')) {
|
if (document.getElementById(path).classList.contains('clicked')) {
|
||||||
return;
|
return;
|
||||||
}else{
|
} else {
|
||||||
getTreeView(path);
|
getTreeView(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
async function getTreeView(path){
|
async function getTreeView(path) {
|
||||||
console.log(path)
|
console.log(path)
|
||||||
const token = getCookie("_xsrf");
|
const token = getCookie("_xsrf");
|
||||||
let res = await fetch(`/api/v2/servers/${server_id}/files`, {
|
let res = await fetch(`/api/v2/servers/${serverId}/files`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'X-XSRFToken': token
|
'X-XSRFToken': token
|
||||||
},
|
},
|
||||||
body: JSON.stringify({"page": "backups", "path": path}),
|
body: JSON.stringify({ "page": "backups", "path": path }),
|
||||||
});
|
});
|
||||||
let responseData = await res.json();
|
let responseData = await res.json();
|
||||||
if (responseData.status === "ok") {
|
if (responseData.status === "ok") {
|
||||||
@ -644,17 +496,17 @@
|
|||||||
let path = response.data.root_path.path;
|
let path = response.data.root_path.path;
|
||||||
let text = `<ul class="tree-nested d-block" id="${path}ul">`;
|
let text = `<ul class="tree-nested d-block" id="${path}ul">`;
|
||||||
Object.entries(response.data).forEach(([key, value]) => {
|
Object.entries(response.data).forEach(([key, value]) => {
|
||||||
if (key === "root_path" || key === "db_stats"){
|
if (key === "root_path" || key === "db_stats") {
|
||||||
//continue is not valid in for each. Return acts as a continue.
|
//continue is not valid in for each. Return acts as a continue.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let checked = ""
|
let checked = ""
|
||||||
let dpath = value.path;
|
let dpath = value.path;
|
||||||
let filename = key;
|
let filename = key;
|
||||||
if (value.excluded){
|
if (value.excluded) {
|
||||||
checked = "checked"
|
checked = "checked"
|
||||||
}
|
}
|
||||||
if (value.dir){
|
if (value.dir) {
|
||||||
text += `<li class="tree-item" data-path="${dpath}">
|
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">
|
\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}>
|
<input type="checkbox" class="checkBoxClass excluded" value="${dpath}" ${checked}>
|
||||||
@ -664,7 +516,7 @@
|
|||||||
<strong>${filename}</strong>
|
<strong>${filename}</strong>
|
||||||
</span>
|
</span>
|
||||||
</input></div><li>`
|
</input></div><li>`
|
||||||
}else{
|
} else {
|
||||||
text += `<li
|
text += `<li
|
||||||
class="d-block tree-ctx-item tree-file"
|
class="d-block tree-ctx-item tree-file"
|
||||||
data-path="${dpath}"
|
data-path="${dpath}"
|
||||||
@ -674,14 +526,14 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
text += `</ul>`;
|
text += `</ul>`;
|
||||||
if(response.data.root_path.top){
|
if (response.data.root_path.top) {
|
||||||
try {
|
try {
|
||||||
document.getElementById('main-tree-div').innerHTML += text;
|
document.getElementById('main-tree-div').innerHTML += text;
|
||||||
document.getElementById('main-tree').parentElement.classList.add("clicked");
|
document.getElementById('main-tree').parentElement.classList.add("clicked");
|
||||||
} catch {
|
} catch {
|
||||||
document.getElementById('files-tree').innerHTML = text;
|
document.getElementById('files-tree').innerHTML = text;
|
||||||
}
|
}
|
||||||
}else{
|
} else {
|
||||||
try {
|
try {
|
||||||
document.getElementById(path + "span").classList.add('tree-caret-down');
|
document.getElementById(path + "span").classList.add('tree-caret-down');
|
||||||
document.getElementById(path).innerHTML += text;
|
document.getElementById(path).innerHTML += text;
|
||||||
|
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 %}
|
@ -67,7 +67,8 @@
|
|||||||
translate('serverFiles', 'download', data['lang']) }}</a>
|
translate('serverFiles', 'download', data['lang']) }}</a>
|
||||||
<a onclick="deleteFileE(event)" href="javascript:void(0)" id="deleteFile" href="#"
|
<a onclick="deleteFileE(event)" href="javascript:void(0)" id="deleteFile" href="#"
|
||||||
style="color: red">{{ translate('serverFiles', 'delete', data['lang']) }}</a>
|
style="color: red">{{ translate('serverFiles', 'delete', data['lang']) }}</a>
|
||||||
<a onclick="deleteFileE(event)" href="javascript:void(0)" id="deleteDir" href="#" style="color: red">{{
|
<a onclick="deleteFileE(event)" href="javascript:void(0)" id="deleteDir" href="#"
|
||||||
|
style="color: red">{{
|
||||||
translate('serverFiles', 'delete', data['lang']) }}</a>
|
translate('serverFiles', 'delete', data['lang']) }}</a>
|
||||||
<a href="javascript:void(0)" class="closebtn" style="color: var(--info);"
|
<a href="javascript:void(0)" class="closebtn" style="color: var(--info);"
|
||||||
onclick="document.getElementById('files-tree-nav').style.display = 'none';">{{
|
onclick="document.getElementById('files-tree-nav').style.display = 'none';">{{
|
||||||
@ -156,7 +157,8 @@
|
|||||||
right: 35px;
|
right: 35px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tree-file:hover{
|
|
||||||
|
.tree-file:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
@ -721,105 +723,7 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendFile(file, path, serverId, left, i, onProgress) {
|
|
||||||
let xmlHttpRequest = new XMLHttpRequest();
|
|
||||||
let token = getCookie("_xsrf")
|
|
||||||
let fileName = file.name
|
|
||||||
let target = '/upload?server_id=' + serverId
|
|
||||||
let mimeType = file.type
|
|
||||||
let size = file.size
|
|
||||||
|
|
||||||
xmlHttpRequest.upload.addEventListener('progress', function (e) {
|
|
||||||
|
|
||||||
if (e.loaded <= size) {
|
|
||||||
var percent = Math.round(e.loaded / size * 100);
|
|
||||||
$(`#upload-progress-bar-${i + 1}`).css('width', percent + '%');
|
|
||||||
$(`#upload-progress-bar-${i + 1}`).html(percent + '%');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
xmlHttpRequest.open('POST', target, true);
|
|
||||||
xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType);
|
|
||||||
xmlHttpRequest.setRequestHeader('X-XSRFToken', token);
|
|
||||||
xmlHttpRequest.setRequestHeader('X-Content-Length', size);
|
|
||||||
xmlHttpRequest.setRequestHeader('X-Content-Disposition', 'attachment; filename="' + fileName + '"');
|
|
||||||
xmlHttpRequest.setRequestHeader('X-Path', path);
|
|
||||||
xmlHttpRequest.setRequestHeader('X-Content-Upload-Type', 'server_files')
|
|
||||||
xmlHttpRequest.setRequestHeader('X-Files-Left', left);
|
|
||||||
xmlHttpRequest.setRequestHeader('X-FileName', fileName);
|
|
||||||
xmlHttpRequest.setRequestHeader('X-ServerId', serverId);
|
|
||||||
xmlHttpRequest.upload.addEventListener('progress', (event) =>
|
|
||||||
onProgress(Math.floor(event.loaded / event.total * 100)), false);
|
|
||||||
xmlHttpRequest.addEventListener('load', (event) => {
|
|
||||||
if (event.target.responseText == 'success') {
|
|
||||||
console.log('Upload for file', file.name, 'was successful!');
|
|
||||||
let caught = false;
|
|
||||||
try {
|
|
||||||
if (document.getElementById(path).classList.contains("clicked")) {
|
|
||||||
var expanded = true;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
var expanded = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
var par_el = document.getElementById(path + "ul");
|
|
||||||
var items = par_el.children;
|
|
||||||
} catch (err) {
|
|
||||||
console.log(err)
|
|
||||||
caught = true;
|
|
||||||
var par_el = document.getElementById("files-tree");
|
|
||||||
var items = par_el.children;
|
|
||||||
}
|
|
||||||
let name = file.name;
|
|
||||||
console.log(par_el)
|
|
||||||
let full_path = path + '/' + name
|
|
||||||
let flag = false;
|
|
||||||
for (var k = 0; k < items.length; ++k) {
|
|
||||||
if ($(items[k]).attr("data-name") == name) {
|
|
||||||
flag = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!flag) {
|
|
||||||
if (caught && expanded == false) {
|
|
||||||
$(par_el).append('<li id=' + '"' + full_path.toString() + 'li' + '"' + 'class="d-block tree-ctx-item tree-file tree-item" data-path=' + '"' + full_path.toString() + '"' + ' data-name=' + '"' + name.toString() + '"' + ' onclick="clickOnFile(event)" ><span style="margin-right: 6px;"><i class="far fa-file"></i></span>' + name + '</li>');
|
|
||||||
} else if (expanded == true) {
|
|
||||||
$(par_el).append('<li id=' + '"' + full_path.toString() + 'li' + '"' + 'class="tree-ctx-item tree-file tree-item" data-path=' + '"' + full_path.toString() + '"' + ' data-name=' + '"' + name.toString() + '"' + ' onclick="clickOnFile(event)" ><span style="margin-right: 6px;"><i class="far fa-file"></i></span>' + name + '</li>');
|
|
||||||
}
|
|
||||||
setTreeViewContext();
|
|
||||||
}
|
|
||||||
$(`#upload-progress-bar-${i + 1}`).removeClass("progress-bar-striped");
|
|
||||||
$(`#upload-progress-bar-${i + 1}`).addClass("bg-success");
|
|
||||||
$(`#upload-progress-bar-${i + 1}`).html('<i style="color: black;" class="fas fa-box-check"></i>')
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let response_text = JSON.parse(event.target.responseText);
|
|
||||||
var x = document.querySelector('.bootbox');
|
|
||||||
if (x) {
|
|
||||||
x.remove()
|
|
||||||
}
|
|
||||||
var x = document.querySelector('.modal-content');
|
|
||||||
if (x) {
|
|
||||||
x.remove()
|
|
||||||
}
|
|
||||||
console.log(JSON.parse(event.target.responseText).info)
|
|
||||||
bootbox.alert({
|
|
||||||
message: JSON.parse(event.target.responseText).info,
|
|
||||||
callback: function () {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
doUpload = false;
|
|
||||||
}
|
|
||||||
}, false);
|
|
||||||
xmlHttpRequest.addEventListener('error', (e) => {
|
|
||||||
console.error('Error while uploading file', file.name + '.', 'Event:', e)
|
|
||||||
}, false);
|
|
||||||
xmlHttpRequest.send(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
let uploadWaitDialog;
|
let uploadWaitDialog;
|
||||||
let doUpload = true;
|
|
||||||
|
|
||||||
async function uploadFilesE(event) {
|
async function uploadFilesE(event) {
|
||||||
path = event.target.parentElement.getAttribute('data-path');
|
path = event.target.parentElement.getAttribute('data-path');
|
||||||
@ -842,6 +746,9 @@
|
|||||||
label: "{{ translate('serverFiles', 'upload', data['lang']) }}",
|
label: "{{ translate('serverFiles', 'upload', data['lang']) }}",
|
||||||
className: "btn-default",
|
className: "btn-default",
|
||||||
callback: async function () {
|
callback: async function () {
|
||||||
|
if ($("#files").get(0).files.length === 0) {
|
||||||
|
return hideUploadBox();
|
||||||
|
}
|
||||||
var height = files.files.length * 50;
|
var height = files.files.length * 50;
|
||||||
|
|
||||||
var waitMessage = '<p class="text-center mb-0">' +
|
var waitMessage = '<p class="text-center mb-0">' +
|
||||||
@ -858,16 +765,13 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
let nFiles = files.files.length;
|
let nFiles = files.files.length;
|
||||||
for (i = 0; i < nFiles; i++) {
|
const uploadPromises = [];
|
||||||
if (!doUpload) {
|
|
||||||
doUpload = true;
|
|
||||||
hideUploadBox();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
for (let i = 0; i < nFiles; i++) {
|
||||||
|
const file = files.files[i];
|
||||||
const progressHtml = `
|
const progressHtml = `
|
||||||
<div style="width: 100%; min-width: 100%;">
|
<div style="width: 100%; min-width: 100%;">
|
||||||
${files.files[i].name}:
|
${file.name}:
|
||||||
<br><div
|
<br><div
|
||||||
id="upload-progress-bar-${i + 1}"
|
id="upload-progress-bar-${i + 1}"
|
||||||
class="progress-bar progress-bar-striped progress-bar-animated"
|
class="progress-bar progress-bar-striped progress-bar-animated"
|
||||||
@ -879,33 +783,38 @@
|
|||||||
></div>
|
></div>
|
||||||
</div><br>
|
</div><br>
|
||||||
`;
|
`;
|
||||||
|
|
||||||
$('#upload-progress-bar-parent').append(progressHtml);
|
$('#upload-progress-bar-parent').append(progressHtml);
|
||||||
|
|
||||||
await sendFile(files.files[i], path, serverId, nFiles - i - 1, i, (progress) => {
|
const uploadPromise = uploadFile("server_upload", file, path, i, (progress) => {
|
||||||
$(`#upload-progress-bar-${i + 1}`).attr('aria-valuenow', progress)
|
$(`#upload-progress-bar-${i + 1}`).attr('aria-valuenow', progress)
|
||||||
$(`#upload-progress-bar-${i + 1}`).css('width', progress + '%');
|
$(`#upload-progress-bar-${i + 1}`).css('width', progress + '%');
|
||||||
});
|
});
|
||||||
|
uploadPromises.push(uploadPromise);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
await Promise.all(uploadPromises);
|
||||||
|
setTimeout(() => {
|
||||||
hideUploadBox();
|
hideUploadBox();
|
||||||
//$('#upload_file').submit(); //.trigger('submit');
|
}, 2000);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
var fileList = document.getElementById("files");
|
|
||||||
fileList.addEventListener("change", function (e) {
|
|
||||||
var list = "";
|
|
||||||
let files = Array.from(this.files)
|
|
||||||
files.forEach(file => {
|
|
||||||
list += "<li class='col-xs-12 file-list'>" + file.name + "</li>"
|
|
||||||
})
|
|
||||||
|
|
||||||
document.getElementById("fileList").innerHTML = list;
|
|
||||||
}, false);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function calculateFileHash(file) {
|
||||||
|
const arrayBuffer = await file.arrayBuffer();
|
||||||
|
const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
|
||||||
|
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||||
|
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
|
||||||
|
|
||||||
|
return hashHex;
|
||||||
|
}
|
||||||
|
|
||||||
function getDirView(event) {
|
function getDirView(event) {
|
||||||
let path = event.target.parentElement.getAttribute("data-path");
|
let path = event.target.parentElement.getAttribute("data-path");
|
||||||
if (document.getElementById(path).classList.contains('clicked')) {
|
if (document.getElementById(path).classList.contains('clicked')) {
|
||||||
@ -1211,5 +1120,5 @@
|
|||||||
|
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
|
<script src="../../static/assets/js/shared/upload.js"></script>
|
||||||
{% end %}
|
{% end %}
|
@ -79,6 +79,24 @@
|
|||||||
<option id="command" value="command">{{ translate('serverScheduleConfig', 'custom' , data['lang'])
|
<option id="command" value="command">{{ translate('serverScheduleConfig', 'custom' , data['lang'])
|
||||||
}}</option>
|
}}</option>
|
||||||
</select>
|
</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>
|
||||||
<div id="ifBasic">
|
<div id="ifBasic">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
@ -232,7 +250,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function replacer(key, value) {
|
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") {
|
if (typeof value == "boolean") {
|
||||||
return value
|
return value
|
||||||
}
|
}
|
||||||
@ -247,7 +265,7 @@
|
|||||||
}
|
}
|
||||||
} else if (value === "" && key == "start_time"){
|
} else if (value === "" && key == "start_time"){
|
||||||
return "00:00";
|
return "00:00";
|
||||||
}else{
|
}else {
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -281,6 +299,11 @@
|
|||||||
// Format the plain form data as JSON
|
// Format the plain form data as JSON
|
||||||
let formDataJsonString = JSON.stringify(formDataObject, replacer);
|
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/`, {
|
let res = await fetch(`/api/v2/servers/${serverId}/tasks/`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
@ -358,6 +381,14 @@
|
|||||||
document.getElementById("ifYes").style.display = "none";
|
document.getElementById("ifYes").style.display = "none";
|
||||||
document.getElementById("command_input").required = false;
|
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() {
|
function basicAdvanced() {
|
||||||
if (document.getElementById('difficulty').value == "advanced") {
|
if (document.getElementById('difficulty').value == "advanced") {
|
||||||
|
@ -301,8 +301,8 @@
|
|||||||
'labelZipFile', data['lang']) }}</label>
|
'labelZipFile', data['lang']) }}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<button type="button" class="btn btn-info upload-button" id="upload-button" onclick="sendFile()"
|
<button type="button" class="btn btn-info upload-button" id="upload-button"
|
||||||
disabled>{{ translate('serverWizard',
|
onclick="uploadFile('import')" disabled>{{ translate('serverWizard',
|
||||||
'uploadButton', data['lang']) }}</button>
|
'uploadButton', data['lang']) }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -495,61 +495,8 @@
|
|||||||
{% end %}
|
{% end %}
|
||||||
|
|
||||||
{% block js%}
|
{% block js%}
|
||||||
|
<script src="../../static/assets/js/shared/upload.js"></script>
|
||||||
<script>
|
<script>
|
||||||
var upload;
|
|
||||||
var file;
|
|
||||||
function sendFile() {
|
|
||||||
file = $("#file")[0].files[0]
|
|
||||||
document.getElementById("upload_input").innerHTML = '<div class="progress" style="width: 100%;"><div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%"> <i class="fa-solid fa-spinner"></i></div></div>'
|
|
||||||
let xmlHttpRequest = new XMLHttpRequest();
|
|
||||||
let token = getCookie("_xsrf")
|
|
||||||
let fileName = encodeURIComponent(file.name)
|
|
||||||
let target = '/upload'
|
|
||||||
let mimeType = file.type
|
|
||||||
let size = file.size
|
|
||||||
let type = 'server_import'
|
|
||||||
|
|
||||||
xmlHttpRequest.upload.addEventListener('progress', function (e) {
|
|
||||||
|
|
||||||
if (e.loaded <= size) {
|
|
||||||
var percent = Math.round(e.loaded / size * 100);
|
|
||||||
$(`#upload-progress-bar`).css('width', percent + '%');
|
|
||||||
$(`#upload-progress-bar`).html(percent + '%');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
xmlHttpRequest.open('POST', target, true);
|
|
||||||
xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType);
|
|
||||||
xmlHttpRequest.setRequestHeader('X-XSRFToken', token);
|
|
||||||
xmlHttpRequest.setRequestHeader('X-Content-Length', size);
|
|
||||||
xmlHttpRequest.setRequestHeader('X-Content-Disposition', 'attachment; filename="' + fileName + '"');
|
|
||||||
xmlHttpRequest.setRequestHeader('X-Content-Upload-Type', type);
|
|
||||||
xmlHttpRequest.setRequestHeader('X-FileName', fileName);
|
|
||||||
xmlHttpRequest.addEventListener('load', (event) => {
|
|
||||||
if (event.target.responseText == 'success') {
|
|
||||||
console.log('Upload for file', file.name, 'was successful!')
|
|
||||||
$("#upload_input").html(`<div class="card-header header-sm d-flex justify-content-between align-items-center" style="width: 100%;"><input value="${decodeURIComponent(fileName)}" type="text" id="file-uploaded" disabled></input> 🔒</div>`);
|
|
||||||
document.getElementById("lower_half").style.visibility = "visible";
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
let response_text = JSON.parse(event.target.responseText);
|
|
||||||
var x = document.querySelector('.bootbox');
|
|
||||||
console.log(JSON.parse(event.target.responseText).info)
|
|
||||||
bootbox.alert({
|
|
||||||
message: JSON.parse(event.target.responseText).info,
|
|
||||||
callback: function () {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
doUpload = false;
|
|
||||||
}
|
|
||||||
}, false);
|
|
||||||
xmlHttpRequest.addEventListener('error', (e) => {
|
|
||||||
console.error('Error while uploading file', file.name + '.', 'Event:', e)
|
|
||||||
}, false);
|
|
||||||
xmlHttpRequest.send(file);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.getElementById("root_upload_button").addEventListener("click", function (event) {
|
document.getElementById("root_upload_button").addEventListener("click", function (event) {
|
||||||
if (file) {
|
if (file) {
|
||||||
upload = true;
|
upload = true;
|
||||||
|
@ -494,8 +494,8 @@
|
|||||||
'labelZipFile', data['lang']) }}</label>
|
'labelZipFile', data['lang']) }}</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="input-group-append">
|
<div class="input-group-append">
|
||||||
<button type="button" class="btn btn-info upload-button" id="upload-button" onclick="sendFile()"
|
<button type="button" class="btn btn-info upload-button" id="upload-button"
|
||||||
disabled>{{ translate('serverWizard',
|
onclick="uploadFile('import')" disabled>{{ translate('serverWizard',
|
||||||
'uploadButton', data['lang']) }}</button>
|
'uploadButton', data['lang']) }}</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -784,6 +784,7 @@
|
|||||||
{% end %}
|
{% end %}
|
||||||
|
|
||||||
{% block js %}
|
{% block js %}
|
||||||
|
<script src="../../static/assets/js/shared/upload.js"></script>
|
||||||
<script>
|
<script>
|
||||||
document.getElementById("root_files_button").addEventListener("click", function (event) {
|
document.getElementById("root_files_button").addEventListener("click", function (event) {
|
||||||
if (document.forms["zip"]["server_path"].value != "") {
|
if (document.forms["zip"]["server_path"].value != "") {
|
||||||
@ -821,55 +822,7 @@
|
|||||||
});
|
});
|
||||||
var upload = false;
|
var upload = false;
|
||||||
var file;
|
var file;
|
||||||
function sendFile() {
|
|
||||||
file = $("#file")[0].files[0]
|
|
||||||
document.getElementById("upload_input").innerHTML = '<div class="progress" style="width: 100%;"><div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%"> <i class="fa-solid fa-spinner"></i></div></div>'
|
|
||||||
let xmlHttpRequest = new XMLHttpRequest();
|
|
||||||
let token = getCookie("_xsrf")
|
|
||||||
let fileName = file.name
|
|
||||||
let target = '/upload'
|
|
||||||
let mimeType = file.type
|
|
||||||
let size = file.size
|
|
||||||
let type = 'server_import'
|
|
||||||
|
|
||||||
xmlHttpRequest.upload.addEventListener('progress', function (e) {
|
|
||||||
|
|
||||||
if (e.loaded <= size) {
|
|
||||||
var percent = Math.round(e.loaded / size * 100);
|
|
||||||
$(`#upload-progress-bar`).css('width', percent + '%');
|
|
||||||
$(`#upload-progress-bar`).html(percent + '%');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
xmlHttpRequest.open('POST', target, true);
|
|
||||||
xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType);
|
|
||||||
xmlHttpRequest.setRequestHeader('X-XSRFToken', token);
|
|
||||||
xmlHttpRequest.setRequestHeader('X-Content-Length', size);
|
|
||||||
xmlHttpRequest.setRequestHeader('X-Content-Disposition', 'attachment; filename="' + fileName + '"');
|
|
||||||
xmlHttpRequest.setRequestHeader('X-Content-Upload-Type', type);
|
|
||||||
xmlHttpRequest.setRequestHeader('X-FileName', fileName);
|
|
||||||
xmlHttpRequest.addEventListener('load', (event) => {
|
|
||||||
if (event.target.responseText == 'success') {
|
|
||||||
console.log('Upload for file', file.name, 'was successful!')
|
|
||||||
$("#upload_input").html(`<div class="card-header header-sm d-flex justify-content-between align-items-center" style="width: 100%;"><input value="${fileName}" type="text" id="file-uploaded" disabled></input> 🔒</div>`);
|
|
||||||
document.getElementById("lower_half").style.visibility = "visible";
|
|
||||||
document.getElementById("lower_half").hidden = false;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
console.log(JSON.parse(event.target.responseText).info)
|
|
||||||
bootbox.alert({
|
|
||||||
message: JSON.parse(event.target.responseText).info,
|
|
||||||
callback: function () {
|
|
||||||
window.location.reload();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, false);
|
|
||||||
xmlHttpRequest.addEventListener('error', (e) => {
|
|
||||||
console.error('Error while uploading file', file.name + '.', 'Event:', e)
|
|
||||||
}, false);
|
|
||||||
xmlHttpRequest.send(file);
|
|
||||||
}
|
|
||||||
</script>
|
</script>
|
||||||
<script type="text/javascript" src="../../static/assets/js/shared/root-dir.js"></script>
|
<script type="text/javascript" src="../../static/assets/js/shared/root-dir.js"></script>
|
||||||
|
|
||||||
|
@ -1,10 +1,11 @@
|
|||||||
import peewee
|
import peewee
|
||||||
import datetime
|
import datetime
|
||||||
|
from app.classes.shared.helpers import Helpers
|
||||||
|
|
||||||
|
|
||||||
def migrate(migrator, database, **kwargs):
|
def migrate(migrator, database, **kwargs):
|
||||||
migrator.add_columns(
|
migrator.add_columns(
|
||||||
"users", valid_tokens_from=peewee.DateTimeField(default=datetime.datetime.now)
|
"users", valid_tokens_from=peewee.DateTimeField(default=Helpers.get_utc_now)
|
||||||
)
|
)
|
||||||
migrator.drop_columns("users", ["api_token"])
|
migrator.drop_columns("users", ["api_token"])
|
||||||
|
|
||||||
|
@ -5,13 +5,7 @@ import logging
|
|||||||
|
|
||||||
from app.classes.shared.console import Console
|
from app.classes.shared.console import Console
|
||||||
from app.classes.shared.migration import Migrator, MigrateHistory
|
from app.classes.shared.migration import Migrator, MigrateHistory
|
||||||
from app.classes.models.management import (
|
from app.classes.models.roles import Roles
|
||||||
Webhooks,
|
|
||||||
Schedules,
|
|
||||||
Backups,
|
|
||||||
)
|
|
||||||
from app.classes.models.server_permissions import RoleServers
|
|
||||||
from app.classes.models.base_model import BaseModel
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -53,6 +47,78 @@ def migrate(migrator: Migrator, database, **kwargs):
|
|||||||
table_name = "servers"
|
table_name = "servers"
|
||||||
database = db
|
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(
|
this_migration = MigrateHistory.get_or_none(
|
||||||
MigrateHistory.name == "20240217_rework_servers_uuid_part2"
|
MigrateHistory.name == "20240217_rework_servers_uuid_part2"
|
||||||
)
|
)
|
||||||
@ -70,8 +136,8 @@ def migrate(migrator: Migrator, database, **kwargs):
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info("Migrating Data from Int to UUID (Foreign Keys)")
|
logger.debug("Migrating Data from Int to UUID (Foreign Keys)")
|
||||||
Console.info("Migrating Data from Int to UUID (Foreign Keys)")
|
Console.debug("Migrating Data from Int to UUID (Foreign Keys)")
|
||||||
|
|
||||||
# Changes on Webhooks Log Table
|
# Changes on Webhooks Log Table
|
||||||
for webhook in Webhooks.select():
|
for webhook in Webhooks.select():
|
||||||
@ -122,8 +188,8 @@ def migrate(migrator: Migrator, database, **kwargs):
|
|||||||
and RoleServers.server_id == old_server_id
|
and RoleServers.server_id == old_server_id
|
||||||
).execute()
|
).execute()
|
||||||
|
|
||||||
logger.info("Migrating Data from Int to UUID (Foreign Keys) : SUCCESS")
|
logger.debug("Migrating Data from Int to UUID (Foreign Keys) : SUCCESS")
|
||||||
Console.info("Migrating Data from Int to UUID (Foreign Keys) : SUCCESS")
|
Console.debug("Migrating Data from Int to UUID (Foreign Keys) : SUCCESS")
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.error("Error while migrating Data from Int to UUID (Foreign Keys)")
|
logger.error("Error while migrating Data from Int to UUID (Foreign Keys)")
|
||||||
@ -135,16 +201,16 @@ def migrate(migrator: Migrator, database, **kwargs):
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info("Migrating Data from Int to UUID (Primary Keys)")
|
logger.debug("Migrating Data from Int to UUID (Primary Keys)")
|
||||||
Console.info("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
|
# Migrating servers from the old id type to the new one
|
||||||
for server in Servers.select():
|
for server in Servers.select():
|
||||||
Servers.update(server_id=server.server_uuid).where(
|
Servers.update(server_id=server.server_uuid).where(
|
||||||
Servers.server_id == server.server_id
|
Servers.server_id == server.server_id
|
||||||
).execute()
|
).execute()
|
||||||
|
|
||||||
logger.info("Migrating Data from Int to UUID (Primary Keys) : SUCCESS")
|
logger.debug("Migrating Data from Int to UUID (Primary Keys) : SUCCESS")
|
||||||
Console.info("Migrating Data from Int to UUID (Primary Keys) : SUCCESS")
|
Console.debug("Migrating Data from Int to UUID (Primary Keys) : SUCCESS")
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.error("Error while migrating Data from Int to UUID (Primary Keys)")
|
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"
|
table_name = "servers"
|
||||||
database = db
|
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:
|
try:
|
||||||
logger.info("Migrating Data from UUID to Int (Primary Keys)")
|
logger.debug("Migrating Data from UUID to Int (Primary Keys)")
|
||||||
Console.info("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
|
# Migrating servers from the old id type to the new one
|
||||||
new_id = 0
|
new_id = 0
|
||||||
for server in Servers.select():
|
for server in Servers.select():
|
||||||
@ -217,8 +355,8 @@ def rollback(migrator: Migrator, database, **kwargs):
|
|||||||
Servers.server_id == server.server_id
|
Servers.server_id == server.server_id
|
||||||
).execute()
|
).execute()
|
||||||
|
|
||||||
logger.info("Migrating Data from UUID to Int (Primary Keys) : SUCCESS")
|
logger.debug("Migrating Data from UUID to Int (Primary Keys) : SUCCESS")
|
||||||
Console.info("Migrating Data from UUID to Int (Primary Keys) : SUCCESS")
|
Console.debug("Migrating Data from UUID to Int (Primary Keys) : SUCCESS")
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.error("Error while migrating Data from UUID to Int (Primary Keys)")
|
logger.error("Error while migrating Data from UUID to Int (Primary Keys)")
|
||||||
@ -230,8 +368,8 @@ def rollback(migrator: Migrator, database, **kwargs):
|
|||||||
return
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
logger.info("Migrating Data from UUID to Int (Foreign Keys)")
|
logger.debug("Migrating Data from UUID to Int (Foreign Keys)")
|
||||||
Console.info("Migrating Data from UUID to Int (Foreign Keys)")
|
Console.debug("Migrating Data from UUID to Int (Foreign Keys)")
|
||||||
# Changes on Webhooks Log Table
|
# Changes on Webhooks Log Table
|
||||||
for webhook in Webhooks.select():
|
for webhook in Webhooks.select():
|
||||||
old_server_id = webhook.server_id_id
|
old_server_id = webhook.server_id_id
|
||||||
@ -281,8 +419,8 @@ def rollback(migrator: Migrator, database, **kwargs):
|
|||||||
and RoleServers.server_id == old_server_id
|
and RoleServers.server_id == old_server_id
|
||||||
).execute()
|
).execute()
|
||||||
|
|
||||||
logger.info("Migrating Data from UUID to Int (Foreign Keys) : SUCCESS")
|
logger.debug("Migrating Data from UUID to Int (Foreign Keys) : SUCCESS")
|
||||||
Console.info("Migrating Data from UUID to Int (Foreign Keys) : SUCCESS")
|
Console.debug("Migrating Data from UUID to Int (Foreign Keys) : SUCCESS")
|
||||||
|
|
||||||
except Exception as ex:
|
except Exception as ex:
|
||||||
logger.error("Error while migrating Data from UUID to Int (Foreign Keys)")
|
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"
|
"serversDesc": "servery, ke kterým má tato role přístup"
|
||||||
},
|
},
|
||||||
"serverBackups": {
|
"serverBackups": {
|
||||||
|
"actions": "Akce",
|
||||||
"after": "Spustit příkaz po záloze",
|
"after": "Spustit příkaz po záloze",
|
||||||
"backupAtMidnight": "Automatické zálohování o půlnoci?",
|
"backupAtMidnight": "Automatické zálohování o půlnoci?",
|
||||||
"backupNow": "Zálohovat nyní!",
|
"backupNow": "Zálohovat nyní!",
|
||||||
"backupTask": "Bylo spuštěno zálohování.",
|
"backupTask": "Bylo spuštěno zálohování.",
|
||||||
|
"backups": "Zálohy serverů",
|
||||||
"before": "Spustit příkaz před zálohou",
|
"before": "Spustit příkaz před zálohou",
|
||||||
"cancel": "Zrušit",
|
"cancel": "Zrušit",
|
||||||
"clickExclude": "Kliknutím vyberete výjimku",
|
"clickExclude": "Kliknutím vyberete výjimku",
|
||||||
@ -333,21 +335,34 @@
|
|||||||
"confirmDelete": "Chcete tuto zálohu odstranit? Tuto akci nelze vrátit zpět.",
|
"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.",
|
"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",
|
"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",
|
"delete": "Smazat",
|
||||||
"destroyBackup": "Zničit zálohu \" + file_to_del + \"?",
|
"destroyBackup": "Zničit zálohu \" + file_to_del + \"?",
|
||||||
"download": "Stáhnout",
|
"download": "Stáhnout",
|
||||||
|
"edit": "upravit",
|
||||||
|
"enabled": "Povoleno",
|
||||||
"excludedBackups": "Vyloučené cesty: ",
|
"excludedBackups": "Vyloučené cesty: ",
|
||||||
"excludedChoose": "Vyberte cesty, které chcete ze zálohování vyloučit.",
|
"excludedChoose": "Vyberte cesty, které chcete ze zálohování vyloučit.",
|
||||||
"exclusionsTitle": "Vyloučení ze zálohování",
|
"exclusionsTitle": "Vyloučení ze zálohování",
|
||||||
|
"failed": "Selhalo",
|
||||||
"maxBackups": "Maximální počet záloh",
|
"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).",
|
"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í",
|
"options": "Nastavení",
|
||||||
"path": "Cesta",
|
"path": "Cesta",
|
||||||
"restore": "Obnovit",
|
"restore": "Obnovit",
|
||||||
"restoring": "Obnovení zálohy. To může chvíli trvat. Buďte prosím trpěliví.",
|
"restoring": "Obnovení zálohy. To může chvíli trvat. Buďte prosím trpěliví.",
|
||||||
|
"run": "Nastartovat zálohu",
|
||||||
"save": "Uložit",
|
"save": "Uložit",
|
||||||
"shutdown": "Vypnout server po dobu zálohování",
|
"shutdown": "Vypnout server po dobu zálohování",
|
||||||
"size": "Velikost",
|
"size": "Velikost",
|
||||||
|
"standby": "V pohotovosti",
|
||||||
|
"status": "Stav",
|
||||||
|
"storage": "Lokace uložiště",
|
||||||
"storageLocation": "Umístění úložiště",
|
"storageLocation": "Umístění úložiště",
|
||||||
"storageLocationDesc": "Kam chcete ukládat zálohy?"
|
"storageLocationDesc": "Kam chcete ukládat zálohy?"
|
||||||
},
|
},
|
||||||
@ -512,6 +527,7 @@
|
|||||||
},
|
},
|
||||||
"serverSchedules": {
|
"serverSchedules": {
|
||||||
"action": "Akce",
|
"action": "Akce",
|
||||||
|
"actionId": "Vyberte zálohu na které se to má potvrdit!",
|
||||||
"areYouSure": "Odstranění naplánované úlohy?",
|
"areYouSure": "Odstranění naplánované úlohy?",
|
||||||
"cancel": "Zrušit",
|
"cancel": "Zrušit",
|
||||||
"cannotSee": "Nevidíte všechno?",
|
"cannotSee": "Nevidíte všechno?",
|
||||||
@ -672,6 +688,9 @@
|
|||||||
"userTheme": "Motiv UI",
|
"userTheme": "Motiv UI",
|
||||||
"uses": "Počet povolených použití (-1==bez omezení)"
|
"uses": "Počet povolených použití (-1==bez omezení)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "Heslo je příliš krátké. Minimální délka je 8 znaků"
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "Seš si jistý že chceš smazat tento webhook?",
|
"areYouSureDel": "Seš si jistý že chceš smazat tento webhook?",
|
||||||
"areYouSureRun": "Seš si jistý že chceš otestovat tento webhook?",
|
"areYouSureRun": "Seš si jistý že chceš otestovat tento webhook?",
|
||||||
|
@ -301,10 +301,12 @@
|
|||||||
"serversDesc": "Server, auf die Nutzer mit dieser Rolle zugreifen darf"
|
"serversDesc": "Server, auf die Nutzer mit dieser Rolle zugreifen darf"
|
||||||
},
|
},
|
||||||
"serverBackups": {
|
"serverBackups": {
|
||||||
|
"actions": "Aktionen",
|
||||||
"after": "Befehl nach dem Backup ausführen",
|
"after": "Befehl nach dem Backup ausführen",
|
||||||
"backupAtMidnight": "Automatisches Backup um 24:00 Uhr?",
|
"backupAtMidnight": "Automatisches Backup um 24:00 Uhr?",
|
||||||
"backupNow": "Jetzt sichern!",
|
"backupNow": "Jetzt sichern!",
|
||||||
"backupTask": "Ein Backup-Auftrag wurde gestartet.",
|
"backupTask": "Ein Backup-Auftrag wurde gestartet.",
|
||||||
|
"backups": "Server-Backups",
|
||||||
"before": "Befehl vor dem Backup ausführen",
|
"before": "Befehl vor dem Backup ausführen",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"clickExclude": "Auswählen, um Ausnahmen zu markieren",
|
"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.",
|
"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.",
|
"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",
|
"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",
|
"delete": "Löschen",
|
||||||
"destroyBackup": "Backup löschen \" + file_to_del + \"?",
|
"destroyBackup": "Backup löschen \" + file_to_del + \"?",
|
||||||
"download": "Herunterladen",
|
"download": "Herunterladen",
|
||||||
|
"edit": "Bearbeiten",
|
||||||
|
"enabled": "Aktiviert",
|
||||||
"excludedBackups": "Ausgeschlossene Verzeichnisse: ",
|
"excludedBackups": "Ausgeschlossene Verzeichnisse: ",
|
||||||
"excludedChoose": "Verzeichnisse auswählen, die nicht gesichert werden sollen",
|
"excludedChoose": "Verzeichnisse auswählen, die nicht gesichert werden sollen",
|
||||||
"exclusionsTitle": "Backup Ausnahmen",
|
"exclusionsTitle": "Backup Ausnahmen",
|
||||||
|
"failed": "Fehlgeschlagen",
|
||||||
"maxBackups": "Maximale Backups",
|
"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)",
|
"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",
|
"options": "Optionen",
|
||||||
"path": "Pfad",
|
"path": "Pfad",
|
||||||
"restore": "Wiederherstellen",
|
"restore": "Wiederherstellen",
|
||||||
"restoring": "Backup wiederherstellen. Dies kann eine Weile dauern.",
|
"restoring": "Backup wiederherstellen. Dies kann eine Weile dauern.",
|
||||||
|
"run": "Backup erstellen",
|
||||||
"save": "Speichern",
|
"save": "Speichern",
|
||||||
"shutdown": "Server für die Dauer des Backups stoppen",
|
"shutdown": "Server für die Dauer des Backups stoppen",
|
||||||
"size": "Größe",
|
"size": "Größe",
|
||||||
|
"standby": "Bereitschaft",
|
||||||
|
"status": "Status",
|
||||||
|
"storage": "Speicherort",
|
||||||
"storageLocation": "Speicherort",
|
"storageLocation": "Speicherort",
|
||||||
"storageLocationDesc": "Wo wollen Sie die Backups speichern?"
|
"storageLocationDesc": "Wo wollen Sie die Backups speichern?"
|
||||||
},
|
},
|
||||||
@ -492,6 +507,7 @@
|
|||||||
},
|
},
|
||||||
"serverSchedules": {
|
"serverSchedules": {
|
||||||
"action": "Aktion",
|
"action": "Aktion",
|
||||||
|
"actionId": "Aktion auswählen",
|
||||||
"areYouSure": "Geplante Aufgabe löschen?",
|
"areYouSure": "Geplante Aufgabe löschen?",
|
||||||
"cancel": "Abbrechen",
|
"cancel": "Abbrechen",
|
||||||
"cannotSee": "Nicht alles sichtbar?",
|
"cannotSee": "Nicht alles sichtbar?",
|
||||||
@ -653,6 +669,9 @@
|
|||||||
"userTheme": "Design für die Benutzeroberfläche",
|
"userTheme": "Design für die Benutzeroberfläche",
|
||||||
"uses": "Anzahl der erlaubten Verwendungen (-1==Keine Begrenzung)"
|
"uses": "Anzahl der erlaubten Verwendungen (-1==Keine Begrenzung)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "Passwort zu kurz. Mindestlänge: 8"
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "Sind Sie sicher, dass Sie diesen Webhook löschen möchten?",
|
"areYouSureDel": "Sind Sie sicher, dass Sie diesen Webhook löschen möchten?",
|
||||||
"areYouSureRun": "Sind Sie sicher, dass Sie diesen Webhook testen möchten?",
|
"areYouSureRun": "Sind Sie sicher, dass Sie diesen Webhook testen möchten?",
|
||||||
|
@ -298,10 +298,12 @@
|
|||||||
"serversDesc": "servers this role is allowed to access"
|
"serversDesc": "servers this role is allowed to access"
|
||||||
},
|
},
|
||||||
"serverBackups": {
|
"serverBackups": {
|
||||||
|
"actions": "Actions",
|
||||||
"after": "Run command after backup",
|
"after": "Run command after backup",
|
||||||
"backupAtMidnight": "Auto-backup at midnight?",
|
"backupAtMidnight": "Auto-backup at midnight?",
|
||||||
"backupNow": "Backup Now!",
|
"backupNow": "Backup Now!",
|
||||||
"backupTask": "A backup task has been started.",
|
"backupTask": "A backup task has been started.",
|
||||||
|
"backups": "Server Backups",
|
||||||
"before": "Run command before backup",
|
"before": "Run command before backup",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"clickExclude": "Click to select Exclusions",
|
"clickExclude": "Click to select Exclusions",
|
||||||
@ -310,21 +312,34 @@
|
|||||||
"confirmDelete": "Do you want to delete this backup? This cannot be undone.",
|
"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.",
|
"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",
|
"currentBackups": "Current Backups",
|
||||||
|
"default": "Default Backup",
|
||||||
|
"defaultExplain": "The backup that Crafty will use before updates. This cannot be changed or deleted.",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"destroyBackup": "Destroy backup \" + file_to_del + \"?",
|
"destroyBackup": "Destroy backup \" + file_to_del + \"?",
|
||||||
"download": "Download",
|
"download": "Download",
|
||||||
|
"edit": "Edit",
|
||||||
|
"enabled": "Enabled",
|
||||||
"excludedBackups": "Excluded Paths: ",
|
"excludedBackups": "Excluded Paths: ",
|
||||||
"excludedChoose": "Choose the paths you wish to exclude from your backups",
|
"excludedChoose": "Choose the paths you wish to exclude from your backups",
|
||||||
"exclusionsTitle": "Backup Exclusions",
|
"exclusionsTitle": "Backup Exclusions",
|
||||||
|
"failed": "Failed",
|
||||||
"maxBackups": "Max Backups",
|
"maxBackups": "Max Backups",
|
||||||
"maxBackupsDesc": "Crafty will not store more than N backups, deleting the oldest (enter 0 to keep all)",
|
"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",
|
"options": "Options",
|
||||||
"path": "Path",
|
"path": "Path",
|
||||||
"restore": "Restore",
|
"restore": "Restore",
|
||||||
"restoring": "Restoring Backup. This may take a while. Please be patient.",
|
"restoring": "Restoring Backup. This may take a while. Please be patient.",
|
||||||
|
"run": "Run Backup",
|
||||||
"save": "Save",
|
"save": "Save",
|
||||||
"shutdown": "Shutdown server for duration of backup",
|
"shutdown": "Shutdown server for duration of backup",
|
||||||
"size": "Size",
|
"size": "Size",
|
||||||
|
"standby": "Standby",
|
||||||
|
"status": "Status",
|
||||||
|
"storage": "Storage Location",
|
||||||
"storageLocation": "Storage Location",
|
"storageLocation": "Storage Location",
|
||||||
"storageLocationDesc": "Where do you want to store backups?"
|
"storageLocationDesc": "Where do you want to store backups?"
|
||||||
},
|
},
|
||||||
@ -489,6 +504,7 @@
|
|||||||
},
|
},
|
||||||
"serverSchedules": {
|
"serverSchedules": {
|
||||||
"action": "Action",
|
"action": "Action",
|
||||||
|
"actionId": "Select Action Child",
|
||||||
"areYouSure": "Delete Scheduled Task?",
|
"areYouSure": "Delete Scheduled Task?",
|
||||||
"cancel": "Cancel",
|
"cancel": "Cancel",
|
||||||
"cannotSee": "Not seeing everything?",
|
"cannotSee": "Not seeing everything?",
|
||||||
@ -651,6 +667,9 @@
|
|||||||
"userTheme": "UI Theme",
|
"userTheme": "UI Theme",
|
||||||
"uses": "Number of uses allowed (-1==No Limit)"
|
"uses": "Number of uses allowed (-1==No Limit)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "Password Too Short. Minimum Length: 8"
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "Are you sure you want to delete this webhook?",
|
"areYouSureDel": "Are you sure you want to delete this webhook?",
|
||||||
"areYouSureRun": "Are you sure you want to test this webhook?",
|
"areYouSureRun": "Are you sure you want to test this webhook?",
|
||||||
|
@ -228,7 +228,7 @@
|
|||||||
"login": "Iniciar Sesión",
|
"login": "Iniciar Sesión",
|
||||||
"password": "Contraseña",
|
"password": "Contraseña",
|
||||||
"username": "Usuario",
|
"username": "Usuario",
|
||||||
"viewStatus": "View Public Status Page"
|
"viewStatus": "Ver página de estado público"
|
||||||
},
|
},
|
||||||
"notify": {
|
"notify": {
|
||||||
"activityLog": "Registros de actividad",
|
"activityLog": "Registros de actividad",
|
||||||
@ -301,10 +301,12 @@
|
|||||||
"serversDesc": "Servidores a los que este grupo puede acceder"
|
"serversDesc": "Servidores a los que este grupo puede acceder"
|
||||||
},
|
},
|
||||||
"serverBackups": {
|
"serverBackups": {
|
||||||
|
"actions": "Acciones",
|
||||||
"after": "Comando ejecutado después del respaldo",
|
"after": "Comando ejecutado después del respaldo",
|
||||||
"backupAtMidnight": "¿Copia de seguridad automática a medianoche?",
|
"backupAtMidnight": "¿Copia de seguridad automática a medianoche?",
|
||||||
"backupNow": "¡Respalde ahora!",
|
"backupNow": "¡Respalde ahora!",
|
||||||
"backupTask": "Se ha iniciado una tarea de copia de seguridad.",
|
"backupTask": "Se ha iniciado una tarea de copia de seguridad.",
|
||||||
|
"backups": "Copias de seguridad del servidor",
|
||||||
"before": "Comando ejecutado antes del respaldo",
|
"before": "Comando ejecutado antes del respaldo",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"clickExclude": "Click para seleccionar las Exclusiones",
|
"clickExclude": "Click para seleccionar las Exclusiones",
|
||||||
@ -313,21 +315,34 @@
|
|||||||
"confirmDelete": "¿Quieres eliminar esta copia de seguridad? Esto no se puede deshacer.",
|
"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.",
|
"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",
|
"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",
|
"delete": "Eliminar",
|
||||||
"destroyBackup": "¿Destruir copia de seguridad \" + file_to_del + \"?",
|
"destroyBackup": "¿Destruir copia de seguridad \" + file_to_del + \"?",
|
||||||
"download": "Descargar",
|
"download": "Descargar",
|
||||||
|
"edit": "Editar",
|
||||||
|
"enabled": "Habilitado",
|
||||||
"excludedBackups": "Rutas Excluidas: ",
|
"excludedBackups": "Rutas Excluidas: ",
|
||||||
"excludedChoose": "Elige las rutas que desea excluir de los respaldos",
|
"excludedChoose": "Elige las rutas que desea excluir de los respaldos",
|
||||||
"exclusionsTitle": "Exclusiones en respaldos.",
|
"exclusionsTitle": "Exclusiones en respaldos.",
|
||||||
|
"failed": "Fallido",
|
||||||
"maxBackups": "Cantidad máxima de respaldos",
|
"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)",
|
"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",
|
"options": "Opciones",
|
||||||
"path": "Ruta",
|
"path": "Ruta",
|
||||||
"restore": "Restaurar",
|
"restore": "Restaurar",
|
||||||
"restoring": "Restaurando copia de seguridad. Esto puede tomar un tiempo. Sea paciente.",
|
"restoring": "Restaurando copia de seguridad. Esto puede tomar un tiempo. Sea paciente.",
|
||||||
|
"run": "Ejecutar Copia de seguridad",
|
||||||
"save": "Guardar",
|
"save": "Guardar",
|
||||||
"shutdown": "Apagar el servidor durante la duración de la copia del respaldo.",
|
"shutdown": "Apagar el servidor durante la duración de la copia del respaldo.",
|
||||||
"size": "Tamaño",
|
"size": "Tamaño",
|
||||||
|
"standby": "En espera",
|
||||||
|
"status": "Estado",
|
||||||
|
"storage": "Ubicación del almacenamiento",
|
||||||
"storageLocation": "Ubicación de almacenamiento",
|
"storageLocation": "Ubicación de almacenamiento",
|
||||||
"storageLocationDesc": "¿Dónde quieres almacenar las copias de seguridad?"
|
"storageLocationDesc": "¿Dónde quieres almacenar las copias de seguridad?"
|
||||||
},
|
},
|
||||||
@ -492,6 +507,7 @@
|
|||||||
},
|
},
|
||||||
"serverSchedules": {
|
"serverSchedules": {
|
||||||
"action": "Acción",
|
"action": "Acción",
|
||||||
|
"actionId": "Seleccionar acción secundaria",
|
||||||
"areYouSure": "¿Borrar tarea programada?",
|
"areYouSure": "¿Borrar tarea programada?",
|
||||||
"cancel": "Cancelar",
|
"cancel": "Cancelar",
|
||||||
"cannotSee": "¿No puede ver todo?",
|
"cannotSee": "¿No puede ver todo?",
|
||||||
@ -653,6 +669,9 @@
|
|||||||
"userTheme": "Tema de Interfaz",
|
"userTheme": "Tema de Interfaz",
|
||||||
"uses": "Número de usos permitidos. (Sin límite: -1)"
|
"uses": "Número de usos permitidos. (Sin límite: -1)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "Contraseña demasiado corta. Longitud mínima: 8"
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "¿Estás seguro de que quieres eliminar este webhook?",
|
"areYouSureDel": "¿Estás seguro de que quieres eliminar este webhook?",
|
||||||
"areYouSureRun": "¿Estás seguro de que quieres probar este webhook?",
|
"areYouSureRun": "¿Estás seguro de que quieres probar este webhook?",
|
||||||
|
@ -301,10 +301,12 @@
|
|||||||
"serversDesc": "Les serveurs auquels ce rôle a accès"
|
"serversDesc": "Les serveurs auquels ce rôle a accès"
|
||||||
},
|
},
|
||||||
"serverBackups": {
|
"serverBackups": {
|
||||||
|
"actions": "Actions",
|
||||||
"after": "Exécuter une commande après la sauvegarde",
|
"after": "Exécuter une commande après la sauvegarde",
|
||||||
"backupAtMidnight": "Sauvegarde Automatique à minuit ?",
|
"backupAtMidnight": "Sauvegarde Automatique à minuit ?",
|
||||||
"backupNow": "Sauvegarder Maintenant !",
|
"backupNow": "Sauvegarder Maintenant !",
|
||||||
"backupTask": "Une sauvegarde vient de démarrer.",
|
"backupTask": "Une sauvegarde vient de démarrer.",
|
||||||
|
"backups": "Sauvegarde de Serveur",
|
||||||
"before": "Exécuter une commande avant la sauvegarde",
|
"before": "Exécuter une commande avant la sauvegarde",
|
||||||
"cancel": "Annuler",
|
"cancel": "Annuler",
|
||||||
"clickExclude": "Cliquer pour sélectionner les Exclusions",
|
"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.",
|
"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.",
|
"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",
|
"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",
|
"delete": "Supprimer",
|
||||||
"destroyBackup": "Supprimer la sauvegarde \" + file_to_del + \" ?",
|
"destroyBackup": "Supprimer la sauvegarde \" + file_to_del + \" ?",
|
||||||
"download": "Télécharger",
|
"download": "Télécharger",
|
||||||
|
"edit": "Modifier",
|
||||||
|
"enabled": "Activé",
|
||||||
"excludedBackups": "Dossiers Exclus : ",
|
"excludedBackups": "Dossiers Exclus : ",
|
||||||
"excludedChoose": "Choisir les dossiers à exclure de la sauvegarde",
|
"excludedChoose": "Choisir les dossiers à exclure de la sauvegarde",
|
||||||
"exclusionsTitle": "Exclusions de Sauvegarde",
|
"exclusionsTitle": "Exclusions de Sauvegarde",
|
||||||
|
"failed": "Echec",
|
||||||
"maxBackups": "Sauvergardes Max",
|
"maxBackups": "Sauvergardes Max",
|
||||||
"maxBackupsDesc": "Crafty ne fera pas plus de N sauvegardes, supprimant les plus anciennes (entrer 0 pour toutes les garder)",
|
"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",
|
"options": "Options",
|
||||||
"path": "Chemin",
|
"path": "Chemin",
|
||||||
"restore": "Restaurer",
|
"restore": "Restaurer",
|
||||||
"restoring": "Restauration de la sauvegarde. Cela peut prendre un peu de temps. S'il vous plaît soyez patient.",
|
"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",
|
"save": "Sauvegarder",
|
||||||
"shutdown": "Extinction du serveur pendant la durée de la sauvegarde",
|
"shutdown": "Extinction du serveur pendant la durée de la sauvegarde",
|
||||||
"size": "Taille",
|
"size": "Taille",
|
||||||
|
"standby": "Attente",
|
||||||
|
"status": "Statut",
|
||||||
|
"storage": "Emplacement de la Sauvegarde",
|
||||||
"storageLocation": "Emplacement de Sauvegarde",
|
"storageLocation": "Emplacement de Sauvegarde",
|
||||||
"storageLocationDesc": "Où veux-tu enregister tes sauvegardes ?"
|
"storageLocationDesc": "Où veux-tu enregister tes sauvegardes ?"
|
||||||
},
|
},
|
||||||
@ -492,6 +507,7 @@
|
|||||||
},
|
},
|
||||||
"serverSchedules": {
|
"serverSchedules": {
|
||||||
"action": "Action",
|
"action": "Action",
|
||||||
|
"actionId": "Sélectionner une configuration de sauvegarde",
|
||||||
"areYouSure": "Supprimer la Tâche Planifiée ?",
|
"areYouSure": "Supprimer la Tâche Planifiée ?",
|
||||||
"cancel": "Annuler",
|
"cancel": "Annuler",
|
||||||
"cannotSee": "Tu ne peux pas tout voir ?",
|
"cannotSee": "Tu ne peux pas tout voir ?",
|
||||||
@ -653,6 +669,9 @@
|
|||||||
"userTheme": "Theme d'Interface Utilisateur",
|
"userTheme": "Theme d'Interface Utilisateur",
|
||||||
"uses": "Nombre d'utilisation Authorisé (-1 == Illimité)"
|
"uses": "Nombre d'utilisation Authorisé (-1 == Illimité)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "Mot de passe trop court. Longueur minimum : 8"
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "Es-tu sûr de vouloir supprimer ce webhook ?",
|
"areYouSureDel": "Es-tu sûr de vouloir supprimer ce webhook ?",
|
||||||
"areYouSureRun": "Es-tu sûr de vouloir tester ce webhook ?",
|
"areYouSureRun": "Es-tu sûr de vouloir tester ce webhook ?",
|
||||||
|
@ -301,10 +301,12 @@
|
|||||||
"serversDesc": "לשרתים מותר לגשת לתפקיד זה"
|
"serversDesc": "לשרתים מותר לגשת לתפקיד זה"
|
||||||
},
|
},
|
||||||
"serverBackups": {
|
"serverBackups": {
|
||||||
|
"actions": "פעולות",
|
||||||
"after": "הרץ פקודה לאחר הגיבוי",
|
"after": "הרץ פקודה לאחר הגיבוי",
|
||||||
"backupAtMidnight": "גיבוי אוטומטי בחצות?",
|
"backupAtMidnight": "גיבוי אוטומטי בחצות?",
|
||||||
"backupNow": "!גיבוי עכשיו",
|
"backupNow": "!גיבוי עכשיו",
|
||||||
"backupTask": "החלה משימת גיבוי.",
|
"backupTask": "החלה משימת גיבוי.",
|
||||||
|
"backups": "גיבויי שרת",
|
||||||
"before": "הרץ פקודה לפני הגיבוי",
|
"before": "הרץ פקודה לפני הגיבוי",
|
||||||
"cancel": "לבטל",
|
"cancel": "לבטל",
|
||||||
"clickExclude": "לחצו כדי לבחור מה לא יהיה בגיבוי",
|
"clickExclude": "לחצו כדי לבחור מה לא יהיה בגיבוי",
|
||||||
@ -313,21 +315,34 @@
|
|||||||
"confirmDelete": "האם ברצונכם למחוק את הגיבוי הזה? אי אפשר לבטל את זה.",
|
"confirmDelete": "האם ברצונכם למחוק את הגיבוי הזה? אי אפשר לבטל את זה.",
|
||||||
"confirmRestore": "האם אתם בטוחים שברצונכם לשחזר מגיבוי זה. כל קבצי השרת הנוכחיים ישתנו למצב גיבוי ולא יהיה אפשר לשחזר.",
|
"confirmRestore": "האם אתם בטוחים שברצונכם לשחזר מגיבוי זה. כל קבצי השרת הנוכחיים ישתנו למצב גיבוי ולא יהיה אפשר לשחזר.",
|
||||||
"currentBackups": "גיבויים נוכחיים",
|
"currentBackups": "גיבויים נוכחיים",
|
||||||
|
"default": "גיבוי ברירת מחדל",
|
||||||
|
"defaultExplain": "הגיבוי ש-Crafty ישתמש בו לפני עדכונים. לא ניתן לשנות או למחוק.",
|
||||||
"delete": "למחוק",
|
"delete": "למחוק",
|
||||||
"destroyBackup": "?\" + file_to_del + \" להרוס גיבוי",
|
"destroyBackup": "?\" + file_to_del + \" להרוס גיבוי",
|
||||||
"download": "הורדה",
|
"download": "הורדה",
|
||||||
|
"edit": "ערוך",
|
||||||
|
"enabled": "מופעל",
|
||||||
"excludedBackups": "נתיבים שלא נכללו: ",
|
"excludedBackups": "נתיבים שלא נכללו: ",
|
||||||
"excludedChoose": "בחרו את הנתיבים שברצונכם לא לכלול בגיבויים",
|
"excludedChoose": "בחרו את הנתיבים שברצונכם לא לכלול בגיבויים",
|
||||||
"exclusionsTitle": "אי הכללות גיבוי",
|
"exclusionsTitle": "אי הכללות גיבוי",
|
||||||
|
"failed": "נכשל",
|
||||||
"maxBackups": "מקסימום גיבויים",
|
"maxBackups": "מקסימום גיבויים",
|
||||||
"maxBackupsDesc": "גיבויים, ימחק את הישן ביותר (הזן 0 כדי לשמור את כולם) N-קראפטי לא יאחסן יותר מ",
|
"maxBackupsDesc": "גיבויים, ימחק את הישן ביותר (הזן 0 כדי לשמור את כולם) N-קראפטי לא יאחסן יותר מ",
|
||||||
|
"myBackup": "הגיבוי החדש שלי",
|
||||||
|
"name": "שם",
|
||||||
|
"newBackup": "צור גיבוי חדש",
|
||||||
|
"no-backup": "אין גיבויים. כדי ליצור תצורת גיבוי חדשה אנא לחץ על גיבוי חדש",
|
||||||
"options": "אפשרויות",
|
"options": "אפשרויות",
|
||||||
"path": "נתיב",
|
"path": "נתיב",
|
||||||
"restore": "לשחזר",
|
"restore": "לשחזר",
|
||||||
"restoring": "שחזור גיבוי. זה עשוי לקחת זמן. אנא חכו בסבלנות.",
|
"restoring": "שחזור גיבוי. זה עשוי לקחת זמן. אנא חכו בסבלנות.",
|
||||||
|
"run": "הפעל גיבוי",
|
||||||
"save": "שמירה",
|
"save": "שמירה",
|
||||||
"shutdown": "כיבוי שרת למשך הגיבוי",
|
"shutdown": "כיבוי שרת למשך הגיבוי",
|
||||||
"size": "גודל",
|
"size": "גודל",
|
||||||
|
"standby": "בהמתנה",
|
||||||
|
"status": "סטטוס",
|
||||||
|
"storage": "מיקום אחסון",
|
||||||
"storageLocation": "מקום איחסון",
|
"storageLocation": "מקום איחסון",
|
||||||
"storageLocationDesc": "איפו אתם רוצים לאחסן גיבויים?"
|
"storageLocationDesc": "איפו אתם רוצים לאחסן גיבויים?"
|
||||||
},
|
},
|
||||||
@ -492,6 +507,7 @@
|
|||||||
},
|
},
|
||||||
"serverSchedules": {
|
"serverSchedules": {
|
||||||
"action": "פעולה",
|
"action": "פעולה",
|
||||||
|
"actionId": "בחר פעולה משנית",
|
||||||
"areYouSure": "למחוק משימה מתוזמנת?",
|
"areYouSure": "למחוק משימה מתוזמנת?",
|
||||||
"cancel": "לבטל",
|
"cancel": "לבטל",
|
||||||
"cannotSee": "לא רואים הכל?",
|
"cannotSee": "לא רואים הכל?",
|
19
app/translations/humanized_index.json
Normal file
19
app/translations/humanized_index.json
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
{
|
||||||
|
"language": {
|
||||||
|
"cs_CS": "Čeština",
|
||||||
|
"de_DE": "Deutsch",
|
||||||
|
"en_EN": "English (US)",
|
||||||
|
"es_ES": "Español",
|
||||||
|
"fr_FR": "Français (France)",
|
||||||
|
"he_IL": "he_IL",
|
||||||
|
"it_IT": "Italiano",
|
||||||
|
"lol_EN": "Lolcatz",
|
||||||
|
"lv_LV": "Latviešu",
|
||||||
|
"nl_BE": "nl_BE",
|
||||||
|
"pl_PL": "Polski",
|
||||||
|
"th_TH": "ไทย",
|
||||||
|
"tr_TR": "Türkçe",
|
||||||
|
"uk_UA": "Українська",
|
||||||
|
"zh_CN": "中文(中国)"
|
||||||
|
}
|
||||||
|
}
|
@ -301,10 +301,12 @@
|
|||||||
"serversDesc": "Server a cui questo ruolo è consentito l'accesso"
|
"serversDesc": "Server a cui questo ruolo è consentito l'accesso"
|
||||||
},
|
},
|
||||||
"serverBackups": {
|
"serverBackups": {
|
||||||
|
"actions": "Azioni",
|
||||||
"after": "Esegui il comando prima del backup",
|
"after": "Esegui il comando prima del backup",
|
||||||
"backupAtMidnight": "Auto-backup a mezzanotte?",
|
"backupAtMidnight": "Auto-backup a mezzanotte?",
|
||||||
"backupNow": "Effettua il Backup Ora!",
|
"backupNow": "Effettua il Backup Ora!",
|
||||||
"backupTask": "Un'azione di backup è cominciata.",
|
"backupTask": "Un'azione di backup è cominciata.",
|
||||||
|
"backups": "Backup del server",
|
||||||
"before": "Esegui il comando dopo il backup",
|
"before": "Esegui il comando dopo il backup",
|
||||||
"cancel": "Cancella",
|
"cancel": "Cancella",
|
||||||
"clickExclude": "Clicca per selezionare le esclusioni",
|
"clickExclude": "Clicca per selezionare le esclusioni",
|
||||||
@ -313,21 +315,34 @@
|
|||||||
"confirmDelete": "Vuoi eliminare questo backup? Non puoi tornare indietro.",
|
"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.",
|
"confirmRestore": "Sei sicuro di voler ripristinare qeusto backup? Tutti i file correnti verranno sovrascritti allo stato di backup e saranno irrecuperabili.",
|
||||||
"currentBackups": "Backup attuali",
|
"currentBackups": "Backup attuali",
|
||||||
|
"default": "Backup predefinito",
|
||||||
|
"defaultExplain": "Il backup che Crafty utilizzerà prima degli aggiornamenti. Non può essere cambiato o eliminato.",
|
||||||
"delete": "Elimina",
|
"delete": "Elimina",
|
||||||
"destroyBackup": "Distruggere il backup \" + file_to_del + \"?",
|
"destroyBackup": "Distruggere il backup \" + file_to_del + \"?",
|
||||||
"download": "Scarica",
|
"download": "Scarica",
|
||||||
|
"edit": "Modifica",
|
||||||
|
"enabled": "Abilitato",
|
||||||
"excludedBackups": "Percorsi esclusi: ",
|
"excludedBackups": "Percorsi esclusi: ",
|
||||||
"excludedChoose": "Scegli i percorsi che desideri escludere dai tuoi backups",
|
"excludedChoose": "Scegli i percorsi che desideri escludere dai tuoi backups",
|
||||||
"exclusionsTitle": "Fai un backup delle esclusioni",
|
"exclusionsTitle": "Fai un backup delle esclusioni",
|
||||||
|
"failed": "Fallito",
|
||||||
"maxBackups": "Backup massimi",
|
"maxBackups": "Backup massimi",
|
||||||
"maxBackupsDesc": "Crafty non memorizzerà più di N backup, cancellando quelli più vecchi (inserisci 0 per mantenerli tutti)",
|
"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",
|
"options": "Opzioni",
|
||||||
"path": "Percorso",
|
"path": "Percorso",
|
||||||
"restore": "Ripristina",
|
"restore": "Ripristina",
|
||||||
"restoring": "Ripristinando il backup. Potrebber volerci un momento. Per favore sii paziente.",
|
"restoring": "Ripristinando il backup. Potrebber volerci un momento. Per favore sii paziente.",
|
||||||
|
"run": "Esegui backup",
|
||||||
"save": "Salva",
|
"save": "Salva",
|
||||||
"shutdown": "Arresto del server per la durata del backup",
|
"shutdown": "Arresto del server per la durata del backup",
|
||||||
"size": "Dimensioni",
|
"size": "Dimensioni",
|
||||||
|
"standby": "Sospeso",
|
||||||
|
"status": "Stato",
|
||||||
|
"storage": "Percorso archiviazione",
|
||||||
"storageLocation": "Percorso di memorizzazione",
|
"storageLocation": "Percorso di memorizzazione",
|
||||||
"storageLocationDesc": "Dove vuoi memorizzare i backup?"
|
"storageLocationDesc": "Dove vuoi memorizzare i backup?"
|
||||||
},
|
},
|
||||||
@ -492,6 +507,7 @@
|
|||||||
},
|
},
|
||||||
"serverSchedules": {
|
"serverSchedules": {
|
||||||
"action": "Azione",
|
"action": "Azione",
|
||||||
|
"actionId": "Seleziona azione da eseguire",
|
||||||
"areYouSure": "Eliminare l'azione programmata?",
|
"areYouSure": "Eliminare l'azione programmata?",
|
||||||
"cancel": "Cancella",
|
"cancel": "Cancella",
|
||||||
"cannotSee": "Non vedi tutto?",
|
"cannotSee": "Non vedi tutto?",
|
||||||
@ -653,6 +669,9 @@
|
|||||||
"userTheme": "Tema IU",
|
"userTheme": "Tema IU",
|
||||||
"uses": "Numero di usi permessi (-1==Nessun limite)"
|
"uses": "Numero di usi permessi (-1==Nessun limite)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "La password è troppo corta. Lunghezza minima: 8"
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "Sei sicuro di voler eliminare questo webhook?",
|
"areYouSureDel": "Sei sicuro di voler eliminare questo webhook?",
|
||||||
"areYouSureRun": "Sei sicuro di voler testare questo webhook?",
|
"areYouSureRun": "Sei sicuro di voler testare questo webhook?",
|
||||||
|
@ -301,10 +301,12 @@
|
|||||||
"serversDesc": "SERVRS DIS ROLE IZ ALLOWD 2 ACCES"
|
"serversDesc": "SERVRS DIS ROLE IZ ALLOWD 2 ACCES"
|
||||||
},
|
},
|
||||||
"serverBackups": {
|
"serverBackups": {
|
||||||
|
"actions": "DO-STUFFZ",
|
||||||
"after": "RUNZ COMMANDZ AFTUR BAKUP",
|
"after": "RUNZ COMMANDZ AFTUR BAKUP",
|
||||||
"backupAtMidnight": "AUTO-BAKUP AT MIDDLENIGHTZ?",
|
"backupAtMidnight": "AUTO-BAKUP AT MIDDLENIGHTZ?",
|
||||||
"backupNow": "BAKUP NOWZ!",
|
"backupNow": "BAKUP NOWZ!",
|
||||||
"backupTask": "OKAI I GETZ FISH, BAK SOONZ",
|
"backupTask": "OKAI I GETZ FISH, BAK SOONZ",
|
||||||
|
"backups": "SERVER BACKUPS",
|
||||||
"before": "RUNZ COMMANDZ BEFOUR BAKUP",
|
"before": "RUNZ COMMANDZ BEFOUR BAKUP",
|
||||||
"cancel": "STAHP",
|
"cancel": "STAHP",
|
||||||
"clickExclude": "CLICK 2 MARK EXCLUSHUNS",
|
"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)",
|
"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.",
|
"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",
|
"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",
|
"delete": "MAK GONE",
|
||||||
"destroyBackup": "EAT BAKUP \" + file_to_del + \"?",
|
"destroyBackup": "EAT BAKUP \" + file_to_del + \"?",
|
||||||
"download": "DOWNLOADZ",
|
"download": "DOWNLOADZ",
|
||||||
|
"edit": "MAKE BETTERS",
|
||||||
|
"enabled": "TURNED ON",
|
||||||
"excludedBackups": "EXCLUSHUNS: ",
|
"excludedBackups": "EXCLUSHUNS: ",
|
||||||
"excludedChoose": "CHOOSE TEH PATHS U WANTS 2 EXCLUDE FRUM UR BAKUPS",
|
"excludedChoose": "CHOOSE TEH PATHS U WANTS 2 EXCLUDE FRUM UR BAKUPS",
|
||||||
"exclusionsTitle": "BAKUP EXCLUSHUNS",
|
"exclusionsTitle": "BAKUP EXCLUSHUNS",
|
||||||
|
"failed": "NOPE'D",
|
||||||
"maxBackups": "MAX BAKUPS",
|
"maxBackups": "MAX BAKUPS",
|
||||||
"maxBackupsDesc": "CWAFTY WILL NOT KEEPZ MOAR THAN N BCKUPS, DELETIN TEH MOST OLDZ FURST (ENTR 0 TO BE BIG GREEDY)",
|
"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",
|
"options": "OPSHUNS",
|
||||||
"path": "PETH",
|
"path": "PETH",
|
||||||
"restore": "RESTOR",
|
"restore": "RESTOR",
|
||||||
"restoring": "RESTORIN BAKUP. DIS CUD TAEK WHILE. PLZ BE PATIENT.",
|
"restoring": "RESTORIN BAKUP. DIS CUD TAEK WHILE. PLZ BE PATIENT.",
|
||||||
|
"run": "DO BACKUP NOWZ",
|
||||||
"save": "DUN",
|
"save": "DUN",
|
||||||
"shutdown": "SLEEPY SERVR WEN MAK BAKAUPZ?",
|
"shutdown": "SLEEPY SERVR WEN MAK BAKAUPZ?",
|
||||||
"size": "HOW BIGZ",
|
"size": "HOW BIGZ",
|
||||||
|
"standby": "WAITIN'",
|
||||||
|
"status": "WHAT'S UP",
|
||||||
|
"storage": "HIDING SPOT",
|
||||||
"storageLocation": "SHINY STASH OV HINGZ",
|
"storageLocation": "SHINY STASH OV HINGZ",
|
||||||
"storageLocationDesc": "WER DO U WANTS 2 STASH BAKUPS?"
|
"storageLocationDesc": "WER DO U WANTS 2 STASH BAKUPS?"
|
||||||
},
|
},
|
||||||
@ -492,6 +507,7 @@
|
|||||||
},
|
},
|
||||||
"serverSchedules": {
|
"serverSchedules": {
|
||||||
"action": "ACTSHUN",
|
"action": "ACTSHUN",
|
||||||
|
"actionId": "PICK ACTION KITTY",
|
||||||
"areYouSure": "FORGET 2 DO DIS ????",
|
"areYouSure": "FORGET 2 DO DIS ????",
|
||||||
"cancel": "STAHP",
|
"cancel": "STAHP",
|
||||||
"cannotSee": "CANNY SEE?",
|
"cannotSee": "CANNY SEE?",
|
||||||
@ -653,6 +669,9 @@
|
|||||||
"userTheme": "THEMEZ",
|
"userTheme": "THEMEZ",
|
||||||
"uses": "NUMBER OV USES ALLOWED (-1==NO LIMIT)"
|
"uses": "NUMBER OV USES ALLOWED (-1==NO LIMIT)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "PASSWRD TOO SMOL. NEEDZ 8 CATZ PLZ"
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "U SURE U WANTZ TO EATZ DIS WEBHOOK?",
|
"areYouSureDel": "U SURE U WANTZ TO EATZ DIS WEBHOOK?",
|
||||||
"areYouSureRun": "U SURE U WANTZ TO TESTZ DIS WEBHOOK?",
|
"areYouSureRun": "U SURE U WANTZ TO TESTZ DIS WEBHOOK?",
|
||||||
|
@ -302,10 +302,12 @@
|
|||||||
"serversDesc": "serveri, kuriem šai lomai ir atļauta piekļuve"
|
"serversDesc": "serveri, kuriem šai lomai ir atļauta piekļuve"
|
||||||
},
|
},
|
||||||
"serverBackups": {
|
"serverBackups": {
|
||||||
|
"actions": "Darbības",
|
||||||
"after": "Palaist komandu pēc dublējuma",
|
"after": "Palaist komandu pēc dublējuma",
|
||||||
"backupAtMidnight": "Automātiski dublēt pusnaktī?",
|
"backupAtMidnight": "Automātiski dublēt pusnaktī?",
|
||||||
"backupNow": "Dublēt Tagad!",
|
"backupNow": "Dublēt Tagad!",
|
||||||
"backupTask": "Dublējuma uzdevums ticis startēts.",
|
"backupTask": "Dublējuma uzdevums ticis startēts.",
|
||||||
|
"backups": "Servera Dublējumi",
|
||||||
"before": "Palaist komandu pirms dublējuma",
|
"before": "Palaist komandu pirms dublējuma",
|
||||||
"cancel": "Atcelt",
|
"cancel": "Atcelt",
|
||||||
"clickExclude": "Nospied lai izvēlētos Izņēmumus",
|
"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.",
|
"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.",
|
"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",
|
"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",
|
"delete": "Dzēst",
|
||||||
"destroyBackup": "Iznīcināt dublējumu \" + file_to_del + \"?",
|
"destroyBackup": "Iznīcināt dublējumu \" + file_to_del + \"?",
|
||||||
"download": "Lejupielādēt",
|
"download": "Lejupielādēt",
|
||||||
|
"edit": "Rediģēt",
|
||||||
|
"enabled": "Iespējots",
|
||||||
"excludedBackups": "Izņēmuma Ceļi: ",
|
"excludedBackups": "Izņēmuma Ceļi: ",
|
||||||
"excludedChoose": "Izvēlies ceļus, kurus tu vēlies izņemt no saviem dublējumiem",
|
"excludedChoose": "Izvēlies ceļus, kurus tu vēlies izņemt no saviem dublējumiem",
|
||||||
"exclusionsTitle": "Dublējuma Izņēmumi",
|
"exclusionsTitle": "Dublējuma Izņēmumi",
|
||||||
|
"failed": "Neizdevās",
|
||||||
"maxBackups": "Maks. Dublējumi",
|
"maxBackups": "Maks. Dublējumi",
|
||||||
"maxBackupsDesc": "Crafty nesaglabās vairāk nekā N dublējumus, dzēšot vecākaos (ievadi 0 lai saglabātu visus)",
|
"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",
|
"options": "Opcijas",
|
||||||
"path": "Ceļš",
|
"path": "Ceļš",
|
||||||
"restore": "Atjaunot",
|
"restore": "Atjaunot",
|
||||||
"restoring": "Atjauno dublējumu. Tas var aizņemt kādi laiku. Esiet pacietīgs.",
|
"restoring": "Atjauno dublējumu. Tas var aizņemt kādi laiku. Esiet pacietīgs.",
|
||||||
|
"run": "Veikt Dublējumu",
|
||||||
"save": "Saglabāt",
|
"save": "Saglabāt",
|
||||||
"shutdown": "Apturēt serveri dublējumkopijas laikā",
|
"shutdown": "Apturēt serveri dublējumkopijas laikā",
|
||||||
"size": "Lielums",
|
"size": "Lielums",
|
||||||
|
"standby": "Gaidstāve",
|
||||||
|
"status": "Statuss",
|
||||||
|
"storage": "Glabātavas Vieta",
|
||||||
"storageLocation": "Krātuves Vieta",
|
"storageLocation": "Krātuves Vieta",
|
||||||
"storageLocationDesc": "Kur jūs vēlaties saglabāt dublējumus?"
|
"storageLocationDesc": "Kur jūs vēlaties saglabāt dublējumus?"
|
||||||
},
|
},
|
||||||
@ -493,6 +508,7 @@
|
|||||||
},
|
},
|
||||||
"serverSchedules": {
|
"serverSchedules": {
|
||||||
"action": "Darbība",
|
"action": "Darbība",
|
||||||
|
"actionId": "Izvēlēties apakšdarbību",
|
||||||
"areYouSure": "Dzēst Ieplānoto Uzdevumu?",
|
"areYouSure": "Dzēst Ieplānoto Uzdevumu?",
|
||||||
"cancel": "Atcelt",
|
"cancel": "Atcelt",
|
||||||
"cannotSee": "Neredziet visu?",
|
"cannotSee": "Neredziet visu?",
|
||||||
@ -654,6 +670,9 @@
|
|||||||
"userTheme": "UI Tēma",
|
"userTheme": "UI Tēma",
|
||||||
"uses": "Dauzums, cik reizes lietot (-1==Bez Limita)"
|
"uses": "Dauzums, cik reizes lietot (-1==Bez Limita)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "Parole pārāk īsa. Minimālais Garums: 8"
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "Vai tiešām vēlies noņemt šo webhook?",
|
"areYouSureDel": "Vai tiešām vēlies noņemt šo webhook?",
|
||||||
"areYouSureRun": "Vai tiešām vēlies testēt šo webhook?",
|
"areYouSureRun": "Vai tiešām vēlies testēt šo webhook?",
|
||||||
|
@ -301,10 +301,12 @@
|
|||||||
"serversDesc": "servers waar deze rol toegang toe heeft"
|
"serversDesc": "servers waar deze rol toegang toe heeft"
|
||||||
},
|
},
|
||||||
"serverBackups": {
|
"serverBackups": {
|
||||||
|
"actions": "Acties",
|
||||||
"after": "Voer opdracht uit na back-up",
|
"after": "Voer opdracht uit na back-up",
|
||||||
"backupAtMidnight": "Automatische back-up maken om middernacht?",
|
"backupAtMidnight": "Automatische back-up maken om middernacht?",
|
||||||
"backupNow": "Nu een back-up maken!",
|
"backupNow": "Nu een back-up maken!",
|
||||||
"backupTask": "Er is een back-uptaak gestart.",
|
"backupTask": "Er is een back-uptaak gestart.",
|
||||||
|
"backups": "Serverbackups",
|
||||||
"before": "Voer opdracht uit vóór back-up",
|
"before": "Voer opdracht uit vóór back-up",
|
||||||
"cancel": "Annuleren",
|
"cancel": "Annuleren",
|
||||||
"clickExclude": "Klik om Uitsluitingen te selecteren",
|
"clickExclude": "Klik om Uitsluitingen te selecteren",
|
||||||
@ -313,21 +315,34 @@
|
|||||||
"confirmDelete": "Wil je deze back-up verwijderen? Dit kan niet ongedaan gemaakt worden.",
|
"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.",
|
"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",
|
"currentBackups": "Huidige back-ups",
|
||||||
|
"default": "Standaardbackup",
|
||||||
|
"defaultExplain": "De backup die Crafty gebruikt vóór updates. Deze kan niet worden gewijzigd of verwijderd.",
|
||||||
"delete": "Verwijderen",
|
"delete": "Verwijderen",
|
||||||
"destroyBackup": "Back-up vernietigen \" + file_to_del + \"?",
|
"destroyBackup": "Back-up vernietigen \" + file_to_del + \"?",
|
||||||
"download": "Downloaden",
|
"download": "Downloaden",
|
||||||
|
"edit": "Bewerken",
|
||||||
|
"enabled": "Ingeschakeld",
|
||||||
"excludedBackups": "Uitgesloten paden: ",
|
"excludedBackups": "Uitgesloten paden: ",
|
||||||
"excludedChoose": "Kies de paden die u wilt uitsluiten van uw back-ups",
|
"excludedChoose": "Kies de paden die u wilt uitsluiten van uw back-ups",
|
||||||
"exclusionsTitle": "Uitsluitingen voor back-ups",
|
"exclusionsTitle": "Uitsluitingen voor back-ups",
|
||||||
|
"failed": "Mislukt",
|
||||||
"maxBackups": "Max Back-ups",
|
"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)",
|
"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",
|
"options": "Opties",
|
||||||
"path": "Pad",
|
"path": "Pad",
|
||||||
"restore": "Herstellen",
|
"restore": "Herstellen",
|
||||||
"restoring": "Back-up herstellen. Dit kan een tijdje duren. Even geduld alstublieft.",
|
"restoring": "Back-up herstellen. Dit kan een tijdje duren. Even geduld alstublieft.",
|
||||||
|
"run": "Backup uitvoeren",
|
||||||
"save": "Opslaan",
|
"save": "Opslaan",
|
||||||
"shutdown": "Sluit de server af voor de duur van de backup",
|
"shutdown": "Sluit de server af voor de duur van de backup",
|
||||||
"size": "Grootte",
|
"size": "Grootte",
|
||||||
|
"standby": "Standby",
|
||||||
|
"status": "Status",
|
||||||
|
"storage": "Opslaglocatie",
|
||||||
"storageLocation": "Opslaglocatie",
|
"storageLocation": "Opslaglocatie",
|
||||||
"storageLocationDesc": "Waar wil je back-ups opslaan?"
|
"storageLocationDesc": "Waar wil je back-ups opslaan?"
|
||||||
},
|
},
|
||||||
@ -492,6 +507,7 @@
|
|||||||
},
|
},
|
||||||
"serverSchedules": {
|
"serverSchedules": {
|
||||||
"action": "Actie",
|
"action": "Actie",
|
||||||
|
"actionId": "Selecteer onderliggende actie",
|
||||||
"areYouSure": "Verwijder Geplande Taak?",
|
"areYouSure": "Verwijder Geplande Taak?",
|
||||||
"cancel": "Annuleren",
|
"cancel": "Annuleren",
|
||||||
"cannotSee": "Ziet u niet alles?",
|
"cannotSee": "Ziet u niet alles?",
|
||||||
@ -653,6 +669,9 @@
|
|||||||
"userTheme": "UI-thema",
|
"userTheme": "UI-thema",
|
||||||
"uses": "Aantal toegestane gebruiken (-1==Geen Limiet)"
|
"uses": "Aantal toegestane gebruiken (-1==Geen Limiet)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "Wachtwoord te kort. Minimumlengte: 8 tekens"
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "Weet u zeker dat u deze webhook wilt verwijderen?",
|
"areYouSureDel": "Weet u zeker dat u deze webhook wilt verwijderen?",
|
||||||
"areYouSureRun": "Weet u zeker dat u deze webhook wilt testen?",
|
"areYouSureRun": "Weet u zeker dat u deze webhook wilt testen?",
|
||||||
|
@ -301,10 +301,12 @@
|
|||||||
"serversDesc": "Serwery które mają tą role mają dostęp"
|
"serversDesc": "Serwery które mają tą role mają dostęp"
|
||||||
},
|
},
|
||||||
"serverBackups": {
|
"serverBackups": {
|
||||||
|
"actions": "Akcje",
|
||||||
"after": "Wykonaj tę komendę po backupie",
|
"after": "Wykonaj tę komendę po backupie",
|
||||||
"backupAtMidnight": "Auto-backup o północy?",
|
"backupAtMidnight": "Auto-backup o północy?",
|
||||||
"backupNow": "Backup Teraz!",
|
"backupNow": "Backup Teraz!",
|
||||||
"backupTask": "Backup został rozpoczęty.",
|
"backupTask": "Backup został rozpoczęty.",
|
||||||
|
"backups": "Kopie zapasowe serwera",
|
||||||
"before": "Wykonaj tę komendę przed backupem",
|
"before": "Wykonaj tę komendę przed backupem",
|
||||||
"cancel": "Anuluj",
|
"cancel": "Anuluj",
|
||||||
"clickExclude": "Kliknij aby zaznaczyć wyjątki",
|
"clickExclude": "Kliknij aby zaznaczyć wyjątki",
|
||||||
@ -313,21 +315,34 @@
|
|||||||
"confirmDelete": "Czy chcesz usunąć ten backup? Nie można tego cofnąć.",
|
"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.",
|
"confirmRestore": "Czy jesteś pewien że chcesz przywrócić z tego backupu. Wszystkie pliki powrócą do stanu z backupu.",
|
||||||
"currentBackups": "Backupy Teraz",
|
"currentBackups": "Backupy Teraz",
|
||||||
|
"default": "Podstawowa kopia zapasowa",
|
||||||
|
"defaultExplain": "Kopia zapasowa przed jakimikolwiek zmianami. Nie można jej usunąć ani edytować.",
|
||||||
"delete": "Usuń",
|
"delete": "Usuń",
|
||||||
"destroyBackup": "Zniszcz Backup \" + file_to_del + \"?",
|
"destroyBackup": "Zniszcz Backup \" + file_to_del + \"?",
|
||||||
"download": "Pobierz",
|
"download": "Pobierz",
|
||||||
|
"edit": "Edytuj",
|
||||||
|
"enabled": "Włączony",
|
||||||
"excludedBackups": "Wykluczone ścieżki: ",
|
"excludedBackups": "Wykluczone ścieżki: ",
|
||||||
"excludedChoose": "Wybierz ścieżki do wykluczenia z backupu",
|
"excludedChoose": "Wybierz ścieżki do wykluczenia z backupu",
|
||||||
"exclusionsTitle": "Wykluczenia backupu",
|
"exclusionsTitle": "Wykluczenia backupu",
|
||||||
|
"failed": "Nieudany!",
|
||||||
"maxBackups": "Maks. Backupów",
|
"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ść)",
|
"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",
|
"options": "Opcje",
|
||||||
"path": "Nazwa pliku",
|
"path": "Nazwa pliku",
|
||||||
"restore": "Przywróć",
|
"restore": "Przywróć",
|
||||||
"restoring": "Przywracanie backupu. To trochę zajmie. Bądź cierpliwy.",
|
"restoring": "Przywracanie backupu. To trochę zajmie. Bądź cierpliwy.",
|
||||||
|
"run": "Wykonaj kopię zapasową",
|
||||||
"save": "Zapisz",
|
"save": "Zapisz",
|
||||||
"shutdown": "Wyłącz serwer na czas backupu",
|
"shutdown": "Wyłącz serwer na czas backupu",
|
||||||
"size": "Rozmiar",
|
"size": "Rozmiar",
|
||||||
|
"standby": "Gotowy",
|
||||||
|
"status": "Status",
|
||||||
|
"storage": "Lokalizacja kopii zapasowych",
|
||||||
"storageLocation": "Ścieżka zapisywania",
|
"storageLocation": "Ścieżka zapisywania",
|
||||||
"storageLocationDesc": "Gdzie chcesz trzymać backupy?"
|
"storageLocationDesc": "Gdzie chcesz trzymać backupy?"
|
||||||
},
|
},
|
||||||
@ -492,6 +507,7 @@
|
|||||||
},
|
},
|
||||||
"serverSchedules": {
|
"serverSchedules": {
|
||||||
"action": "Akcja",
|
"action": "Akcja",
|
||||||
|
"actionId": "Zaznacz zadanie podwładne",
|
||||||
"areYouSure": "Usuń zaplanowane (zadanie)?",
|
"areYouSure": "Usuń zaplanowane (zadanie)?",
|
||||||
"cancel": "Anuluj",
|
"cancel": "Anuluj",
|
||||||
"cannotSee": "Nie widzisz wszystkiego?",
|
"cannotSee": "Nie widzisz wszystkiego?",
|
||||||
@ -652,6 +668,9 @@
|
|||||||
"userTheme": "Wygląd interfejsu",
|
"userTheme": "Wygląd interfejsu",
|
||||||
"uses": "Ilość użyć (-1==Bez limitu)"
|
"uses": "Ilość użyć (-1==Bez limitu)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "Hasło jest zbyt krótkie. Hasło musi posiadać minimum 8 znaków."
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "Usunąć ten webhook?",
|
"areYouSureDel": "Usunąć ten webhook?",
|
||||||
"areYouSureRun": "Przetestować ten webhook?",
|
"areYouSureRun": "Przetestować ten webhook?",
|
||||||
|
@ -301,10 +301,12 @@
|
|||||||
"serversDesc": "เซิร์ฟเวอร์ที่บทบาทนี้ได้รับอนุญาตให้เข้าถึง"
|
"serversDesc": "เซิร์ฟเวอร์ที่บทบาทนี้ได้รับอนุญาตให้เข้าถึง"
|
||||||
},
|
},
|
||||||
"serverBackups": {
|
"serverBackups": {
|
||||||
|
"actions": "คำสั่งด่วน",
|
||||||
"after": "ส่งคำสั่งหลังการสำรองข้อมูล",
|
"after": "ส่งคำสั่งหลังการสำรองข้อมูล",
|
||||||
"backupAtMidnight": "คุณต้องการสำรองข้อมูลอัตโนมัติตอนเที่ยงคืนหรือไม่?",
|
"backupAtMidnight": "คุณต้องการสำรองข้อมูลอัตโนมัติตอนเที่ยงคืนหรือไม่?",
|
||||||
"backupNow": "สำรองข้อมูลตอนนี้!",
|
"backupNow": "สำรองข้อมูลตอนนี้!",
|
||||||
"backupTask": "เริ่มการสำรองข้อมูลแล้ว",
|
"backupTask": "เริ่มการสำรองข้อมูลแล้ว",
|
||||||
|
"backups": "ข้อมูลสำรองเซิร์ฟเวอร์",
|
||||||
"before": "ส่งคำสั่งก่อนการสำรองข้อมูล",
|
"before": "ส่งคำสั่งก่อนการสำรองข้อมูล",
|
||||||
"cancel": "ยกเลิก",
|
"cancel": "ยกเลิก",
|
||||||
"clickExclude": "คลิกเพื่อเลือกการยกเว้น",
|
"clickExclude": "คลิกเพื่อเลือกการยกเว้น",
|
||||||
@ -313,21 +315,34 @@
|
|||||||
"confirmDelete": "คุณต้องการลบข้อมูลสำรองนี้หรือไม่ สิ่งนี้ไม่สามารถยกเลิกได้",
|
"confirmDelete": "คุณต้องการลบข้อมูลสำรองนี้หรือไม่ สิ่งนี้ไม่สามารถยกเลิกได้",
|
||||||
"confirmRestore": "คุณแน่ใจหรือไม่ว่าต้องการกู้คืนจากข้อมูลสำรองนี้ ไฟล์เซิร์ฟเวอร์ปัจจุบันทั้งหมดจะเปลี่ยนเป็นแบบสำรองและจะไม่สามารถกู้คืนได้",
|
"confirmRestore": "คุณแน่ใจหรือไม่ว่าต้องการกู้คืนจากข้อมูลสำรองนี้ ไฟล์เซิร์ฟเวอร์ปัจจุบันทั้งหมดจะเปลี่ยนเป็นแบบสำรองและจะไม่สามารถกู้คืนได้",
|
||||||
"currentBackups": "ไฟล์สำรองข้อมูลปัจจุบัน",
|
"currentBackups": "ไฟล์สำรองข้อมูลปัจจุบัน",
|
||||||
|
"default": "ข้อมูลสำรองเริ่มต้น",
|
||||||
|
"defaultExplain": "ข้อมูลสำรองที่ Crafty จะใช้ก่อนการอัพเดต สิ่งนี้ไม่สามารถเปลี่ยนแปลงหรือลบได้",
|
||||||
"delete": "ลบ",
|
"delete": "ลบ",
|
||||||
"destroyBackup": "คุณต้องการทำลายข้อมูลสำรอง \" + file_to_del + \"หรือไม่",
|
"destroyBackup": "คุณต้องการทำลายข้อมูลสำรอง \" + file_to_del + \"หรือไม่",
|
||||||
"download": "ดาวน์โหลด",
|
"download": "ดาวน์โหลด",
|
||||||
|
"edit": "แก้ไข",
|
||||||
|
"enabled": "เปิดใช้งาน",
|
||||||
"excludedBackups": "เส้นทางที่ยกเว้น: ",
|
"excludedBackups": "เส้นทางที่ยกเว้น: ",
|
||||||
"excludedChoose": "เลือกเส้นทางที่คุณต้องการยกเว้นจากการสำรองข้อมูลของคุณ",
|
"excludedChoose": "เลือกเส้นทางที่คุณต้องการยกเว้นจากการสำรองข้อมูลของคุณ",
|
||||||
"exclusionsTitle": "ข้อยกเว้นการสำรองข้อมูล",
|
"exclusionsTitle": "ข้อยกเว้นการสำรองข้อมูล",
|
||||||
|
"failed": "ล้มเหลว",
|
||||||
"maxBackups": "ต้องการเก็บข้อมูลสำรองกี่ครั้ง?",
|
"maxBackups": "ต้องการเก็บข้อมูลสำรองกี่ครั้ง?",
|
||||||
"maxBackupsDesc": "Crafty จะไม่เก็บข้อมูลสำรองมากกว่า N รายการ โดยจะลบข้อมูลสำรองที่เก่าที่สุด (ป้อน 0 เพื่อเก็บทั้งหมด)",
|
"maxBackupsDesc": "Crafty จะไม่เก็บข้อมูลสำรองมากกว่า N รายการ โดยจะลบข้อมูลสำรองที่เก่าที่สุด (ป้อน 0 เพื่อเก็บทั้งหมด)",
|
||||||
|
"myBackup": "ข้อมูลสำรองใหม่ของฉัน",
|
||||||
|
"name": "ชื่อ",
|
||||||
|
"newBackup": "สร้างข้อมูลสำรองใหม่",
|
||||||
|
"no-backup": "ไม่มีการสำรองข้อมูล หากต้องการตั้งค่าการสำรองข้อมูลใหม่ กรุณากด สร้างข้อมูลสำรองใหม่",
|
||||||
"options": "ตัวเลือก",
|
"options": "ตัวเลือก",
|
||||||
"path": "เส้นทาง",
|
"path": "เส้นทาง",
|
||||||
"restore": "คืนค่า",
|
"restore": "คืนค่า",
|
||||||
"restoring": "กำลังกู้คืนข้อมูลสำรอง การดำเนินการนี้อาจใช้เวลาสักครู่ กรุณาอดทนรออย่างใจเย็น",
|
"restoring": "กำลังกู้คืนข้อมูลสำรอง การดำเนินการนี้อาจใช้เวลาสักครู่ กรุณาอดทนรออย่างใจเย็น",
|
||||||
|
"run": "เริ่มทำงานไฟล์สำรอง",
|
||||||
"save": "บันทึก",
|
"save": "บันทึก",
|
||||||
"shutdown": "ปิดเซิร์ฟเวอร์ตามระยะเวลาของการสำรองข้อมูล",
|
"shutdown": "ปิดเซิร์ฟเวอร์ตามระยะเวลาของการสำรองข้อมูล",
|
||||||
"size": "ขนาด",
|
"size": "ขนาด",
|
||||||
|
"standby": "พร้อมใช้งาน",
|
||||||
|
"status": "สถานะ",
|
||||||
|
"storage": "พื้นที่จัดเก็บข้อมูล",
|
||||||
"storageLocation": "สถานที่จัดเก็บ",
|
"storageLocation": "สถานที่จัดเก็บ",
|
||||||
"storageLocationDesc": "คุณต้องการสำรองข้อมูลไว้ที่ไหน?"
|
"storageLocationDesc": "คุณต้องการสำรองข้อมูลไว้ที่ไหน?"
|
||||||
},
|
},
|
||||||
@ -492,6 +507,7 @@
|
|||||||
},
|
},
|
||||||
"serverSchedules": {
|
"serverSchedules": {
|
||||||
"action": "การกระทำ",
|
"action": "การกระทำ",
|
||||||
|
"actionId": "เลือกลูกของการกระทำ",
|
||||||
"areYouSure": "ลบงานที่กำหนดเวลาไว้?",
|
"areYouSure": "ลบงานที่กำหนดเวลาไว้?",
|
||||||
"cancel": "ยกเลิก",
|
"cancel": "ยกเลิก",
|
||||||
"cannotSee": "ไม่เห็นอะไรเลยใช่ใหม?",
|
"cannotSee": "ไม่เห็นอะไรเลยใช่ใหม?",
|
||||||
@ -652,6 +668,9 @@
|
|||||||
"userTheme": "ธีม UI",
|
"userTheme": "ธีม UI",
|
||||||
"uses": "จำนวนการใช้งานที่อนุญาต (-1==ไม่มีขีดจำกัด)"
|
"uses": "จำนวนการใช้งานที่อนุญาต (-1==ไม่มีขีดจำกัด)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "รหัสผ่านสั้นเกินไป จำนวนตัวอักขระขั้นต่ำ: 8"
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "คุณแน่ใจหรือไม่ว่าต้องการลบ Webhook นี้?",
|
"areYouSureDel": "คุณแน่ใจหรือไม่ว่าต้องการลบ Webhook นี้?",
|
||||||
"areYouSureRun": "คุณแน่ใจหรือไม่ว่าต้องการทดสอบ Webhook นี้?",
|
"areYouSureRun": "คุณแน่ใจหรือไม่ว่าต้องการทดสอบ Webhook นี้?",
|
||||||
|
@ -301,10 +301,12 @@
|
|||||||
"serversDesc": "bu rolün erişmesine izin verilen sunucular"
|
"serversDesc": "bu rolün erişmesine izin verilen sunucular"
|
||||||
},
|
},
|
||||||
"serverBackups": {
|
"serverBackups": {
|
||||||
|
"actions": "Eylemler",
|
||||||
"after": "Yedeklemeden sonra bir komut çalıştır",
|
"after": "Yedeklemeden sonra bir komut çalıştır",
|
||||||
"backupAtMidnight": "Gece yarısında otomatik yedekleme yapılsın mı?",
|
"backupAtMidnight": "Gece yarısında otomatik yedekleme yapılsın mı?",
|
||||||
"backupNow": "Backup Now!",
|
"backupNow": "Backup Now!",
|
||||||
"backupTask": "Bir yedekleme görevi başlatıldı.",
|
"backupTask": "Bir yedekleme görevi başlatıldı.",
|
||||||
|
"backups": "Sunucu Yedekleri",
|
||||||
"before": "Yedeklemeden önce bir komut çalıştır",
|
"before": "Yedeklemeden önce bir komut çalıştır",
|
||||||
"cancel": "İptal",
|
"cancel": "İptal",
|
||||||
"clickExclude": "İstisnaları seçmek için tıklayın",
|
"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.",
|
"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.",
|
"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",
|
"currentBackups": "Mevcut Yedekmeler",
|
||||||
|
"default": "Varsayılan Yedek",
|
||||||
|
"defaultExplain": "Crafty'nin güncellemelerden önce kullanacağı yedek. Bu değiştirilemez ya da silinemez.",
|
||||||
"delete": "Sil",
|
"delete": "Sil",
|
||||||
"destroyBackup": "\" + file_to_del + \" yedeklemesi yok edilsin mi?",
|
"destroyBackup": "\" + file_to_del + \" yedeklemesi yok edilsin mi?",
|
||||||
"download": "İndir",
|
"download": "İndir",
|
||||||
|
"edit": "Düzenle",
|
||||||
|
"enabled": "Etkin",
|
||||||
"excludedBackups": "Hariç Tutulan Yollar: ",
|
"excludedBackups": "Hariç Tutulan Yollar: ",
|
||||||
"excludedChoose": "Yedeklemelerinizden hariç tutmak istediğiniz yolları seçin",
|
"excludedChoose": "Yedeklemelerinizden hariç tutmak istediğiniz yolları seçin",
|
||||||
"exclusionsTitle": "Yedekleme İstisnaları",
|
"exclusionsTitle": "Yedekleme İstisnaları",
|
||||||
|
"failed": "Başarısız",
|
||||||
"maxBackups": "Maksimum Yedekleme Sayısı",
|
"maxBackups": "Maksimum Yedekleme Sayısı",
|
||||||
"maxBackupsDesc": "Crafty N yedeklemeden fazlasını saklamayacak, en eskisini silecektir (tümünü saklamak için 0 girin)",
|
"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",
|
"options": "Seçenekler",
|
||||||
"path": "Dosya Yolu",
|
"path": "Dosya Yolu",
|
||||||
"restore": "Geri Yükleme",
|
"restore": "Geri Yükleme",
|
||||||
"restoring": "Yedekleme geri yükleniyor. Bu biraz zaman alabilir. Lütfen sabırlı olun.",
|
"restoring": "Yedekleme geri yükleniyor. Bu biraz zaman alabilir. Lütfen sabırlı olun.",
|
||||||
|
"run": "Yedeği Çalıştır",
|
||||||
"save": "Kaydet",
|
"save": "Kaydet",
|
||||||
"shutdown": "Yedekleme süresince sunucuyu kapat",
|
"shutdown": "Yedekleme süresince sunucuyu kapat",
|
||||||
"size": "Boyut",
|
"size": "Boyut",
|
||||||
|
"standby": "Beklemede",
|
||||||
|
"status": "Durum",
|
||||||
|
"storage": "Depolama Konumu",
|
||||||
"storageLocation": "Depolama Konumu",
|
"storageLocation": "Depolama Konumu",
|
||||||
"storageLocationDesc": "Yedekmeleri nerede saklamak istiyorsunuz?"
|
"storageLocationDesc": "Yedekmeleri nerede saklamak istiyorsunuz?"
|
||||||
},
|
},
|
||||||
@ -492,6 +507,7 @@
|
|||||||
},
|
},
|
||||||
"serverSchedules": {
|
"serverSchedules": {
|
||||||
"action": "Eylem",
|
"action": "Eylem",
|
||||||
|
"actionId": "Alt Eylem Seçiniz",
|
||||||
"areYouSure": "Zamanlanmış Görev Silinsin mi?",
|
"areYouSure": "Zamanlanmış Görev Silinsin mi?",
|
||||||
"cancel": "İptal",
|
"cancel": "İptal",
|
||||||
"cannotSee": "Her şeyi göremiyor musun?",
|
"cannotSee": "Her şeyi göremiyor musun?",
|
||||||
@ -652,6 +668,9 @@
|
|||||||
"userTheme": "UI Teması",
|
"userTheme": "UI Teması",
|
||||||
"uses": "İzin verilen kullanım sayısı (-1==Sınır Yok)"
|
"uses": "İzin verilen kullanım sayısı (-1==Sınır Yok)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "Şifre çok kısa. Şifre en az 8 karakter olmalı."
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "Bu webhooku silmek istediğinizden emin misiniz?",
|
"areYouSureDel": "Bu webhooku silmek istediğinizden emin misiniz?",
|
||||||
"areYouSureRun": "Bu webhooku test etmek istediğinizden emin misiniz?",
|
"areYouSureRun": "Bu webhooku test etmek istediğinizden emin misiniz?",
|
||||||
|
@ -301,10 +301,12 @@
|
|||||||
"serversDesc": "сервери які доступні для цієї ролі"
|
"serversDesc": "сервери які доступні для цієї ролі"
|
||||||
},
|
},
|
||||||
"serverBackups": {
|
"serverBackups": {
|
||||||
|
"actions": "Дії",
|
||||||
"after": "Виконати команду після завершення бекапу",
|
"after": "Виконати команду після завершення бекапу",
|
||||||
"backupAtMidnight": "Авто-бекап опівночі?",
|
"backupAtMidnight": "Авто-бекап опівночі?",
|
||||||
"backupNow": "Запустити бекап!",
|
"backupNow": "Запустити бекап!",
|
||||||
"backupTask": "Бекап запущено.",
|
"backupTask": "Бекап запущено.",
|
||||||
|
"backups": "Сервер Бекапів",
|
||||||
"before": "Виконати команду перед початком бекапу",
|
"before": "Виконати команду перед початком бекапу",
|
||||||
"cancel": "Відмінити",
|
"cancel": "Відмінити",
|
||||||
"clickExclude": "Додати винятки",
|
"clickExclude": "Додати винятки",
|
||||||
@ -313,21 +315,34 @@
|
|||||||
"confirmDelete": "Ви дійсно бажаєте видати бекап? Ця дія незворотня.",
|
"confirmDelete": "Ви дійсно бажаєте видати бекап? Ця дія незворотня.",
|
||||||
"confirmRestore": "Ви впевненні що бажаєте відновити даний бекап? При відновленні сервер буде вимкнуто та відновлено за допомогою даного бекапу, минулі файли будуть втрачені!",
|
"confirmRestore": "Ви впевненні що бажаєте відновити даний бекап? При відновленні сервер буде вимкнуто та відновлено за допомогою даного бекапу, минулі файли будуть втрачені!",
|
||||||
"currentBackups": "Поточні бекапи",
|
"currentBackups": "Поточні бекапи",
|
||||||
|
"default": "Звичайний Бекап",
|
||||||
|
"defaultExplain": "Бекап цього Crafty буде створений перед оновленням. Це не можна змінити чи видалити.",
|
||||||
"delete": "Видалити",
|
"delete": "Видалити",
|
||||||
"destroyBackup": "Видалити бекап \" + file_to_del + \"?",
|
"destroyBackup": "Видалити бекап \" + file_to_del + \"?",
|
||||||
"download": "Завантажити",
|
"download": "Завантажити",
|
||||||
|
"edit": "Редагувати",
|
||||||
|
"enabled": "Увімкненно",
|
||||||
"excludedBackups": "Винятки: ",
|
"excludedBackups": "Винятки: ",
|
||||||
"excludedChoose": "Виберіть папки які бажаєте додати у винятки",
|
"excludedChoose": "Виберіть папки які бажаєте додати у винятки",
|
||||||
"exclusionsTitle": "Бекап винятки",
|
"exclusionsTitle": "Бекап винятки",
|
||||||
|
"failed": "Помилка",
|
||||||
"maxBackups": "Максимум бекапів",
|
"maxBackups": "Максимум бекапів",
|
||||||
"maxBackupsDesc": "Crafty не зможе зберігати більше ніж N бекапів, видалятиме старі (введіть 0 для зберігання усіх бекапів)",
|
"maxBackupsDesc": "Crafty не зможе зберігати більше ніж N бекапів, видалятиме старі (введіть 0 для зберігання усіх бекапів)",
|
||||||
|
"myBackup": "Мій новий бекап",
|
||||||
|
"name": "Назва",
|
||||||
|
"newBackup": "Створити новий бекап",
|
||||||
|
"no-backup": "Немає бекапів. Щоб створити бекап, натисніть кнопку Мій новий Бекап",
|
||||||
"options": "Налаштування",
|
"options": "Налаштування",
|
||||||
"path": "Шлях",
|
"path": "Шлях",
|
||||||
"restore": "Відновити",
|
"restore": "Відновити",
|
||||||
"restoring": "Відновлення бекапу. Це може зайняти деякий час. Будь ласка будьте терплячі.",
|
"restoring": "Відновлення бекапу. Це може зайняти деякий час. Будь ласка будьте терплячі.",
|
||||||
|
"run": "Запустити бекап",
|
||||||
"save": "Зберегти",
|
"save": "Зберегти",
|
||||||
"shutdown": "Вимикати сервер на час бекапу",
|
"shutdown": "Вимикати сервер на час бекапу",
|
||||||
"size": "Розмір",
|
"size": "Розмір",
|
||||||
|
"standby": "Очікування",
|
||||||
|
"status": "Статус",
|
||||||
|
"storage": "Місце збереження",
|
||||||
"storageLocation": "Місце зберігання",
|
"storageLocation": "Місце зберігання",
|
||||||
"storageLocationDesc": "Де ви бажаєте зберігати бекапи?"
|
"storageLocationDesc": "Де ви бажаєте зберігати бекапи?"
|
||||||
},
|
},
|
||||||
@ -492,6 +507,7 @@
|
|||||||
},
|
},
|
||||||
"serverSchedules": {
|
"serverSchedules": {
|
||||||
"action": "Дія",
|
"action": "Дія",
|
||||||
|
"actionId": "Вибрати дочірню дію",
|
||||||
"areYouSure": "Видалити заплановане завдання?",
|
"areYouSure": "Видалити заплановане завдання?",
|
||||||
"cancel": "Відмінити",
|
"cancel": "Відмінити",
|
||||||
"cannotSee": "Нічого не бачите?",
|
"cannotSee": "Нічого не бачите?",
|
||||||
@ -652,6 +668,9 @@
|
|||||||
"userTheme": "Тема інтерфейсу",
|
"userTheme": "Тема інтерфейсу",
|
||||||
"uses": "Дозволена кількість використань(-1==Без ліміту)"
|
"uses": "Дозволена кількість використань(-1==Без ліміту)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "Пароль, надто короткий. Мінімальна довжина: 8 символів"
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "Ви впевнені, що хочете видалити цей Вебхук?",
|
"areYouSureDel": "Ви впевнені, що хочете видалити цей Вебхук?",
|
||||||
"areYouSureRun": "Ви впевнені, що хочете перевірити цей Вебхук?",
|
"areYouSureRun": "Ви впевнені, що хочете перевірити цей Вебхук?",
|
||||||
|
@ -301,10 +301,12 @@
|
|||||||
"serversDesc": "此角色允许访问的服务器"
|
"serversDesc": "此角色允许访问的服务器"
|
||||||
},
|
},
|
||||||
"serverBackups": {
|
"serverBackups": {
|
||||||
|
"actions": "操作",
|
||||||
"after": "备份后运行指令",
|
"after": "备份后运行指令",
|
||||||
"backupAtMidnight": "午夜自动备份?",
|
"backupAtMidnight": "午夜自动备份?",
|
||||||
"backupNow": "现在备份!",
|
"backupNow": "现在备份!",
|
||||||
"backupTask": "一个备份任务已开始。",
|
"backupTask": "一个备份任务已开始。",
|
||||||
|
"backups": "服务器备份",
|
||||||
"before": "备份前运行指令",
|
"before": "备份前运行指令",
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"clickExclude": "点击来选择排除项",
|
"clickExclude": "点击来选择排除项",
|
||||||
@ -313,21 +315,34 @@
|
|||||||
"confirmDelete": "您想要删除这个备份吗?此操作不能撤销。",
|
"confirmDelete": "您想要删除这个备份吗?此操作不能撤销。",
|
||||||
"confirmRestore": "你确定要从此备份恢复吗?所有现存的服务器文件将更改到备份时的状态,并且无法撤销。",
|
"confirmRestore": "你确定要从此备份恢复吗?所有现存的服务器文件将更改到备份时的状态,并且无法撤销。",
|
||||||
"currentBackups": "现有备份",
|
"currentBackups": "现有备份",
|
||||||
|
"default": "默认备份",
|
||||||
|
"defaultExplain": "Crafty 在更新前会使用的备份。此项目不能被更改或删除。",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"destroyBackup": "删除备份 \" + file_to_del + \"?",
|
"destroyBackup": "删除备份 \" + file_to_del + \"?",
|
||||||
"download": "下载",
|
"download": "下载",
|
||||||
|
"edit": "编辑",
|
||||||
|
"enabled": "已启用",
|
||||||
"excludedBackups": "排除的路径:",
|
"excludedBackups": "排除的路径:",
|
||||||
"excludedChoose": "选择您希望从您的备份中排除的路径",
|
"excludedChoose": "选择您希望从您的备份中排除的路径",
|
||||||
"exclusionsTitle": "备份排除项",
|
"exclusionsTitle": "备份排除项",
|
||||||
|
"failed": "失败",
|
||||||
"maxBackups": "最大备份数量",
|
"maxBackups": "最大备份数量",
|
||||||
"maxBackupsDesc": "Crafty 不会存储多于 N 个备份,并且会删除最旧的备份(输入 0 以保留所有备份)",
|
"maxBackupsDesc": "Crafty 不会存储多于 N 个备份,并且会删除最旧的备份(输入 0 以保留所有备份)",
|
||||||
|
"myBackup": "我的新备份",
|
||||||
|
"name": "名称",
|
||||||
|
"newBackup": "创建新备份",
|
||||||
|
"no-backup": "暂无备份。请点击“新备份”以创建一个新的备份配置。",
|
||||||
"options": "选项",
|
"options": "选项",
|
||||||
"path": "路径",
|
"path": "路径",
|
||||||
"restore": "恢复",
|
"restore": "恢复",
|
||||||
"restoring": "正在恢复备份。这需要一点时间。请耐心等待。",
|
"restoring": "正在恢复备份。这需要一点时间。请耐心等待。",
|
||||||
|
"run": "运行备份",
|
||||||
"save": "保存",
|
"save": "保存",
|
||||||
"shutdown": "在备份期间停止服务器",
|
"shutdown": "在备份期间停止服务器",
|
||||||
"size": "大小",
|
"size": "大小",
|
||||||
|
"standby": "等候",
|
||||||
|
"status": "状态",
|
||||||
|
"storage": "存储位置",
|
||||||
"storageLocation": "存储位置",
|
"storageLocation": "存储位置",
|
||||||
"storageLocationDesc": "您想要在哪里存储备份?"
|
"storageLocationDesc": "您想要在哪里存储备份?"
|
||||||
},
|
},
|
||||||
@ -492,6 +507,7 @@
|
|||||||
},
|
},
|
||||||
"serverSchedules": {
|
"serverSchedules": {
|
||||||
"action": "操作",
|
"action": "操作",
|
||||||
|
"actionId": "选择子操作",
|
||||||
"areYouSure": "删除计划任务?",
|
"areYouSure": "删除计划任务?",
|
||||||
"cancel": "取消",
|
"cancel": "取消",
|
||||||
"cannotSee": "什么都看不到?",
|
"cannotSee": "什么都看不到?",
|
||||||
@ -653,6 +669,9 @@
|
|||||||
"userTheme": "UI 主题",
|
"userTheme": "UI 主题",
|
||||||
"uses": "使用次数限制(-1==无限制)"
|
"uses": "使用次数限制(-1==无限制)"
|
||||||
},
|
},
|
||||||
|
"validators": {
|
||||||
|
"passLength": "密码过短。最短长度:8"
|
||||||
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"areYouSureDel": "您确定要删除此 webhook 吗?",
|
"areYouSureDel": "您确定要删除此 webhook 吗?",
|
||||||
"areYouSureRun": "您确定要测试此 webhook 吗?",
|
"areYouSureRun": "您确定要测试此 webhook 吗?",
|
||||||
|
@ -13,9 +13,9 @@ psutil==5.9.5
|
|||||||
pyOpenSSL==24.0.0
|
pyOpenSSL==24.0.0
|
||||||
pyjwt==2.8.0
|
pyjwt==2.8.0
|
||||||
PyYAML==6.0.1
|
PyYAML==6.0.1
|
||||||
requests==2.31.0
|
requests==2.32.0
|
||||||
termcolor==1.1
|
termcolor==1.1
|
||||||
tornado==6.3.3
|
tornado==6.4.1
|
||||||
tzlocal==5.1
|
tzlocal==5.1
|
||||||
jsonschema==4.19.1
|
jsonschema==4.19.1
|
||||||
orjson==3.9.15
|
orjson==3.9.15
|
||||||
|
Loading…
Reference in New Issue
Block a user