mirror of
https://gitlab.com/crafty-controller/crafty-4.git
synced 2025-12-05 01:10:15 +00:00
Merge branch 'dev' into refactor/download-api-v2
This commit is contained in:
11
CHANGELOG.md
11
CHANGELOG.md
@@ -1,13 +1,18 @@
|
||||
# Changelog
|
||||
## --- [4.4.12] - 2025/TBD
|
||||
|
||||
### Refactor
|
||||
- Modularize helpers (file / crypto) ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/795))
|
||||
### New features
|
||||
TBD
|
||||
- Snapshot Style Backups ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/795))
|
||||
### Bug fixes
|
||||
TBD
|
||||
- Fixed inconsistent password schema error handling ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/868))
|
||||
- Fix cvalidation to stop users being able to disable their own account ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/870))
|
||||
### Tweaks
|
||||
TBD
|
||||
### Lang
|
||||
TBD
|
||||
- Fixed grammar, spelling/capitalization, and sentence structure issues ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/876))
|
||||
- Removed 2 unused statements ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/876))
|
||||
<br><br>
|
||||
|
||||
## --- [4.4.11] - 2025/06/15
|
||||
|
||||
@@ -5,7 +5,7 @@ from prometheus_client import CollectorRegistry, Gauge
|
||||
|
||||
from app.classes.models.management import HelpersManagement, HelpersWebhooks
|
||||
from app.classes.models.servers import HelperServers
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ import typing as t
|
||||
|
||||
from app.classes.models.roles import HelperRoles
|
||||
from app.classes.models.server_permissions import PermissionsServers, RoleServers
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -6,12 +6,13 @@ import pathlib
|
||||
import typing as t
|
||||
|
||||
from app.classes.controllers.roles_controller import RolesController
|
||||
from app.classes.shared.file_helpers import FileHelpers
|
||||
from app.classes.helpers.file_helpers import FileHelpers
|
||||
|
||||
from app.classes.shared.singleton import Singleton
|
||||
from app.classes.shared.server import ServerInstance
|
||||
from app.classes.shared.console import Console
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.shared.backup_mgr import BackupManager
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
from app.classes.shared.main_models import DatabaseShortcuts
|
||||
|
||||
from app.classes.minecraft.stats import Stats
|
||||
@@ -39,6 +40,9 @@ class ServersController(metaclass=Singleton):
|
||||
self.stats = Stats(self.helper, self)
|
||||
self.web_sock = WebSocketManager()
|
||||
self.server_subpage = {}
|
||||
self.backups_mgr = BackupManager(
|
||||
self.helper, self.file_helper, self.management_helper
|
||||
)
|
||||
|
||||
# **********************************************************************************
|
||||
# Generic Servers Methods
|
||||
@@ -235,6 +239,7 @@ class ServersController(metaclass=Singleton):
|
||||
self.management_helper,
|
||||
self.stats,
|
||||
self.file_helper,
|
||||
self.backups_mgr,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import re
|
||||
from datetime import timezone, datetime, timedelta
|
||||
import logging
|
||||
import pyotp
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
from app.classes.models.users import HelperUsers
|
||||
from app.classes.models.totp import HelperTOTP
|
||||
|
||||
@@ -100,10 +100,15 @@ class TOTPController:
|
||||
"""clears out totp codes older than 1 minute when one is sent"""
|
||||
now = datetime.now(tz=timezone.utc)
|
||||
# Clean up expired entries reclaim some memory
|
||||
for key in list(self.used_totp_codes.keys()):
|
||||
for item in list(self.used_totp_codes[key].keys()):
|
||||
if now - self.used_totp_codes[key][item] > timedelta(seconds=60):
|
||||
del self.used_totp_codes[key][item]
|
||||
for key, totp_dict in self.used_totp_codes.items():
|
||||
for item, timestamp in totp_dict.items():
|
||||
if now - timestamp > timedelta(seconds=60):
|
||||
# needs to ref the self var to remove expired entries
|
||||
del self.used_totp_codes[ # pylint: disable=unnecessary-dict-index-lookup
|
||||
key
|
||||
][
|
||||
item
|
||||
]
|
||||
|
||||
def verify_user_totp(
|
||||
self, user_id: int, totp_id: str, totp_name: str, totp_code: str
|
||||
@@ -188,9 +193,9 @@ class TOTPController:
|
||||
minutes. This runs on a schedule every 24 hours from tasks.py
|
||||
"""
|
||||
logger.info("Checking and purging stale pending MFA")
|
||||
for totp_id in list(self.pending_totp.keys()):
|
||||
if datetime.now(tz=timezone.utc) - self.pending_totp[totp_id][
|
||||
"iat"
|
||||
] > timedelta(minutes=60):
|
||||
for totp_id, data in self.pending_totp.items():
|
||||
if datetime.now(tz=timezone.utc) - data[totp_id]["iat"] > timedelta(
|
||||
minutes=60
|
||||
):
|
||||
del self.pending_totp[totp_id] # Safe deletion
|
||||
logger.info(f"Deleted expired entry {totp_id}")
|
||||
|
||||
@@ -62,9 +62,14 @@ class UsersController:
|
||||
"password": {
|
||||
"type": "string",
|
||||
"minLength": self.helper.minimum_password_length,
|
||||
"pattern": "(?=.*[^0-9])",
|
||||
"examples": ["crafty"],
|
||||
"title": "Password",
|
||||
"error": "passLength",
|
||||
"error": {
|
||||
"minLength": "passLength",
|
||||
"type": "numbericPassword",
|
||||
"pattern": "numbericPassword",
|
||||
},
|
||||
},
|
||||
"email": {
|
||||
"type": "string",
|
||||
|
||||
123
app/classes/helpers/cryptography_helper.py
Normal file
123
app/classes/helpers/cryptography_helper.py
Normal file
@@ -0,0 +1,123 @@
|
||||
import base64
|
||||
import binascii
|
||||
from hashlib import blake2b
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class CryptoHelper:
|
||||
def __init__(self, helper):
|
||||
self.helper = helper
|
||||
self.test = "hello world"
|
||||
|
||||
def say_hello_world(self):
|
||||
print(self.test)
|
||||
|
||||
@staticmethod
|
||||
def blake2b_hash_bytes(bytes_to_hash: bytes) -> bytes:
|
||||
"""
|
||||
Hashes given bytes with blake2b hash function, returns digest as bytes.
|
||||
|
||||
Args:
|
||||
bytes_to_hash: Bytes to be hashed.
|
||||
|
||||
Returns: Digest of bytes hashed
|
||||
"""
|
||||
blake2 = blake2b()
|
||||
blake2.update(bytes_to_hash)
|
||||
return blake2.digest()
|
||||
|
||||
@staticmethod
|
||||
def blake2_hash_file(path_to_file: Path) -> bytes:
|
||||
"""
|
||||
Hashes given file at path with blake2b hash function, returns digest as bytes.
|
||||
|
||||
Args:
|
||||
path_to_file: Path to file to hash.
|
||||
|
||||
Returns: Digest of file.
|
||||
"""
|
||||
blake2 = blake2b()
|
||||
try:
|
||||
with path_to_file.open("rb") as file_to_hash:
|
||||
while True:
|
||||
# Reads file 20kb at a time.
|
||||
data = file_to_hash.read(20_000)
|
||||
# Stops reading if at end of file.
|
||||
if not data:
|
||||
break
|
||||
blake2.update(data)
|
||||
# Activity can raise FileNotFound, PermissionError, or OSError.
|
||||
except OSError as why:
|
||||
raise RuntimeError(f"Error accessing file: {path_to_file}.") from why
|
||||
return blake2.digest()
|
||||
|
||||
@staticmethod
|
||||
def bytes_to_b64(input_bytes: bytes) -> str:
|
||||
"""
|
||||
Converts input bytes to base64 encoded string.
|
||||
|
||||
Args:
|
||||
input_bytes: Input bytes for conversion.
|
||||
|
||||
Returns: String of base64 encoded bytes.
|
||||
|
||||
"""
|
||||
# base64.b64encode(input_bytes).decode("UTF-8") appends a trailing new line.
|
||||
# That newline is getting pulled off of the string before returning it.
|
||||
return base64.b64encode(input_bytes).decode("UTF-8").rstrip("\n")
|
||||
|
||||
@staticmethod
|
||||
def b64_to_bytes(input_str: str) -> bytes:
|
||||
"""
|
||||
Converts base64 encoded string to bytes.
|
||||
|
||||
Args:
|
||||
input_str: Base64 bytes encodes as a string.
|
||||
|
||||
Returns: Bytes from base64 encoded string.
|
||||
|
||||
"""
|
||||
return base64.b64decode(input_str)
|
||||
|
||||
@staticmethod
|
||||
def bytes_to_hex(input_bytes: bytes) -> str:
|
||||
"""
|
||||
Converts input bytes to hex encoded string.
|
||||
|
||||
Args:
|
||||
input_bytes: Bytes to be encoded as hex string.
|
||||
|
||||
Returns: Bytes encoded as hex string.
|
||||
|
||||
"""
|
||||
return input_bytes.hex()
|
||||
|
||||
@staticmethod
|
||||
def str_to_b64(input_str: str) -> str:
|
||||
"""
|
||||
Given source string, converts to base64 encoded string.
|
||||
|
||||
Args:
|
||||
input_str: String to convert.
|
||||
|
||||
Returns: b64 encoded string.
|
||||
|
||||
"""
|
||||
return base64.b64encode(input_str.encode("UTF-8")).decode("UTF-8").rstrip("\n")
|
||||
|
||||
@staticmethod
|
||||
def b64_to_str(input_b64: str) -> str:
|
||||
"""
|
||||
Converts b64 encoded string to string. Can raise RuntimeError if code cannot be
|
||||
decoded.
|
||||
|
||||
Args:
|
||||
input_b64: Base64 encoded string.
|
||||
|
||||
Returns: Decoded string from b64.
|
||||
|
||||
"""
|
||||
try:
|
||||
return base64.b64decode(input_b64).decode("UTF-8")
|
||||
except (RuntimeError, UnicodeError, binascii.Error) as why:
|
||||
raise RuntimeError(f"Unable to decode {input_b64} to b64.") from why
|
||||
1056
app/classes/helpers/file_helpers.py
Normal file
1056
app/classes/helpers/file_helpers.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,37 +1,37 @@
|
||||
import base64
|
||||
import contextlib
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import ctypes
|
||||
import html
|
||||
import itertools
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import pathlib
|
||||
import re
|
||||
import secrets
|
||||
import shlex
|
||||
import shutil
|
||||
import socket
|
||||
import string
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
import uuid
|
||||
import string
|
||||
import base64
|
||||
import socket
|
||||
import secrets
|
||||
import logging
|
||||
import html
|
||||
import pathlib
|
||||
import ctypes
|
||||
import shutil
|
||||
import shlex
|
||||
import subprocess
|
||||
import itertools
|
||||
from socket import gethostname
|
||||
from contextlib import redirect_stderr, suppress
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from socket import gethostname
|
||||
|
||||
import libgravatar
|
||||
from packaging import version as pkg_version
|
||||
from cryptography import x509
|
||||
from cryptography.x509.oid import NameOID
|
||||
from cryptography.hazmat.primitives import serialization, hashes
|
||||
from cryptography.hazmat.primitives import hashes, serialization
|
||||
from cryptography.hazmat.primitives.asymmetric import rsa
|
||||
from cryptography.x509.oid import NameOID
|
||||
from packaging import version as pkg_version
|
||||
|
||||
|
||||
from app.classes.shared.null_writer import NullWriter
|
||||
from app.classes.helpers.cryptography_helper import CryptoHelper
|
||||
from app.classes.shared.console import Console
|
||||
from app.classes.shared.installer import installer
|
||||
from app.classes.shared.null_writer import NullWriter
|
||||
from app.classes.shared.translation import Translation
|
||||
|
||||
with redirect_stderr(NullWriter()):
|
||||
@@ -108,8 +108,8 @@ CONFIG_CATEGORIES = {
|
||||
|
||||
try:
|
||||
import requests
|
||||
from requests import get
|
||||
from argon2 import PasswordHasher
|
||||
from requests import get
|
||||
|
||||
except ModuleNotFoundError as err:
|
||||
logger.critical(f"Import Error: Unable to load {err.name} module", exc_info=True)
|
||||
@@ -148,6 +148,7 @@ class Helpers:
|
||||
self.ignored_names = ["crafty_managed.txt"]
|
||||
self.crafty_starting = False
|
||||
self.minimum_password_length = 8
|
||||
self.crypto_helper = CryptoHelper(self)
|
||||
|
||||
self.theme_list = self.load_themes()
|
||||
|
||||
@@ -170,11 +171,11 @@ class Helpers:
|
||||
if response.status_code == 200:
|
||||
remote_version = pkg_version.parse(json.loads(response.text)[0]["name"])
|
||||
|
||||
# Get local version data from the file and parse the semver
|
||||
local_version = pkg_version.parse(self.get_version_string())
|
||||
# Get local version data from the file and parse the semver
|
||||
local_version = pkg_version.parse(self.get_version_string())
|
||||
|
||||
if remote_version > local_version:
|
||||
return remote_version
|
||||
if remote_version > local_version:
|
||||
return remote_version
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Unable to check for new crafty version! \n{e}")
|
||||
@@ -332,8 +333,7 @@ class Helpers:
|
||||
def check_file_perms(path):
|
||||
try:
|
||||
with open(path, "r", encoding="utf-8"):
|
||||
pass
|
||||
logger.info(f"{path} is readable")
|
||||
logger.info(f"{path} is readable")
|
||||
return True
|
||||
except PermissionError:
|
||||
return False
|
||||
@@ -414,7 +414,11 @@ class Helpers:
|
||||
def check_port(server_port):
|
||||
try:
|
||||
ip = get("https://api.ipify.org", timeout=1).content.decode("utf8")
|
||||
except:
|
||||
except Exception as e:
|
||||
logger.info(
|
||||
f"Unable to connect to api.ipify.org, \
|
||||
falling back to google.com: {e}"
|
||||
)
|
||||
ip = "google.com"
|
||||
a_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
a_socket.settimeout(20.0)
|
||||
@@ -621,23 +625,35 @@ class Helpers:
|
||||
|
||||
return mounts
|
||||
|
||||
def is_subdir(self, child_path, parent_path):
|
||||
@staticmethod
|
||||
def is_subdir(child_path: str, parent_path: str) -> bool:
|
||||
"""
|
||||
Checks if given child_path is a subdirectory of given parent_path. Returns True
|
||||
or False.
|
||||
|
||||
Args:
|
||||
child_path: Child path to check.
|
||||
parent_path: Possible parent path of child path.
|
||||
|
||||
Returns:
|
||||
True if child_path is a subdirectory of parent_path. Otherwise, False.
|
||||
|
||||
"""
|
||||
server_path = os.path.realpath(child_path)
|
||||
root_dir = os.path.realpath(parent_path)
|
||||
|
||||
if self.is_os_windows():
|
||||
try:
|
||||
relative = os.path.relpath(server_path, root_dir)
|
||||
except:
|
||||
# Windows will crash out if two paths are on different
|
||||
# Drives We can happily return false if this is the case.
|
||||
# Since two different drives will not be relative to eachother.
|
||||
return False
|
||||
else:
|
||||
try:
|
||||
relative = os.path.relpath(server_path, root_dir)
|
||||
if relative.startswith(os.pardir):
|
||||
return False
|
||||
|
||||
if relative.startswith(os.pardir):
|
||||
except ValueError:
|
||||
# Windows will crash out if two paths are on different Drives We can happily
|
||||
# return false if this is the case. Since two different drives will not be
|
||||
# relative to each-other.
|
||||
return False
|
||||
|
||||
# If all checks pass, child path must be a child of parent.
|
||||
return True
|
||||
|
||||
def set_setting(self, key, new_value):
|
||||
@@ -1265,7 +1281,7 @@ class Helpers:
|
||||
random_generator() = G8sjO2
|
||||
random_generator(3, abcdef) = adf
|
||||
"""
|
||||
return "".join(secrets.choice(chars) for x in range(size))
|
||||
return "".join(secrets.choice(chars) for _ in range(size))
|
||||
|
||||
@staticmethod
|
||||
def is_os_windows():
|
||||
@@ -1319,16 +1335,19 @@ class Helpers:
|
||||
rel = os.path.join(folder, raw_filename)
|
||||
dpath = os.path.join(folder, filename)
|
||||
if os.path.isdir(rel):
|
||||
output += f"""<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="radio" name="root_path" value="{dpath}">
|
||||
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}" data-name="{filename}" onclick="getDirView(event)">
|
||||
<i class="text-info far fa-folder"></i>
|
||||
<i class="text-info far fa-folder-open"></i>
|
||||
{filename}
|
||||
</span>
|
||||
</input></div><li>
|
||||
\n"""
|
||||
# lines below had too long warnings disabled for readability
|
||||
output += (
|
||||
f"""<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">""" # pylint: disable=line-too-long
|
||||
+ """<input type="radio" name="root_path" value="{dpath}">"""
|
||||
+ """<span id="{dpath}span" class="files-tree-title" data-path="{dpath}" data-name="{filename}" onclick="getDirView(event)">""" # pylint: disable=line-too-long
|
||||
+ """ <i class="text-info far fa-folder"></i>"""
|
||||
+ """ <i class="text-info far fa-folder-open"></i>"""
|
||||
+ """ {filename}"""
|
||||
+ """ </span>"""
|
||||
+ """</input></div><li>"""
|
||||
+ """\n"""
|
||||
)
|
||||
return output
|
||||
|
||||
@staticmethod
|
||||
@@ -1341,15 +1360,18 @@ class Helpers:
|
||||
rel = os.path.join(folder, raw_filename)
|
||||
dpath = os.path.join(folder, filename)
|
||||
if os.path.isdir(rel):
|
||||
output += f"""<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="radio" name="root_path" value="{dpath}">
|
||||
<span id="{dpath}span" class="files-tree-title" data-path="{dpath}" data-name="{filename}" onclick="getDirView(event)">
|
||||
<i class="text-info far fa-folder"></i>
|
||||
<i class="text-info far fa-folder-open"></i>
|
||||
{filename}
|
||||
</span>
|
||||
</input></div><li>"""
|
||||
# lines below had too long warnings disabled for readability
|
||||
output += (
|
||||
f"""<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">""" # pylint: disable=line-too-long
|
||||
+ """<input type="radio" name="root_path" value="{dpath}">"""
|
||||
+ """<span id="{dpath}span" class="files-tree-title" data-path="{dpath}" data-name="{filename}" onclick="getDirView(event)">""" # pylint: disable=line-too-long
|
||||
+ """ <i class="text-info far fa-folder"></i>"""
|
||||
+ """ <i class="text-info far fa-folder-open"></i>"""
|
||||
+ """ {filename}"""
|
||||
+ """ </span>"""
|
||||
+ """</input></div><li>"""
|
||||
)
|
||||
return output
|
||||
|
||||
@staticmethod
|
||||
@@ -1382,9 +1404,9 @@ class Helpers:
|
||||
decoded_bytes = base64.b64decode(prop["value"])
|
||||
decoded_str = decoded_bytes.decode("utf-8")
|
||||
texture_json = json.loads(decoded_str)
|
||||
skin_url = texture_json["textures"]["SKIN"]["url"]
|
||||
skin_response = requests.get(skin_url, stream=True, timeout=10)
|
||||
if skin_response.status_code == 200:
|
||||
return base64.b64encode(skin_response.content)
|
||||
skin_url = texture_json["textures"]["SKIN"]["url"]
|
||||
skin_response = requests.get(skin_url, stream=True, timeout=10)
|
||||
if skin_response.status_code == 200:
|
||||
return base64.b64encode(skin_response.content)
|
||||
else:
|
||||
return
|
||||
@@ -8,7 +8,7 @@ import requests
|
||||
|
||||
from app.classes.controllers.servers_controller import ServersController
|
||||
from app.classes.models.server_permissions import PermissionsServers
|
||||
from app.classes.shared.file_helpers import FileHelpers
|
||||
from app.classes.helpers.file_helpers import FileHelpers
|
||||
from app.classes.shared.websocket_manager import WebSocketManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -10,7 +10,7 @@ from app.classes.minecraft.mc_ping import ping
|
||||
from app.classes.models.management import HostStats
|
||||
from app.classes.models.servers import HelperServers
|
||||
from app.classes.shared.null_writer import NullWriter
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
|
||||
with redirect_stderr(NullWriter()):
|
||||
import psutil
|
||||
|
||||
@@ -16,7 +16,7 @@ from app.classes.models.base_model import BaseModel
|
||||
from app.classes.models.users import HelperUsers
|
||||
from app.classes.models.servers import Servers
|
||||
from app.classes.models.server_permissions import PermissionsServers
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
from app.classes.shared.websocket_manager import WebSocketManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -117,6 +117,7 @@ class Backups(BaseModel):
|
||||
default = BooleanField(default=False)
|
||||
status = CharField(default='{"status": "Standby", "message": ""}')
|
||||
enabled = BooleanField(default=True)
|
||||
backup_type = CharField(default="zip_vault")
|
||||
|
||||
class Meta:
|
||||
table_name = "backups"
|
||||
@@ -368,6 +369,7 @@ class HelpersManagement:
|
||||
"after": backup.after,
|
||||
"default": backup.default,
|
||||
"enabled": backup.enabled,
|
||||
"backup_type": backup.backup_type,
|
||||
}
|
||||
else:
|
||||
data = Backups.select().where(Backups.server_id == server_id).execute()
|
||||
|
||||
@@ -12,7 +12,7 @@ from peewee import (
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
|
||||
from app.classes.models.base_model import BaseModel
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import datetime
|
||||
from datetime import timedelta
|
||||
|
||||
from app.classes.models.servers import Servers, HelperServers
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
from app.classes.shared.main_models import DatabaseShortcuts
|
||||
from app.classes.shared.migration import MigrationManager
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ from app.classes.shared.main_models import DatabaseShortcuts
|
||||
from app.classes.models.base_model import BaseModel
|
||||
|
||||
# from app.classes.models.users import Users
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import logging
|
||||
|
||||
from peewee import ForeignKeyField, CharField
|
||||
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
from app.classes.models.users import Users
|
||||
from app.classes.models.base_model import BaseModel
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ from peewee import (
|
||||
)
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
from app.classes.models.base_model import BaseModel
|
||||
from app.classes.models.roles import Roles, HelperRoles
|
||||
|
||||
|
||||
418
app/classes/shared/backup_mgr.py
Normal file
418
app/classes/shared/backup_mgr.py
Normal file
@@ -0,0 +1,418 @@
|
||||
import datetime
|
||||
import io
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import shutil
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
# TZLocal is set as a hidden import on win pipeline
|
||||
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
||||
|
||||
from tzlocal import get_localzone
|
||||
|
||||
from app.classes.helpers.cryptography_helper import CryptoHelper
|
||||
from app.classes.helpers.file_helpers import FileHelpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
from app.classes.models.management import HelpersManagement
|
||||
from app.classes.models.server_permissions import PermissionsServers
|
||||
from app.classes.models.users import HelperUsers
|
||||
from app.classes.shared.console import Console
|
||||
from app.classes.shared.websocket_manager import WebSocketManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BackupManager:
|
||||
|
||||
SNAPSHOT_BACKUP_DATE_FORMAT_STRING = "%Y-%m-%d-%H-%M-%S"
|
||||
|
||||
def __init__(self, helper, file_helper, management_helper):
|
||||
self.helper = helper
|
||||
self.file_helper = file_helper
|
||||
self.management_helper = management_helper
|
||||
try:
|
||||
self.tz = get_localzone()
|
||||
except ZoneInfoNotFoundError as e:
|
||||
logger.error(
|
||||
"Could not capture time zone from system. Falling back to Europe/London"
|
||||
f" error: {e}"
|
||||
)
|
||||
self.tz = ZoneInfo("Europe/London")
|
||||
|
||||
def restore_starter( # pylint: disable=too-many-positional-arguments
|
||||
self, backup_config, backup_location, backup_file, svr_obj, in_place
|
||||
):
|
||||
server_path = svr_obj.settings["path"]
|
||||
if Helpers.validate_traversal(backup_location, backup_file):
|
||||
if svr_obj.check_running():
|
||||
svr_obj.stop_server()
|
||||
if backup_config["backup_type"] != "zip_vault":
|
||||
self.snapshot_restore(backup_config, backup_file, svr_obj)
|
||||
else:
|
||||
if not in_place: # If user does not want to backup in place we will
|
||||
# clean the server dir
|
||||
for item in os.listdir(server_path):
|
||||
if (
|
||||
os.path.isdir(os.path.join(server_path, item))
|
||||
and item != "db_stats"
|
||||
):
|
||||
self.file_helper.del_dirs(os.path.join(server_path, item))
|
||||
else:
|
||||
self.file_helper.del_file(os.path.join(server_path, item))
|
||||
self.file_helper.restore_archive(backup_location, server_path)
|
||||
|
||||
def backup_starter(self, backup_config, server):
|
||||
"""Notify users of backup starting, and start the backup.
|
||||
|
||||
Args:
|
||||
backup_config (_type_): _description_
|
||||
server (_type_): Server object to backup
|
||||
"""
|
||||
# Notify users of backup starting
|
||||
logger.info(f"Starting server {server.name} (ID {server.server_id}) backup")
|
||||
server_users = PermissionsServers.get_server_user_list(server.server_id)
|
||||
# Alert the start of the backup to the authorized users.
|
||||
for user in server_users:
|
||||
WebSocketManager().broadcast_user(
|
||||
user,
|
||||
"notification",
|
||||
self.helper.translation.translate(
|
||||
"notify", "backupStarted", HelperUsers.get_user_lang_by_id(user)
|
||||
).format(server.name),
|
||||
)
|
||||
time.sleep(3)
|
||||
|
||||
# Start the backup
|
||||
if backup_config.get("backup_type", "zip_vault") == "zip_vault":
|
||||
self.zip_vault(backup_config, server)
|
||||
else:
|
||||
self.snapshot_backup(backup_config, server)
|
||||
|
||||
def zip_vault(self, backup_config, server):
|
||||
|
||||
# Adjust the location to include the backup ID for destination.
|
||||
backup_location = os.path.join(
|
||||
backup_config["backup_location"], backup_config["backup_id"]
|
||||
)
|
||||
|
||||
# Check if the backup location even exists.
|
||||
if not backup_location:
|
||||
Console.critical("No backup path found. Canceling")
|
||||
return None
|
||||
|
||||
self.helper.ensure_dir_exists(backup_location)
|
||||
|
||||
try:
|
||||
backup_filename = (
|
||||
f"{backup_location}/"
|
||||
f"""{datetime.datetime.now()
|
||||
.astimezone(self.tz)
|
||||
.strftime('%Y-%m-%d_%H-%M-%S')}"""
|
||||
)
|
||||
logger.info(
|
||||
f"Creating backup of server {server.name}"
|
||||
f" (ID#{server.server_id}, path={server.server_path}) "
|
||||
f"at '{backup_filename}'"
|
||||
)
|
||||
excluded_dirs = HelpersManagement.get_excluded_backup_dirs(
|
||||
backup_config["backup_id"]
|
||||
)
|
||||
server_dir = Helpers.get_os_understandable_path(server.server_path)
|
||||
|
||||
self.file_helper.make_backup(
|
||||
Helpers.get_os_understandable_path(backup_filename),
|
||||
server_dir,
|
||||
excluded_dirs,
|
||||
server.server_id,
|
||||
backup_config["backup_id"],
|
||||
backup_config["backup_name"],
|
||||
backup_config["compress"],
|
||||
)
|
||||
|
||||
self.remove_old_backups(backup_config, server)
|
||||
|
||||
logger.info(f"Backup of server: {server.name} completed")
|
||||
results = {
|
||||
"percent": 100,
|
||||
"total_files": 0,
|
||||
"current_file": 0,
|
||||
"backup_id": backup_config["backup_id"],
|
||||
}
|
||||
if len(WebSocketManager().clients) > 0:
|
||||
WebSocketManager().broadcast_page_params(
|
||||
"/panel/server_detail",
|
||||
{"id": str(server.server_id)},
|
||||
"backup_status",
|
||||
results,
|
||||
)
|
||||
server_users = PermissionsServers.get_server_user_list(server.server_id)
|
||||
for user in server_users:
|
||||
WebSocketManager().broadcast_user(
|
||||
user,
|
||||
"notification",
|
||||
self.helper.translation.translate(
|
||||
"notify",
|
||||
"backupComplete",
|
||||
HelperUsers.get_user_lang_by_id(user),
|
||||
).format(server.name),
|
||||
)
|
||||
# pause to let people read message.
|
||||
HelpersManagement.update_backup_config(
|
||||
backup_config["backup_id"],
|
||||
{"status": json.dumps({"status": "Standby", "message": ""})},
|
||||
)
|
||||
time.sleep(5)
|
||||
except Exception as e:
|
||||
self.fail_backup(e, backup_config, server)
|
||||
|
||||
@staticmethod
|
||||
def fail_backup(why: Exception, backup_config: dict, server) -> None:
|
||||
"""
|
||||
Fails the backup if an error is encountered during the backup.
|
||||
|
||||
Args:
|
||||
why: Exception raised to fail backup.
|
||||
backup_config: Backup config dict
|
||||
server: Server object.
|
||||
|
||||
Returns: None
|
||||
|
||||
"""
|
||||
logger.exception(
|
||||
"Failed to create backup of server"
|
||||
f" {server.name} (ID {server.server_id})"
|
||||
)
|
||||
results: dict = {
|
||||
"percent": 100,
|
||||
"total_files": 0,
|
||||
"current_file": 0,
|
||||
"backup_id": backup_config["backup_id"],
|
||||
}
|
||||
if len(WebSocketManager().clients) > 0:
|
||||
WebSocketManager().broadcast_page_params(
|
||||
"/panel/server_detail",
|
||||
{"id": str(server.server_id)},
|
||||
"backup_status",
|
||||
results,
|
||||
)
|
||||
|
||||
HelpersManagement.update_backup_config(
|
||||
backup_config["backup_id"],
|
||||
{"status": json.dumps({"status": "Failed", "message": f"{why}"})},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def list_backups(backup_config: dict, server_id) -> list:
|
||||
if not backup_config:
|
||||
logger.info(
|
||||
f"Error putting backup file list for server with ID: {server_id}"
|
||||
)
|
||||
return []
|
||||
backup_location = os.path.join(
|
||||
backup_config["backup_location"],
|
||||
backup_config["backup_id"],
|
||||
)
|
||||
if backup_config["backup_type"] == "snapshot":
|
||||
backup_location = os.path.join(
|
||||
backup_config["backup_location"], "snapshot_backups", "manifests"
|
||||
)
|
||||
if not Helpers.check_path_exists(
|
||||
Helpers.get_os_understandable_path(backup_location)
|
||||
):
|
||||
return []
|
||||
files = Helpers.get_human_readable_files_sizes(
|
||||
Helpers.list_dir_by_date(
|
||||
Helpers.get_os_understandable_path(backup_location)
|
||||
)
|
||||
)
|
||||
if backup_config["backup_type"] == "snapshot":
|
||||
return [
|
||||
{
|
||||
"path": os.path.relpath(
|
||||
f["path"],
|
||||
start=Helpers.get_os_understandable_path(backup_location),
|
||||
),
|
||||
"size": "",
|
||||
}
|
||||
for f in files
|
||||
if f["path"].endswith(".manifest")
|
||||
]
|
||||
return [
|
||||
{
|
||||
"path": os.path.relpath(
|
||||
f["path"],
|
||||
start=Helpers.get_os_understandable_path(backup_location),
|
||||
),
|
||||
"size": f["size"],
|
||||
}
|
||||
for f in files
|
||||
if f["path"].endswith(".zip")
|
||||
]
|
||||
|
||||
def remove_old_backups(self, backup_config, server):
|
||||
while (
|
||||
len(self.list_backups(backup_config, server)) > backup_config["max_backups"]
|
||||
and backup_config["max_backups"] > 0
|
||||
):
|
||||
backup_list = self.list_backups(backup_config, server.server_id)
|
||||
oldfile = backup_list[0]
|
||||
oldfile_path = os.path.join(
|
||||
backup_config["backup_location"],
|
||||
backup_config["backup_id"],
|
||||
oldfile["path"],
|
||||
)
|
||||
logger.info(f"Removing old backup '{oldfile['path']}'")
|
||||
os.remove(Helpers.get_os_understandable_path(oldfile_path))
|
||||
|
||||
def snapshot_backup(self, backup_config, server) -> None:
|
||||
"""
|
||||
Creates snapshot style backup of server. No file will be saved more than once
|
||||
over all backups. Designed to enable encryption of files and s3 compatability in
|
||||
the future.
|
||||
|
||||
Args:
|
||||
backup_config: Backup config to use.
|
||||
server: Server instance.
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
logger.info(f"Starting snapshot style backup for {server.name}")
|
||||
|
||||
# Create backup variables.
|
||||
use_compression = backup_config["compress"]
|
||||
source_path = Path(server.server_path)
|
||||
backup_repository_path = (
|
||||
Path(backup_config["backup_location"]) / "snapshot_backups"
|
||||
)
|
||||
backup_time = datetime.datetime.now()
|
||||
backup_time_filesafe = backup_time.strftime(
|
||||
self.SNAPSHOT_BACKUP_DATE_FORMAT_STRING
|
||||
)
|
||||
backup_manifest_path = (
|
||||
backup_repository_path / "manifests" / f"{backup_time_filesafe}.manifest"
|
||||
)
|
||||
|
||||
excluded_dirs = HelpersManagement.get_excluded_backup_dirs(
|
||||
backup_config["backup_id"]
|
||||
)
|
||||
list_of_files: list[Path] = FileHelpers.discover_files(
|
||||
source_path, excluded_dirs
|
||||
)
|
||||
|
||||
# Create manifest file
|
||||
try:
|
||||
backup_manifest_path.parent.mkdir(exist_ok=True, parents=True)
|
||||
manifest_file: io.TextIOWrapper = backup_manifest_path.open("w+")
|
||||
except OSError as why:
|
||||
self.fail_backup(why, backup_config, server)
|
||||
return
|
||||
|
||||
# Write manifest file version.
|
||||
manifest_file.write("00\n")
|
||||
|
||||
# Iterate over source files and save into backup repository.
|
||||
for file in list_of_files:
|
||||
try:
|
||||
file_hash = CryptoHelper.blake2_hash_file(file)
|
||||
self.file_helper.save_file(
|
||||
file, backup_repository_path, file_hash, use_compression
|
||||
)
|
||||
# May return OSError if file path is not logical.
|
||||
file_local_path = self.file_helper.get_local_path_with_base(
|
||||
file, source_path
|
||||
)
|
||||
except OSError as why:
|
||||
manifest_file.close()
|
||||
backup_manifest_path.unlink(missing_ok=True)
|
||||
self.fail_backup(why, backup_config, server)
|
||||
return
|
||||
|
||||
# Write saved file into manifest.
|
||||
manifest_file.write(
|
||||
f"{CryptoHelper.bytes_to_b64(file_hash)}:"
|
||||
f"{CryptoHelper.str_to_b64(file_local_path)}\n"
|
||||
)
|
||||
|
||||
manifest_file.close()
|
||||
|
||||
self.file_helper.clean_old_backups(
|
||||
backup_config["max_backups"], backup_repository_path
|
||||
)
|
||||
|
||||
def snapshot_restore(
|
||||
self, backup_config: {str}, backup_manifest_filename: str, server
|
||||
) -> None:
|
||||
"""
|
||||
Restores snapshot style backup.
|
||||
|
||||
Args:
|
||||
backup_config: Backup Config.
|
||||
backup_manifest_filename: Filename of backup manifest.
|
||||
server: Server config.
|
||||
|
||||
Returns:
|
||||
"""
|
||||
destination_path = Path(server.settings["path"])
|
||||
source_manifest_path = Path(
|
||||
backup_config["backup_location"],
|
||||
"snapshot_backups",
|
||||
"manifests",
|
||||
backup_manifest_filename,
|
||||
)
|
||||
# /snapshot_backups/manifests/manifest.manifest
|
||||
backup_repository_path = source_manifest_path.parent.parent
|
||||
|
||||
# Ensure destination is not a file.
|
||||
if destination_path.is_file():
|
||||
raise RuntimeError(
|
||||
f"Destination path {destination_path} for restore is a file."
|
||||
)
|
||||
|
||||
# Ensure target is empty.
|
||||
if destination_path.exists():
|
||||
shutil.rmtree(destination_path)
|
||||
|
||||
# Ensure target directory exists.
|
||||
destination_path.mkdir(exist_ok=True, parents=True)
|
||||
|
||||
# Open backup manifest.
|
||||
try:
|
||||
backup_manifest_file: io.TextIOWrapper = source_manifest_path.open(
|
||||
"r", encoding="utf-8"
|
||||
)
|
||||
except OSError as why:
|
||||
raise RuntimeError(
|
||||
f"Unable to open backup manifest at {source_manifest_path}."
|
||||
) from why
|
||||
|
||||
# Ensure backup manifest is of readable version.
|
||||
if backup_manifest_file.readline() != "00\n":
|
||||
backup_manifest_file.close()
|
||||
raise RuntimeError(
|
||||
f"Backup manifest file {source_manifest_path} is of unreadable "
|
||||
f"version."
|
||||
)
|
||||
|
||||
# Begin restoring files from manifest.
|
||||
for file_hash_and_path in backup_manifest_file:
|
||||
hash_and_local_path: list[str] = file_hash_and_path.split(":")
|
||||
file_hash: bytes = CryptoHelper.b64_to_bytes(hash_and_local_path[0])
|
||||
recovered_file_path: Path = Path(
|
||||
destination_path,
|
||||
CryptoHelper.b64_to_str(input_b64=hash_and_local_path[1]),
|
||||
).resolve()
|
||||
|
||||
# Recover file
|
||||
try:
|
||||
self.file_helper.read_file(
|
||||
file_hash, recovered_file_path, backup_repository_path
|
||||
)
|
||||
except RuntimeError as why:
|
||||
backup_manifest_file.close()
|
||||
raise RuntimeError(f"Unable to recover file {file_hash}.") from why
|
||||
|
||||
# Restore complete, close backup manifest file.
|
||||
backup_manifest_file.close()
|
||||
@@ -7,7 +7,7 @@ import getpass
|
||||
from app.classes.shared.console import Console
|
||||
from app.classes.shared.import3 import Import3
|
||||
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
from app.classes.shared.tasks import TasksManager
|
||||
from app.classes.shared.migration import MigrationManager
|
||||
from app.classes.shared.main_controller import Controller
|
||||
|
||||
@@ -1,434 +0,0 @@
|
||||
import os
|
||||
import psutil
|
||||
import shutil
|
||||
import logging
|
||||
import pathlib
|
||||
import tempfile
|
||||
import zipfile
|
||||
import hashlib
|
||||
from typing import BinaryIO
|
||||
import mimetypes
|
||||
from zipfile import ZipFile, ZIP_DEFLATED, ZIP_STORED
|
||||
import urllib.request
|
||||
import ssl
|
||||
import time
|
||||
import certifi
|
||||
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.shared.console import Console
|
||||
from app.classes.shared.websocket_manager import WebSocketManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FileHelpers:
|
||||
allowed_quotes = ['"', "'", "`"]
|
||||
|
||||
def __init__(self, helper):
|
||||
self.helper: Helpers = helper
|
||||
self.mime_types = mimetypes.MimeTypes()
|
||||
|
||||
@staticmethod
|
||||
def ssl_get_file(
|
||||
url, out_path, out_file, max_retries=3, backoff_factor=2, headers=None
|
||||
):
|
||||
"""
|
||||
Downloads a file from a given URL using HTTPS with SSL context verification,
|
||||
retries with exponential backoff and providing download progress feedback.
|
||||
|
||||
Parameters:
|
||||
- url (str): The URL of the file to download. Must start with "https".
|
||||
- out_path (str): The local path where the file will be saved.
|
||||
- out_file (str): The name of the file to save the downloaded content as.
|
||||
- max_retries (int, optional): The maximum number of retry attempts
|
||||
in case of download failure. Defaults to 3.
|
||||
- backoff_factor (int, optional): The factor by which the wait time
|
||||
increases after each failed attempt. Defaults to 2.
|
||||
- headers (dict, optional):
|
||||
A dictionary of HTTP headers to send with the request.
|
||||
|
||||
Returns:
|
||||
- bool: True if the download was successful, False otherwise.
|
||||
|
||||
Raises:
|
||||
- urllib.error.URLError: If a URL error occurs during the download.
|
||||
- ssl.SSLError: If an SSL error occurs during the download.
|
||||
Exception: If an unexpected error occurs during the download.
|
||||
|
||||
Note:
|
||||
This method logs critical errors and download progress information.
|
||||
Ensure that the logger is properly configured to capture this information.
|
||||
"""
|
||||
if not url.lower().startswith("https"):
|
||||
logger.error("SSL File Get - Error: URL must start with https.")
|
||||
return False
|
||||
|
||||
ssl_context = ssl.create_default_context(cafile=certifi.where())
|
||||
|
||||
if not headers:
|
||||
headers = {
|
||||
"User-Agent": (
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) "
|
||||
"AppleWebKit/537.36 (KHTML, like Gecko) "
|
||||
"Chrome/58.0.3029.110 Safari/537.3"
|
||||
)
|
||||
}
|
||||
req = urllib.request.Request(url, headers=headers)
|
||||
|
||||
write_path = os.path.join(out_path, out_file)
|
||||
attempt = 0
|
||||
|
||||
logger.info(f"SSL File Get - Requesting remote: {url}")
|
||||
file_path_full = os.path.join(out_path, out_file)
|
||||
logger.info(f"SSL File Get - Download Destination: {file_path_full}")
|
||||
|
||||
while attempt < max_retries:
|
||||
try:
|
||||
with urllib.request.urlopen(req, context=ssl_context) as response:
|
||||
total_size = response.getheader("Content-Length")
|
||||
if total_size:
|
||||
total_size = int(total_size)
|
||||
downloaded = 0
|
||||
with open(write_path, "wb") as file:
|
||||
while True:
|
||||
chunk = response.read(1024 * 1024) # 1 MB
|
||||
if not chunk:
|
||||
break
|
||||
file.write(chunk)
|
||||
downloaded += len(chunk)
|
||||
if total_size:
|
||||
progress = (downloaded / total_size) * 100
|
||||
logger.info(
|
||||
f"SSL File Get - Download progress: {progress:.2f}%"
|
||||
)
|
||||
return True
|
||||
except (urllib.error.URLError, ssl.SSLError) as e:
|
||||
logger.warning(f"SSL File Get - Attempt {attempt+1} failed: {e}")
|
||||
time.sleep(backoff_factor**attempt)
|
||||
except Exception as e:
|
||||
logger.critical(f"SSL File Get - Unexpected error: {e}")
|
||||
return False
|
||||
finally:
|
||||
attempt += 1
|
||||
|
||||
logger.error("SSL File Get - Maximum retries reached. Download failed.")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def del_dirs(path):
|
||||
path = pathlib.Path(path)
|
||||
for sub in path.iterdir():
|
||||
if sub.is_dir():
|
||||
# Delete folder if it is a folder
|
||||
FileHelpers.del_dirs(sub)
|
||||
else:
|
||||
# Delete file if it is a file:
|
||||
try:
|
||||
sub.unlink()
|
||||
except:
|
||||
logger.error(f"Unable to delete file {sub}")
|
||||
try:
|
||||
# This removes the top-level folder:
|
||||
path.rmdir()
|
||||
except Exception as e:
|
||||
logger.error("Unable to remove top level")
|
||||
return e
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def del_file(path):
|
||||
path = pathlib.Path(path)
|
||||
try:
|
||||
logger.debug(f"Deleting file: {path}")
|
||||
# Remove the file
|
||||
os.remove(path)
|
||||
return True
|
||||
except (FileNotFoundError, PermissionError) as e:
|
||||
logger.error(f"Path specified is not a file or does not exist. {path}")
|
||||
return e
|
||||
|
||||
def check_mime_types(self, file_path):
|
||||
m_type, _value = self.mime_types.guess_type(file_path)
|
||||
return m_type
|
||||
|
||||
def has_enough_storage(self):
|
||||
paths = pathlib.Path("C:/")
|
||||
print(repr(paths))
|
||||
disk_stats = psutil.disk_usage(str(paths))
|
||||
print(disk_stats)
|
||||
|
||||
@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
|
||||
def copy_dir(src_path, dest_path, dirs_exist_ok=False):
|
||||
# pylint: disable=unexpected-keyword-arg
|
||||
shutil.copytree(src_path, dest_path, dirs_exist_ok=dirs_exist_ok)
|
||||
|
||||
@staticmethod
|
||||
def copy_file(src_path, dest_path):
|
||||
shutil.copy(src_path, dest_path)
|
||||
|
||||
@staticmethod
|
||||
def move_dir(src_path, dest_path):
|
||||
shutil.move(src_path, dest_path)
|
||||
|
||||
@staticmethod
|
||||
def move_dir_exist(src_path, dest_path):
|
||||
FileHelpers.copy_dir(src_path, dest_path, True)
|
||||
FileHelpers.del_dirs(src_path)
|
||||
|
||||
@staticmethod
|
||||
def move_file(src_path, dest_path):
|
||||
shutil.move(src_path, dest_path)
|
||||
|
||||
@staticmethod
|
||||
def make_archive(path_to_destination, path_to_zip, comment=""):
|
||||
# create a ZipFile object
|
||||
string_path = str(path_to_destination)
|
||||
path_to_destination = string_path
|
||||
if not path_to_destination.endswith(".zip"):
|
||||
path_to_destination += ".zip"
|
||||
with ZipFile(path_to_destination, "w") 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):
|
||||
ziproot = path_to_zip
|
||||
for file in files:
|
||||
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}"
|
||||
)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def make_compressed_archive(path_to_destination, path_to_zip, comment=""):
|
||||
# create a ZipFile object
|
||||
path_to_destination += ".zip"
|
||||
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):
|
||||
ziproot = path_to_zip
|
||||
for file in files:
|
||||
try:
|
||||
logger.info(f"packaging: {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 packaging: {os.path.join(root, file)}!"
|
||||
f" - Error was: {e}"
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
def make_backup(
|
||||
self,
|
||||
path_to_destination,
|
||||
path_to_zip,
|
||||
excluded_dirs,
|
||||
server_id,
|
||||
backup_id,
|
||||
comment="",
|
||||
compressed=None,
|
||||
):
|
||||
# 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,
|
||||
)
|
||||
WebSocketManager().broadcast_page_params(
|
||||
"/panel/edit_backup",
|
||||
{"id": str(server_id)},
|
||||
"backup_status",
|
||||
results,
|
||||
)
|
||||
# Set the compression mode based on the `compressed` parameter
|
||||
compression_mode = ZIP_DEFLATED if compressed else ZIP_STORED
|
||||
with ZipFile(path_to_destination, "w", compression_mode) as zip_file:
|
||||
zip_file.comment = bytes(
|
||||
comment, "utf-8"
|
||||
) # comments over 65535 bytes will be truncated
|
||||
for root, dirs, files in os.walk(path_to_zip, topdown=True):
|
||||
for l_dir in dirs[:]:
|
||||
# make all paths in exclusions a unix style slash
|
||||
# to match directories.
|
||||
if str(os.path.join(root, l_dir)).replace("\\", "/") in ex_replace:
|
||||
dirs.remove(l_dir)
|
||||
ziproot = path_to_zip
|
||||
# iterate through list of files
|
||||
for file in files:
|
||||
# check if file/dir is in exclusions list.
|
||||
# Only proceed if not exluded.
|
||||
if (
|
||||
str(os.path.join(root, file)).replace("\\", "/")
|
||||
not in ex_replace
|
||||
and file != "crafty.sqlite"
|
||||
):
|
||||
try:
|
||||
logger.debug(f"backing up: {os.path.join(root, file)}")
|
||||
# add trailing slash to zip root dir if not windows.
|
||||
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}"
|
||||
)
|
||||
# debug logging for exlusions list
|
||||
else:
|
||||
logger.debug(f"Found {file} in exclusion list. Skipping...")
|
||||
|
||||
try:
|
||||
# add current file bytes to total bytes.
|
||||
total_bytes += os.path.getsize(os.path.join(root, file))
|
||||
except FileNotFoundError as why:
|
||||
logger.debug(f"Failed to calculate file size with error {why}")
|
||||
# calcualte percentage based off total size and current archive size
|
||||
percent = round((total_bytes / dir_bytes) * 100, 2)
|
||||
# package results
|
||||
results = {
|
||||
"percent": percent,
|
||||
"total_files": self.helper.human_readable_file_size(dir_bytes),
|
||||
"backup_id": backup_id,
|
||||
}
|
||||
# send status results to page.
|
||||
WebSocketManager().broadcast_page_params(
|
||||
"/panel/server_detail",
|
||||
{"id": str(server_id)},
|
||||
"backup_status",
|
||||
results,
|
||||
)
|
||||
WebSocketManager().broadcast_page_params(
|
||||
"/panel/edit_backup",
|
||||
{"id": str(server_id)},
|
||||
"backup_status",
|
||||
results,
|
||||
)
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def restore_archive(archive_location, destination):
|
||||
with zipfile.ZipFile(archive_location, "r") as zip_ref:
|
||||
zip_ref.extractall(destination)
|
||||
|
||||
@staticmethod
|
||||
def unzip_file(zip_path, server_update=False):
|
||||
ignored_names = [
|
||||
"server.properties",
|
||||
"permissions.json",
|
||||
"allowlist.json",
|
||||
]
|
||||
# Get directory without zipfile name
|
||||
new_dir = pathlib.Path(zip_path).parents[0]
|
||||
# make sure we're able to access the zip file
|
||||
if Helpers.check_file_perms(zip_path) and os.path.isfile(zip_path):
|
||||
# make sure the directory we're unzipping this to exists
|
||||
Helpers.ensure_dir_exists(new_dir)
|
||||
# we'll make a temporary directory to unzip this to.
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
try:
|
||||
with zipfile.ZipFile(zip_path, "r") as zip_ref:
|
||||
# we'll extract this to the temp dir using zipfile module
|
||||
zip_ref.extractall(temp_dir)
|
||||
# we'll iterate through the top level directory moving everything
|
||||
# out of the temp directory and into it's final home.
|
||||
for item in os.listdir(temp_dir):
|
||||
# if the file is one of our ignored names we'll skip it
|
||||
if item in ignored_names and server_update:
|
||||
continue
|
||||
# we handle files and dirs differently or we'll crash out.
|
||||
if os.path.isdir(os.path.join(temp_dir, item)):
|
||||
try:
|
||||
FileHelpers.move_dir_exist(
|
||||
os.path.join(temp_dir, item),
|
||||
os.path.join(new_dir, item),
|
||||
)
|
||||
except Exception as ex:
|
||||
logger.error(f"ERROR IN ZIP IMPORT: {ex}")
|
||||
else:
|
||||
try:
|
||||
FileHelpers.move_file(
|
||||
os.path.join(temp_dir, item),
|
||||
os.path.join(new_dir, item),
|
||||
)
|
||||
except Exception as ex:
|
||||
logger.error(f"ERROR IN ZIP IMPORT: {ex}")
|
||||
except Exception as ex:
|
||||
Console.error(ex)
|
||||
else:
|
||||
return "false"
|
||||
return
|
||||
|
||||
def unzip_server(self, zip_path, user_id):
|
||||
if Helpers.check_file_perms(zip_path):
|
||||
temp_dir = tempfile.mkdtemp()
|
||||
with zipfile.ZipFile(zip_path, "r") as zip_ref:
|
||||
# extracts archive to temp directory
|
||||
zip_ref.extractall(temp_dir)
|
||||
if user_id:
|
||||
return temp_dir
|
||||
@@ -6,8 +6,8 @@ import threading
|
||||
|
||||
from app.classes.controllers.server_perms_controller import PermissionsServers
|
||||
from app.classes.controllers.servers_controller import ServersController
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.shared.file_helpers import FileHelpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
from app.classes.helpers.file_helpers import FileHelpers
|
||||
from app.classes.shared.websocket_manager import WebSocketManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -32,8 +32,8 @@ from app.classes.controllers.servers_controller import ServersController
|
||||
from app.classes.controllers.totp_controller import TOTPController
|
||||
from app.classes.shared.authentication import Authentication
|
||||
from app.classes.shared.console import Console
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.shared.file_helpers import FileHelpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
from app.classes.helpers.file_helpers import FileHelpers
|
||||
from app.classes.shared.import_helper import ImportHelpers
|
||||
from app.classes.minecraft.bigbucket import BigBucket
|
||||
from app.classes.shared.websocket_manager import WebSocketManager
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
|
||||
from app.classes.shared.helpers import Helpers # pylint: disable=unused-import
|
||||
from app.classes.helpers.helpers import Helpers # pylint: disable=unused-import
|
||||
from app.classes.shared.console import Console
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -17,7 +17,7 @@ from playhouse.migrate import (
|
||||
)
|
||||
|
||||
from app.classes.shared.console import Console
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -99,7 +99,7 @@ class Migrator(object):
|
||||
"""
|
||||
Cleans the operations.
|
||||
"""
|
||||
self.operations = list()
|
||||
self.operations = []
|
||||
|
||||
def sql(self, sql: str, *params):
|
||||
"""
|
||||
@@ -438,7 +438,7 @@ class MigrationManager(object):
|
||||
"""
|
||||
Reads a migration from a file.
|
||||
"""
|
||||
call_params = dict()
|
||||
call_params = {}
|
||||
if Helpers.is_os_windows() and sys.version_info >= (3, 0):
|
||||
# if system is windows - force utf-8 encoding
|
||||
call_params["encoding"] = "utf-8"
|
||||
|
||||
@@ -32,8 +32,8 @@ from app.classes.models.management import HelpersManagement, HelpersWebhooks
|
||||
from app.classes.models.users import HelperUsers
|
||||
from app.classes.models.server_permissions import PermissionsServers
|
||||
from app.classes.shared.console import Console
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.shared.file_helpers import FileHelpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
from app.classes.helpers.file_helpers import FileHelpers
|
||||
from app.classes.shared.null_writer import NullWriter
|
||||
from app.classes.shared.websocket_manager import WebSocketManager
|
||||
from app.classes.web.webhooks.webhook_factory import WebhookFactory
|
||||
@@ -160,10 +160,13 @@ class ServerInstance:
|
||||
stats: Stats
|
||||
stats_helper: HelperServerStats
|
||||
|
||||
def __init__(self, server_id, helper, management_helper, stats, file_helper):
|
||||
def __init__(
|
||||
self, server_id, helper, management_helper, stats, file_helper, backup_mgr
|
||||
):
|
||||
self.helper = helper
|
||||
self.file_helper = file_helper
|
||||
self.management_helper = management_helper
|
||||
self.backup_mgr = backup_mgr
|
||||
# holders for our process
|
||||
self.process = None
|
||||
self.line = False
|
||||
@@ -1128,16 +1131,52 @@ class ServerInstance:
|
||||
f.write("eula=true")
|
||||
self.run_threaded_server(user_id)
|
||||
|
||||
def server_restore_threader(self, backup_id, backup_file, in_place=False):
|
||||
# import the server again based on zipfile
|
||||
backup_config = HelpersManagement.get_backup_config(backup_id)
|
||||
backup_location = os.path.join(
|
||||
backup_config["backup_location"],
|
||||
backup_config["backup_id"],
|
||||
backup_file,
|
||||
)
|
||||
restore_thread = threading.Thread(
|
||||
target=self.backup_mgr.restore_starter,
|
||||
daemon=True,
|
||||
name=f"backup_{backup_config['backup_id']}",
|
||||
args=[backup_config, backup_location, backup_file, self, in_place],
|
||||
)
|
||||
|
||||
restore_thread.start()
|
||||
|
||||
def server_backup_threader(self, backup_id, update=False):
|
||||
# Check to see if we're already backing up
|
||||
if self.check_backup_by_id(backup_id):
|
||||
return False
|
||||
backup_config = HelpersManagement.get_backup_config(backup_id)
|
||||
if backup_config["before"]:
|
||||
logger.debug(
|
||||
"Found running server and send command option. Sending command"
|
||||
)
|
||||
self.send_command(backup_config["before"])
|
||||
# Pause to let command run
|
||||
time.sleep(5)
|
||||
|
||||
self.was_running = False
|
||||
if backup_config["shutdown"]:
|
||||
logger.info(
|
||||
"Found shutdown preference. Delaying"
|
||||
+ "backup start. Shutting down server."
|
||||
)
|
||||
if not update:
|
||||
if self.check_running():
|
||||
self.stop_server()
|
||||
self.was_running = True
|
||||
|
||||
backup_thread = threading.Thread(
|
||||
target=self.backup_server,
|
||||
daemon=True,
|
||||
name=f"backup_{backup_id}",
|
||||
args=[backup_id, update],
|
||||
name=f"backup_{backup_config['backup_id']}",
|
||||
args=[backup_id],
|
||||
)
|
||||
logger.info(
|
||||
f"Starting Backup Thread for server {self.settings['server_name']}."
|
||||
@@ -1157,8 +1196,7 @@ class ServerInstance:
|
||||
logger.info(f"Backup Thread started for server {self.settings['server_name']}.")
|
||||
|
||||
@callback
|
||||
def backup_server(self, backup_id, update):
|
||||
was_server_running = None
|
||||
def backup_server(self, backup_id):
|
||||
logger.info(f"Starting server {self.name} (ID {self.server_id}) backup")
|
||||
server_users = PermissionsServers.get_server_user_list(self.server_id)
|
||||
# Alert the start of the backup to the authorized users.
|
||||
@@ -1190,127 +1228,16 @@ class ServerInstance:
|
||||
self.send_command(conf["before"])
|
||||
# Pause to let command run
|
||||
time.sleep(5)
|
||||
|
||||
if conf["shutdown"]:
|
||||
self.backup_mgr.backup_starter(conf, self)
|
||||
if conf["after"]:
|
||||
self.send_command(conf["after"])
|
||||
if conf["shutdown"] and self.was_running:
|
||||
logger.info(
|
||||
"Found shutdown preference. Delaying"
|
||||
+ "backup start. Shutting down server."
|
||||
)
|
||||
if not update:
|
||||
was_server_running = False
|
||||
if self.check_running():
|
||||
self.stop_server()
|
||||
was_server_running = True
|
||||
|
||||
self.helper.ensure_dir_exists(backup_location)
|
||||
|
||||
try:
|
||||
backup_filename = (
|
||||
f"{backup_location}/"
|
||||
f"{datetime.datetime.now().astimezone(self.tz).strftime('%Y-%m-%d_%H-%M-%S')}" # pylint: disable=line-too-long
|
||||
)
|
||||
logger.info(
|
||||
f"Creating backup of server '{self.settings['server_name']}'"
|
||||
f" (ID#{self.server_id}, path={self.server_path}) "
|
||||
f"at '{backup_filename}'"
|
||||
)
|
||||
excluded_dirs = HelpersManagement.get_excluded_backup_dirs(backup_id)
|
||||
server_dir = Helpers.get_os_understandable_path(self.settings["path"])
|
||||
|
||||
self.file_helper.make_backup(
|
||||
Helpers.get_os_understandable_path(backup_filename),
|
||||
server_dir,
|
||||
excluded_dirs,
|
||||
self.server_id,
|
||||
backup_id,
|
||||
conf["backup_name"],
|
||||
conf["compress"],
|
||||
)
|
||||
|
||||
while (
|
||||
len(self.list_backups(conf)) > conf["max_backups"]
|
||||
and conf["max_backups"] > 0
|
||||
):
|
||||
backup_list = self.list_backups(conf)
|
||||
oldfile = backup_list[0]
|
||||
oldfile_path = f"{backup_location}/{oldfile['path']}"
|
||||
logger.info(f"Removing old backup '{oldfile['path']}'")
|
||||
os.remove(Helpers.get_os_understandable_path(oldfile_path))
|
||||
|
||||
logger.info(f"Backup of server: {self.name} completed")
|
||||
results = {
|
||||
"percent": 100,
|
||||
"total_files": 0,
|
||||
"current_file": 0,
|
||||
"backup_id": backup_id,
|
||||
}
|
||||
if len(WebSocketManager().clients) > 0:
|
||||
WebSocketManager().broadcast_page_params(
|
||||
"/panel/server_detail",
|
||||
{"id": str(self.server_id)},
|
||||
"backup_status",
|
||||
results,
|
||||
)
|
||||
server_users = PermissionsServers.get_server_user_list(self.server_id)
|
||||
for user in server_users:
|
||||
WebSocketManager().broadcast_user(
|
||||
user,
|
||||
"notification",
|
||||
self.helper.translation.translate(
|
||||
"notify",
|
||||
"backupComplete",
|
||||
HelperUsers.get_user_lang_by_id(user),
|
||||
).format(self.name),
|
||||
)
|
||||
if was_server_running:
|
||||
logger.info(
|
||||
"Backup complete. User had shutdown preference. Starting server."
|
||||
)
|
||||
self.run_threaded_server(HelperUsers.get_user_id_by_name("system"))
|
||||
time.sleep(3)
|
||||
if conf["after"]:
|
||||
if self.check_running():
|
||||
logger.debug(
|
||||
"Found running server and send command option. Sending command"
|
||||
)
|
||||
self.send_command(conf["after"])
|
||||
# pause to let people read message.
|
||||
HelpersManagement.update_backup_config(
|
||||
backup_id,
|
||||
{"status": json.dumps({"status": "Standby", "message": ""})},
|
||||
)
|
||||
time.sleep(5)
|
||||
except Exception as e:
|
||||
logger.exception(
|
||||
f"Failed to create backup of server {self.name} (ID {self.server_id})"
|
||||
)
|
||||
results = {
|
||||
"percent": 100,
|
||||
"total_files": 0,
|
||||
"current_file": 0,
|
||||
"backup_id": backup_id,
|
||||
}
|
||||
if len(WebSocketManager().clients) > 0:
|
||||
WebSocketManager().broadcast_page_params(
|
||||
"/panel/server_detail",
|
||||
{"id": str(self.server_id)},
|
||||
"backup_status",
|
||||
results,
|
||||
)
|
||||
if was_server_running:
|
||||
logger.info(
|
||||
"Backup complete. User had shutdown preference. Starting server."
|
||||
)
|
||||
self.run_threaded_server(HelperUsers.get_user_id_by_name("system"))
|
||||
HelpersManagement.update_backup_config(
|
||||
backup_id,
|
||||
{"status": json.dumps({"status": "Failed", "message": f"{e}"})},
|
||||
"Backup complete. User had shutdown preference. Starting server."
|
||||
)
|
||||
self.run_threaded_server(HelperUsers.get_user_id_by_name("system"))
|
||||
self.set_backup_status()
|
||||
|
||||
def last_backup_status(self):
|
||||
return self.last_backup_failed
|
||||
|
||||
def set_backup_status(self):
|
||||
backups = HelpersManagement.get_backups_by_server(self.server_id, True)
|
||||
alert = False
|
||||
@@ -1319,35 +1246,8 @@ class ServerInstance:
|
||||
alert = True
|
||||
self.last_backup_failed = alert
|
||||
|
||||
def list_backups(self, backup_config: dict) -> list:
|
||||
if not backup_config:
|
||||
logger.info(
|
||||
f"Error putting backup file list for server with ID: {self.server_id}"
|
||||
)
|
||||
return []
|
||||
backup_location = os.path.join(
|
||||
backup_config["backup_location"], backup_config["backup_id"]
|
||||
)
|
||||
if not Helpers.check_path_exists(
|
||||
Helpers.get_os_understandable_path(backup_location)
|
||||
):
|
||||
return []
|
||||
files = Helpers.get_human_readable_files_sizes(
|
||||
Helpers.list_dir_by_date(
|
||||
Helpers.get_os_understandable_path(backup_location)
|
||||
)
|
||||
)
|
||||
return [
|
||||
{
|
||||
"path": os.path.relpath(
|
||||
f["path"],
|
||||
start=Helpers.get_os_understandable_path(backup_location),
|
||||
),
|
||||
"size": f["size"],
|
||||
}
|
||||
for f in files
|
||||
if f["path"].endswith(".zip")
|
||||
]
|
||||
def last_backup_status(self):
|
||||
return self.last_backup_failed
|
||||
|
||||
@callback
|
||||
def jar_update(self):
|
||||
@@ -1432,23 +1332,21 @@ class ServerInstance:
|
||||
self.stop_threaded_server()
|
||||
else:
|
||||
was_started = False
|
||||
ws_params = {
|
||||
"isUpdating": self.check_update(),
|
||||
"server_id": self.server_id,
|
||||
"wasRunning": was_started,
|
||||
}
|
||||
if len(WebSocketManager().clients) > 0:
|
||||
# There are clients
|
||||
self.check_update()
|
||||
message = (
|
||||
'<a data-id="' + str(self.server_id) + '" class=""> UPDATING...</i></a>'
|
||||
)
|
||||
ws_params["string"] = message
|
||||
for user in server_users:
|
||||
WebSocketManager().broadcast_user_page(
|
||||
"/panel/server_detail",
|
||||
user,
|
||||
"update_button_status",
|
||||
{
|
||||
"isUpdating": self.check_update(),
|
||||
"server_id": self.server_id,
|
||||
"wasRunning": was_started,
|
||||
"string": message,
|
||||
},
|
||||
"/panel/server_detail", user, "update_button_status", ws_params
|
||||
)
|
||||
current_executable = os.path.join(
|
||||
Helpers.get_os_understandable_path(self.settings["path"]),
|
||||
|
||||
@@ -16,8 +16,8 @@ from app.classes.models.management import HelpersManagement
|
||||
from app.classes.models.users import HelperUsers
|
||||
from app.classes.controllers.users_controller import UsersController
|
||||
from app.classes.shared.console import Console
|
||||
from app.classes.shared.file_helpers import FileHelpers
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.helpers.file_helpers import FileHelpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
from app.classes.shared.main_controller import Controller
|
||||
from app.classes.web.tornado_handler import Webserver
|
||||
from app.classes.shared.websocket_manager import WebSocketManager
|
||||
|
||||
@@ -8,8 +8,8 @@ import tornado.web
|
||||
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
|
||||
from app.classes.models.server_permissions import EnumPermissionsServer
|
||||
from app.classes.models.users import ApiKeys
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.shared.file_helpers import FileHelpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
from app.classes.helpers.file_helpers import FileHelpers
|
||||
from app.classes.shared.main_controller import Controller
|
||||
from app.classes.shared.translation import Translation
|
||||
from app.classes.shared.main_models import DatabaseShortcuts
|
||||
|
||||
@@ -20,7 +20,7 @@ from app.classes.models.server_permissions import EnumPermissionsServer
|
||||
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
|
||||
from app.classes.models.management import HelpersManagement
|
||||
from app.classes.controllers.roles_controller import RolesController
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
from app.classes.shared.main_models import DatabaseShortcuts
|
||||
from app.classes.web.base_handler import BaseHandler
|
||||
from app.classes.web.webhooks.webhook_factory import WebhookFactory
|
||||
@@ -1252,8 +1252,8 @@ class PanelHandler(BaseHandler):
|
||||
).is_backingup
|
||||
self.controller.servers.refresh_server_settings(server_id)
|
||||
try:
|
||||
page_data["backup_list"] = server.list_backups(
|
||||
page_data["backup_config"]
|
||||
page_data["backup_list"] = server.backup_mgr.list_backups(
|
||||
page_data["backup_config"], server.server_id
|
||||
)
|
||||
except:
|
||||
page_data["backup_list"] = []
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import logging
|
||||
from app.classes.web.base_api_handler import BaseApiHandler
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ from datetime import datetime, timedelta
|
||||
from jsonschema import validate
|
||||
from jsonschema.exceptions import ValidationError
|
||||
from app.classes.models.users import Users
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
from app.classes.web.base_api_handler import BaseApiHandler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -3,7 +3,7 @@ import json
|
||||
from jsonschema import ValidationError, validate
|
||||
import orjson
|
||||
from playhouse.shortcuts import model_to_dict
|
||||
from app.classes.shared.file_helpers import FileHelpers
|
||||
from app.classes.helpers.file_helpers import FileHelpers
|
||||
from app.classes.web.base_api_handler import BaseApiHandler
|
||||
|
||||
config_json_schema = {
|
||||
|
||||
@@ -5,7 +5,7 @@ import html
|
||||
from jsonschema import validate
|
||||
from jsonschema.exceptions import ValidationError
|
||||
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
from app.classes.web.base_api_handler import BaseApiHandler
|
||||
from app.classes.web.websocket_handler import WebSocketManager
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import os
|
||||
import logging
|
||||
import shutil
|
||||
import anyio
|
||||
from PIL import Image
|
||||
from app.classes.models.server_permissions import EnumPermissionsServer
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
from app.classes.web.base_api_handler import BaseApiHandler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -195,13 +196,15 @@ class ApiFilesUploadHandler(BaseApiHandler):
|
||||
# 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:
|
||||
async with await anyio.open_file(
|
||||
os.path.join(self.upload_dir, self.filename), "wb"
|
||||
) as file:
|
||||
chunk = self.request.body
|
||||
if chunk:
|
||||
file.write(chunk)
|
||||
await 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(
|
||||
calculated_hash = self.file_helper.calculate_file_hash_sha256(
|
||||
os.path.join(self.upload_dir, self.filename)
|
||||
)
|
||||
logger.info(
|
||||
@@ -278,8 +281,8 @@ class ApiFilesUploadHandler(BaseApiHandler):
|
||||
)
|
||||
|
||||
# Save the chunk
|
||||
with open(chunk_path, "wb") as f:
|
||||
f.write(self.request.body)
|
||||
async with await anyio.open_file(chunk_path, "wb") as f:
|
||||
await f.write(self.request.body)
|
||||
|
||||
# Check if all chunks are received
|
||||
received_chunks = [
|
||||
@@ -290,11 +293,11 @@ class ApiFilesUploadHandler(BaseApiHandler):
|
||||
# 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:
|
||||
async with await anyio.open_file(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())
|
||||
async with await anyio.open_file(chunk_file, "rb") as infile:
|
||||
await outfile.write(await infile.read())
|
||||
os.remove(chunk_file)
|
||||
if upload_type == "background":
|
||||
# Strip EXIF data
|
||||
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
import json
|
||||
from app.classes.models.server_permissions import EnumPermissionsServer
|
||||
from app.classes.models.servers import Servers
|
||||
from app.classes.shared.file_helpers import FileHelpers
|
||||
from app.classes.helpers.file_helpers import FileHelpers
|
||||
from app.classes.web.base_api_handler import BaseApiHandler
|
||||
|
||||
|
||||
|
||||
@@ -4,9 +4,8 @@ import os
|
||||
from jsonschema import validate
|
||||
from jsonschema.exceptions import ValidationError
|
||||
from app.classes.models.server_permissions import EnumPermissionsServer
|
||||
from app.classes.shared.file_helpers import FileHelpers
|
||||
from app.classes.helpers.file_helpers import FileHelpers
|
||||
from app.classes.web.base_api_handler import BaseApiHandler
|
||||
from app.classes.shared.helpers import Helpers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -68,6 +67,11 @@ BACKUP_PATCH_SCHEMA = {
|
||||
"error": "typeList",
|
||||
"fill": True,
|
||||
},
|
||||
"backup_type": {
|
||||
"type": "string",
|
||||
"enum": ["zip_vault", "snapshot"],
|
||||
"error": "enumErr",
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"minProperties": 1,
|
||||
@@ -107,6 +111,11 @@ BASIC_BACKUP_PATCH_SCHEMA = {
|
||||
"error": "typeList",
|
||||
"fill": True,
|
||||
},
|
||||
"backup_type": {
|
||||
"type": "string",
|
||||
"enum": ["zip_vault", "snapshot"],
|
||||
"error": "enumErr",
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"minProperties": 1,
|
||||
@@ -250,35 +259,11 @@ class ApiServersServerBackupsBackupIndexHandler(BaseApiHandler):
|
||||
"error_data": str(e),
|
||||
},
|
||||
)
|
||||
in_place = data.get("inPlace")
|
||||
svr_obj = self.controller.servers.get_server_instance_by_id(server_id)
|
||||
server_data = self.controller.servers.get_server_data_by_id(server_id)
|
||||
zip_name = data["filename"]
|
||||
# import the server again based on zipfile
|
||||
backup_config = self.controller.management.get_backup_config(backup_id)
|
||||
backup_location = os.path.join(
|
||||
backup_config["backup_location"],
|
||||
backup_config["backup_id"],
|
||||
data["filename"],
|
||||
svr_obj.server_restore_threader(
|
||||
backup_id, data["filename"], data.get("inPlace")
|
||||
)
|
||||
|
||||
if Helpers.validate_traversal(backup_location, zip_name):
|
||||
if svr_obj.check_running():
|
||||
svr_obj.stop_server()
|
||||
if (
|
||||
not in_place
|
||||
): # If user does not want to backup in place we will clean the server dir
|
||||
for item in os.listdir(server_data["path"]):
|
||||
if os.path.isdir(os.path.join(server_data["path"], item)):
|
||||
self.file_helper.del_dirs(
|
||||
os.path.join(server_data["path"], item)
|
||||
)
|
||||
else:
|
||||
self.file_helper.del_file(
|
||||
os.path.join(server_data["path"], item)
|
||||
)
|
||||
self.file_helper.restore_archive(backup_location, server_data["path"])
|
||||
|
||||
return self.finish_json(200, {"status": "ok"})
|
||||
|
||||
def patch(self, server_id: str, backup_id: str):
|
||||
|
||||
@@ -48,6 +48,11 @@ backup_patch_schema = {
|
||||
"error": "typeList",
|
||||
"fill": True,
|
||||
},
|
||||
"backup_type": {
|
||||
"type": "string",
|
||||
"enum": ["zip_vault", "snapshot"],
|
||||
"error": "enumErr",
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"minProperties": 1,
|
||||
@@ -87,6 +92,11 @@ basic_backup_patch_schema = {
|
||||
"error": "typeList",
|
||||
"fill": True,
|
||||
},
|
||||
"backup_type": {
|
||||
"type": "string",
|
||||
"enum": ["zip_vault", "snapshot"],
|
||||
"error": "enumErr",
|
||||
},
|
||||
},
|
||||
"additionalProperties": False,
|
||||
"minProperties": 1,
|
||||
|
||||
@@ -7,8 +7,8 @@ from jsonschema import validate
|
||||
from jsonschema.exceptions import ValidationError
|
||||
|
||||
from app.classes.models.server_permissions import EnumPermissionsServer
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.shared.file_helpers import FileHelpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
from app.classes.helpers.file_helpers import FileHelpers
|
||||
from app.classes.web.base_api_handler import BaseApiHandler
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -103,7 +103,7 @@ class ApiUsersIndexHandler(BaseApiHandler):
|
||||
offending_key = why.path[0] if why.path else None
|
||||
err = f"""{offending_key} {self.translator.translate(
|
||||
"validators",
|
||||
why.schema.get("error", "additionalProperties"),
|
||||
why.schema.get("error", "additionalProperties")[why.validator],
|
||||
self.controller.users.get_user_lang_by_id(auth_data[4]["user_id"]),
|
||||
)} {why.schema.get("enum", "")}"""
|
||||
return self.finish_json(
|
||||
|
||||
@@ -102,7 +102,7 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
|
||||
},
|
||||
)
|
||||
user_model = self.controller.users.get_user_object(user_id)
|
||||
for totp in list(user_model.totp_user):
|
||||
for totp in user_model.totp_user:
|
||||
self.controller.totp.delete_user_totp(totp)
|
||||
self.controller.totp.remove_all_recovery_codes(user_model.user_id)
|
||||
self.controller.users.remove_user(user_id)
|
||||
@@ -165,6 +165,21 @@ class ApiUsersUserIndexHandler(BaseApiHandler):
|
||||
"error_data": f"{str(err)}",
|
||||
},
|
||||
)
|
||||
if (
|
||||
user_id == "@me" or str(auth_data[4]["user_id"]) == str(user_id)
|
||||
) and data.get(
|
||||
"enabled"
|
||||
) is False: # User cannot enable or disable themselves
|
||||
return self.finish_json(
|
||||
400,
|
||||
{
|
||||
"status": "error",
|
||||
"error": "NOT_AUTHORIZED",
|
||||
"error_data": self.helper.translation.translate(
|
||||
"userConfig", "selfDisable", auth_data[4]["lang"]
|
||||
),
|
||||
},
|
||||
)
|
||||
if user_id == "@me":
|
||||
user_id = user["user_id"]
|
||||
if (
|
||||
|
||||
@@ -4,7 +4,7 @@ import tornado.web
|
||||
import tornado.escape
|
||||
|
||||
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
from app.classes.shared.main_models import DatabaseShortcuts
|
||||
from app.classes.web.base_handler import BaseHandler
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ try:
|
||||
import tornado.web
|
||||
|
||||
except ModuleNotFoundError as e:
|
||||
from app.classes.shared.helpers import helper
|
||||
from app.classes.helpers.helpers import helper
|
||||
|
||||
helper.auto_installer_fix(e)
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ import tornado.httpserver
|
||||
|
||||
from app.classes.models.management import HelpersManagement
|
||||
from app.classes.shared.console import Console
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.shared.file_helpers import FileHelpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
from app.classes.helpers.file_helpers import FileHelpers
|
||||
from app.classes.shared.main_controller import Controller
|
||||
from app.classes.web.public_handler import PublicHandler
|
||||
from app.classes.web.panel_handler import PanelHandler
|
||||
|
||||
@@ -2,7 +2,7 @@ from abc import ABC, abstractmethod
|
||||
import logging
|
||||
import requests
|
||||
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
helper = Helpers()
|
||||
|
||||
@@ -5,7 +5,7 @@ from urllib.parse import parse_qsl
|
||||
import tornado.websocket
|
||||
|
||||
from app.classes.shared.main_controller import Controller
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
from app.classes.shared.websocket_manager import WebSocketManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -40,4 +40,43 @@
|
||||
|
||||
#restore-box {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.ribbon {
|
||||
margin: -10px;
|
||||
background: rebeccapurple;
|
||||
color: white;
|
||||
padding: 1em 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
transform: translateX(30%) translateY(0%) rotate(45deg);
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
.ribbon:before,
|
||||
.ribbon:after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
margin: 0 -1px;
|
||||
/* tweak */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rebeccapurple;
|
||||
}
|
||||
|
||||
.ribbon:before {
|
||||
right: 100%;
|
||||
}
|
||||
|
||||
.ribbon:after {
|
||||
left: 100%;
|
||||
}
|
||||
|
||||
.parent {
|
||||
overflow: hidden;
|
||||
/* required */
|
||||
position: relative;
|
||||
/* required for demo*/
|
||||
}
|
||||
@@ -410,7 +410,7 @@ data['lang']) }}{% end %}
|
||||
}
|
||||
}
|
||||
function replacer(key, value) {
|
||||
if (typeof value == "boolean" || key === "email" || key === "permissions" || key === "roles") {
|
||||
if (typeof value == "boolean" || key === "email" || key === "permissions" || key === "roles" || key === "password") {
|
||||
console.log(key)
|
||||
return value
|
||||
} else {
|
||||
@@ -530,7 +530,7 @@ data['lang']) }}{% end %}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
$(".delete-user").click(function () {
|
||||
var file_to_del = $(this).data("file");
|
||||
|
||||
@@ -616,4 +616,4 @@ data['lang']) }}{% end %}
|
||||
</script>
|
||||
<script src="../../static/assets/js/shared/userSettings.js"></script>
|
||||
|
||||
{% end %}
|
||||
{% end %}
|
||||
|
||||
@@ -73,8 +73,7 @@
|
||||
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;">Backup Type</th>
|
||||
<th scope="col" style="width: 10%; min-width: 50px;">{{ translate('serverBackups', 'actions',
|
||||
data['lang']) }}</th>
|
||||
</tr>
|
||||
|
||||
@@ -40,7 +40,14 @@
|
||||
{% include "parts/m_server_controls_list.html %}
|
||||
</span>
|
||||
<div class="row">
|
||||
<div class="col-md-6 col-sm-12">
|
||||
<div class="col-md-6 col-sm-12 parent">
|
||||
{% if data["backup_config"].get("backup_type") != "snapshot" %}
|
||||
<span id="beta-ribbon" class="d-none">
|
||||
{% else %}
|
||||
<span id="beta-ribbon"></span>
|
||||
{% end %}
|
||||
<h4 class="ribbon">BETA</h4>
|
||||
</span>
|
||||
<br>
|
||||
<br>
|
||||
<div id="{{data['backup_config'].get('backup_id', None)}}_status" class="progress"
|
||||
@@ -59,6 +66,21 @@
|
||||
</div>
|
||||
{% end %}
|
||||
<form id="backup-form" class="forms-sample">
|
||||
{% if not data["backup_config"].get("backup_id", None) %}
|
||||
<div class="btn-group btn-group-toggle" data-toggle="buttons">
|
||||
<label class="btn btn-secondary active">
|
||||
<input type="radio" class="btn-check" name="backup_type" id="zip_vault" value="zip_vault"
|
||||
autocomplete="off" checked> Full Zip Vault
|
||||
</label>
|
||||
<label class="btn btn-secondary">
|
||||
<input type="radio" class="btn-check" name="backup_type" id="snapshot" value="snapshot"
|
||||
data-disclaimer="{{ translate('serverBackups', 'snapshotDisclaimer', data['lang']) }}"
|
||||
autocomplete="off"> Snapshot
|
||||
</label>
|
||||
</div>
|
||||
<br>
|
||||
<br>
|
||||
{% end %}
|
||||
<div class="form-group">
|
||||
<label for="backup_name">{{ translate('serverBackups', 'name', data['lang']) }}
|
||||
{% if data["backup_config"].get("default", None) %}
|
||||
@@ -74,9 +96,9 @@
|
||||
{% else %}
|
||||
<input type="text" class="form-control" name="backup_name" id="backup_name"
|
||||
placeholder="{{ translate('serverBackups', 'myBackup', data['lang']) }}">
|
||||
<br>
|
||||
<br>
|
||||
{% 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'])
|
||||
@@ -225,7 +247,8 @@
|
||||
<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">
|
||||
<button data-file="{{ backup['path'] }}" data-type="{{data['backup_config']['backup_type']}}"
|
||||
class="btn btn-warning restore_button">
|
||||
<i class="fas fa-undo-alt" aria-hidden="true"></i>
|
||||
{{ translate('serverBackups', 'restore', data['lang']) }}
|
||||
</button>
|
||||
@@ -386,6 +409,21 @@
|
||||
}
|
||||
}
|
||||
|
||||
$("#snapshot").on("change", function () {
|
||||
if ($("#snapshot").is(':checked')) {
|
||||
$("#beta-ribbon").removeClass("d-none")
|
||||
console.log($("#snapshot").data("disclaimer"))
|
||||
bootbox.alert($("#snapshot").data("disclaimer"))
|
||||
} else {
|
||||
console.log("In else")
|
||||
$("#beta-ribbon").addClass("d-none")
|
||||
}
|
||||
})
|
||||
$("#zip_vault").on("change", function () {
|
||||
console.log("In else")
|
||||
$("#beta-ribbon").addClass("d-none")
|
||||
})
|
||||
|
||||
$(document).ready(function () {
|
||||
$(".backup-explain").on("click", function (e) {
|
||||
e.preventDefault();
|
||||
@@ -534,9 +572,12 @@
|
||||
|
||||
$(".restore_button").click(function () {
|
||||
var file_to_restore = $(this).data("file");
|
||||
bootbox.dialog({
|
||||
title: "{{ translate('serverBackups', 'restore', data['lang']) }} " + file_to_restore,
|
||||
message: `
|
||||
if ($(this).data("type") === "snapshot") {
|
||||
restore_backup(file_to_restore, serverId, 1)
|
||||
} else {
|
||||
bootbox.dialog({
|
||||
title: "{{ translate('serverBackups', 'restore', data['lang']) }} " + file_to_restore,
|
||||
message: `
|
||||
<div id="restore-box">
|
||||
<div class="custom-control custom-switch">
|
||||
<input class="custom-control-input" type="radio" name="restoreOption" id="optionInPlace" value=1>
|
||||
@@ -554,24 +595,25 @@
|
||||
</div>
|
||||
</div>
|
||||
`,
|
||||
buttons: {
|
||||
cancel: {
|
||||
label: '<i class="fas fa-times"></i> {{ translate("serverBackups", "cancel", 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 () {
|
||||
var selectedOption = $('input[name="restoreOption"]:checked').val();
|
||||
if (!selectedOption || typeof (selectedOption) == "undefined") {
|
||||
bootbox.alert(`{{ translate("serverBackups", "radioRequired", data['lang']) }}`)
|
||||
} else {
|
||||
restore_backup(file_to_restore, serverId, selectedOption)
|
||||
confirm: {
|
||||
label: '<i class="fas fa-check"></i> {{ translate("serverBackups", "restore", data['lang']) }}',
|
||||
className: 'btn-outline-danger',
|
||||
callback: function () {
|
||||
var selectedOption = $('input[name="restoreOption"]:checked').val();
|
||||
if (!selectedOption || typeof (selectedOption) == "undefined") {
|
||||
bootbox.alert(`{{ translate("serverBackups", "radioRequired", data['lang']) }}`)
|
||||
} else {
|
||||
restore_backup(file_to_restore, serverId, selectedOption)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$("#backup_now_button").click(function () {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import peewee
|
||||
import datetime
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
|
||||
|
||||
def migrate(migrator, database, **kwargs):
|
||||
|
||||
@@ -6,10 +6,10 @@ import peewee
|
||||
import logging
|
||||
|
||||
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
from app.classes.shared.console import Console
|
||||
from app.classes.helpers.file_helpers import FileHelpers
|
||||
from app.classes.shared.migration import Migrator, MigrateHistory
|
||||
from app.classes.shared.file_helpers import FileHelpers
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
16
app/migrations/20240811_snapshot_backups.py
Normal file
16
app/migrations/20240811_snapshot_backups.py
Normal file
@@ -0,0 +1,16 @@
|
||||
# Generated by database migrator
|
||||
import peewee
|
||||
|
||||
|
||||
def migrate(migrator, database, **kwargs):
|
||||
"""
|
||||
Write your migrations here.
|
||||
"""
|
||||
migrator.add_columns("backups", backup_type=peewee.CharField(default="zip_vault"))
|
||||
|
||||
|
||||
def rollback(migrator, database, **kwargs):
|
||||
"""
|
||||
Write your rollback migrations here.
|
||||
"""
|
||||
migrator.drop_columns("backups", ["backup_type"])
|
||||
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.shared.file_helpers import FileHelpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
from app.classes.helpers.file_helpers import FileHelpers
|
||||
from app.classes.shared.console import Console
|
||||
|
||||
console = Console()
|
||||
|
||||
@@ -32,9 +32,42 @@
|
||||
"yes": "Yes"
|
||||
},
|
||||
"base": {
|
||||
"createMFA": "Add Multi-Factor Authentication to your account!",
|
||||
"doesNotWorkWithoutJavascript": "<strong>Warning: </strong>Crafty doesn't work properly when JavaScript isn't enabled!",
|
||||
"getMFA": "Secure your servers!",
|
||||
"createMFA": "Add Multi Factor Authentication to your account!"
|
||||
"getMFA": "Secure your servers!"
|
||||
},
|
||||
"configJson": {
|
||||
"False": "False",
|
||||
"True": "True",
|
||||
"allow_nsfw_profile_pictures": "Allow Gravatar™ NSFW profile pictures",
|
||||
"big_bucket_repo": "System Big Bucket™ remote URL",
|
||||
"cookie_expire": "Session cookies expire after (days)",
|
||||
"crafty_logs_delete_after_days": "How long Crafty should hold onto app logs (days)",
|
||||
"delete_default_json": "Delete default JSON file on startup",
|
||||
"dir_size_poll_freq_minutes": "How often Crafty should poll your server directories for their sizes (minutes)",
|
||||
"disabled_language_files": "Globally disabled language files",
|
||||
"enable_otp_skew": "Enable MFA skew (+-1)",
|
||||
"enable_user_self_delete": "Allow users to delete their own account",
|
||||
"general": "General",
|
||||
"history_max_age": "How long Crafty should hold onto your server statistics (days)",
|
||||
"https_port": "Crafty's port",
|
||||
"keywords": "Keywords Crafty should highlight in your server terminals",
|
||||
"language": "Global system language. (For public pages and translation fallback)",
|
||||
"logs": "Logging",
|
||||
"max_audit_entries": "Maximum number of entries allowed in Crafty's activity log",
|
||||
"max_log_lines": "Maximum number of log lines per file",
|
||||
"max_login_attempts": "Maximum number of times a remote IP can fail login before we time it out",
|
||||
"miscellaneous": "Miscellaneous",
|
||||
"monitored_mounts": "Storage mounts visible on dashboard",
|
||||
"monitoring": "Monitoring",
|
||||
"reset_secrets_on_next_boot": "Reset secrets on next boot",
|
||||
"security": "Security",
|
||||
"show_contribute_link": "Show contribution links in panel",
|
||||
"show_errors": "Show webserver debug errors",
|
||||
"stats_update_frequency_seconds": "How often we should poll your server for stats (seconds)",
|
||||
"submit": "Submit",
|
||||
"superMFA": "Require superusers to enable MFA",
|
||||
"virtual_terminal_lines": "How many lines we should allow in your server terminal buffer"
|
||||
},
|
||||
"credits": {
|
||||
"developmentTeam": "Development Team",
|
||||
@@ -89,7 +122,7 @@
|
||||
"cpuUsage": "CPU Usage",
|
||||
"crashed": "Crashed",
|
||||
"dashboard": "Dashboard",
|
||||
"delay-explained": "The service/agent has recently started and is delaying the start of the minecraft server instance",
|
||||
"delay-explained": "The service/agent has recently started and is delaying the start of the Minecraft server instance",
|
||||
"host": "Host",
|
||||
"installing": "Installing...",
|
||||
"kill": "Kill Process",
|
||||
@@ -186,7 +219,7 @@
|
||||
"agree": "Agree",
|
||||
"bedrockError": "Bedrock downloads unavailable. Please check",
|
||||
"bigBucket1": "Big Bucket Health Check Failed. Please check",
|
||||
"bigBucket2": "for the most up to date information.",
|
||||
"bigBucket2": "for the most up-to-date information.",
|
||||
"cancel": "Cancel",
|
||||
"contact": "Contact Crafty Control Support via Discord",
|
||||
"craftyStatus": "Crafty's status page",
|
||||
@@ -198,14 +231,13 @@
|
||||
"eulaMsg": "You must agree to the ",
|
||||
"eulaTitle": "Agree To EULA",
|
||||
"fileError": "File type must be an image.",
|
||||
"fileTooLarge": "Upload failed. File upload too large. Contact system administrator for assistance.",
|
||||
"hereIsTheError": "Here is the error",
|
||||
"installerJava": "Failed to install {} : Forge Server Installs require Java. We have detected Java is not installed. Please install java then install the server.",
|
||||
"installerJava": "Failed to install {} : Forge Server Installs require Java. We have detected that Java is not installed. Please install Java, then install the server.",
|
||||
"internet": "We have detected the machine running Crafty has no connection to the internet. Client connections to the server may be limited.",
|
||||
"migration": "Crafty's main server storage is being mirgated to a new location. All server starts have been suspended during this time. Please wait while we finish this migration",
|
||||
"no-file": "We can't seem to locate the requested file. Double check the path. Does Crafty have proper permissions?",
|
||||
"migration": "Crafty's main server storage is being migrated to a new location. All server starts have been suspended during this time. Please wait while we finish this migration",
|
||||
"no-file": "We can't seem to locate the requested file. Double-check the path. Does Crafty have proper permissions?",
|
||||
"noInternet": "Crafty is having trouble accessing the internet. Server Creation has been disabled. Please check your internet connection and refresh this page.",
|
||||
"noJava": "Server {} failed to start with error code: We have detected Java is not installed. Please install java then start the server.",
|
||||
"noJava": "Server {} failed to start with error: We have detected Java is not installed. Please install Java, then start the server.",
|
||||
"not-downloaded": "We can't seem to find your executable file. Has it finished downloading? Are the permissions set to executable?",
|
||||
"portReminder": "We have detected this is the first time {} has been run. Make sure to forward port {} through your router/firewall to make this remotely accessible from the internet.",
|
||||
"privMsg": "and the ",
|
||||
@@ -224,35 +256,34 @@
|
||||
"2fa": "(If MFA is Enabled)",
|
||||
"2faRecovery": "Recover MFA",
|
||||
"accountDisabled": "Your user account has been administratively disabled. Please contact your system administrator for more details.",
|
||||
"backupCodeTitle": "Backup Code",
|
||||
"backupCode": "Enter Backup Code",
|
||||
"backupCodeTitle": "Backup Code",
|
||||
"burnedBackupCode": "You have burned one of your backup codes. Please reroll your backup codes to avoid being locked out",
|
||||
"cancel": "Cancel",
|
||||
"cooldown": "Cooldown active. Try again in",
|
||||
"defaultPath": "The password you entered is the default credential path, not the password. Please find the default password in that location.",
|
||||
"disabled": "User account disabled. Please contact your system administrator for more info.",
|
||||
"forgotPassword": "Forgot Password",
|
||||
"totpSelect": "Authenticator App",
|
||||
"incorrect": "Incorrect username, password, or MFA",
|
||||
"login": "Log In",
|
||||
"password": "Password",
|
||||
"passwordRecovery": "Recover Password",
|
||||
"totp": "Enter Authenticator Code",
|
||||
"totpSelect": "Authenticator App",
|
||||
"username": "Username",
|
||||
"viewStatus": "View Public Status Page",
|
||||
"totp": "Enter Authenticator Code"
|
||||
"viewStatus": "View Public Status Page"
|
||||
},
|
||||
"notify": {
|
||||
"activityLog": "Activity Logs",
|
||||
"accountSettings": "Account Settings",
|
||||
"activityLog": "Activity Logs",
|
||||
"backupComplete": "Backup completed successfully for server {}",
|
||||
"backupStarted": "Backup started for server {}",
|
||||
"backup_desc": "We detected the backup migration may have partially or fully failed. Please confirm your backups records on the backups tab.",
|
||||
"backup_desc": "We detected the backup migration may have partially or fully failed. Please confirm your backup records on the backups tab.",
|
||||
"backup_title": "Backup Migration Warning",
|
||||
"downloadLogs": "Download Support Logs?",
|
||||
"finishedPreparing": "We've finished preparing your support logs. Please click download to download",
|
||||
"logout": "Logout",
|
||||
"preparingLogs": " Please wait while we prepare your logs... We`ll send a notification when they`re ready. This may take a while for large deployments.",
|
||||
"schedule_desc": "We detected some or all of your scheduled tasks were not successfully transfered during the upgrade. Please confirm your schedules in the schedules tab.",
|
||||
"schedule_desc": "We detected some or all of your scheduled tasks were not successfully transferred during the upgrade. Please confirm your schedules in the schedules tab.",
|
||||
"schedule_title": "Schedules Migration Warning",
|
||||
"supportLogs": "Support Logs"
|
||||
},
|
||||
@@ -261,29 +292,29 @@
|
||||
"pleaseConnect": "Please connect to the internet to use Crafty."
|
||||
},
|
||||
"otp": {
|
||||
"name": "Name",
|
||||
"id": "ID",
|
||||
"config": "Config",
|
||||
"copyBackupCodes": "Copy Codes",
|
||||
"delete": "Delete",
|
||||
"deleteConfirm": "Are you sure you want to delete this MFA method?",
|
||||
"yes": "Yes",
|
||||
"no": "No",
|
||||
"2faCreate": "Add New MFA Method",
|
||||
"backupCodes": "Please copy your backup codes.",
|
||||
"backupCodesMax": "A maximum of 6 back codes has already been reached.",
|
||||
"backupOtp": "A MFA method is required to request backup codes",
|
||||
"saveWarn": "Make sure to save these backup codes or you could lose access",
|
||||
"backupOtp": "An MFA method is required to request backup codes",
|
||||
"config": "Config",
|
||||
"confirm2FA": "Please Confirm Authenticator Code",
|
||||
"copyBackupCodes": "Copy Codes",
|
||||
"delete": "Delete",
|
||||
"deleteConfirm": "Are you sure you want to delete this MFA method?",
|
||||
"goToPage": "Go to MFA page",
|
||||
"id": "ID",
|
||||
"mfaWarn": "You must have MFA enabled to interact with this system. Please create an MFA key. If you are seeing this message and have already created a key, please log out, then log back in with MFA.",
|
||||
"name": "Name",
|
||||
"newName": "Enter a friendly name for this MFA method",
|
||||
"newOTP": "Add New Multi-Factor Auth",
|
||||
"no": "No",
|
||||
"otpReq": "MFA is required for this user. You cannot remove your only MFA method.",
|
||||
"otpTitle": "MFA Methods",
|
||||
"renewRecovery": "Renew Recovery Codes",
|
||||
"newOTP": "Add New Multi Factor Auth",
|
||||
"verifyTitle": "Verify MFA method using authenticator",
|
||||
"saveWarn": "Make sure to save these backup codes or you could lose access",
|
||||
"verify": "Failed to verify MFA. Incorrect code entered.",
|
||||
"mfaWarn": "You must have MFA enabled to interact with this system. Please create a MFA key. If you are seeing this message and have already created a key please log out then log back in with MFA.",
|
||||
"goToPage": "Go to MFA page",
|
||||
"otpReq": "MFA is required for this user. You cannot remove your only MFA method."
|
||||
"verifyTitle": "Verify MFA method using authenticator",
|
||||
"yes": "Yes"
|
||||
},
|
||||
"panelConfig": {
|
||||
"adminControls": "Admin Controls",
|
||||
@@ -316,40 +347,6 @@
|
||||
"user": "User",
|
||||
"users": "Users"
|
||||
},
|
||||
"configJson": {
|
||||
"general": "General",
|
||||
"security": "Security",
|
||||
"logs": "Logging",
|
||||
"monitoring": "Monitoring",
|
||||
"miscellaneous": "Miscellaneous",
|
||||
"allow_nsfw_profile_pictures": "Allow Gravatar™ NSFW profile pictures",
|
||||
"big_bucket_repo": "System Big Bucket™ remote URL",
|
||||
"cookie_expire": "Session cookies expire after (days)",
|
||||
"crafty_logs_delete_after_days": "How long Crafty should hold onto app logs (days)",
|
||||
"delete_default_json": "Delete default json file on startup",
|
||||
"dir_size_poll_freq_minutes": "How often Crafty should poll your server directories for their sizes (minutes)",
|
||||
"disabled_language_files": "Globally disabled language files",
|
||||
"enable_user_self_delete": "Allow users to delete their own account",
|
||||
"enable_otp_skew": "Enable MFA skew (+-1)",
|
||||
"False": "False",
|
||||
"general_user_log_access": "Lower privilege users have access to crafty logs (Including Auth Logs)",
|
||||
"history_max_age": "How long Crafty should hold onto your server statistics (days)",
|
||||
"https_port": "Crafty's port",
|
||||
"keywords": "Keywords Crafty should highlight in your server terminals",
|
||||
"language": "Global system lanuage. (For public pages and translation fallback)",
|
||||
"max_audit_entries": "Maximum number of entries allowed in Crafty's activity log",
|
||||
"max_log_lines": "Maximum number of log lines per file",
|
||||
"max_login_attempts": "Maximum number of times a remote IP can fail login before we time it out",
|
||||
"monitored_mounts": "Storage mounts visible on dashboard",
|
||||
"reset_secrets_on_next_boot": "Reset secrets on next boot",
|
||||
"show_contribute_link": "Show contribution links in panel",
|
||||
"show_errors": "Show webserver debug errors",
|
||||
"stats_update_frequency_seconds": "How often we should poll your server for stats (seconds)",
|
||||
"submit": "Submit",
|
||||
"superMFA": "Require superusers to enable MFA",
|
||||
"True": "True",
|
||||
"virtual_terminal_lines": "How many lines we should allow in your server terminal buffer"
|
||||
},
|
||||
"rolesConfig": {
|
||||
"config": "Role Config",
|
||||
"configDesc": "Here is where you can change the configuration of your role",
|
||||
@@ -357,12 +354,12 @@
|
||||
"created": "Created: ",
|
||||
"delRole": "Delete Role",
|
||||
"doesNotExist": "You cannot delete something that does not exist yet",
|
||||
"requireMFA": "Require role users to enable MFA (Multi Factor Authentication)",
|
||||
"pageTitle": "Edit Role",
|
||||
"pageTitleNew": "New Role",
|
||||
"permAccess": "Access?",
|
||||
"permName": "Permission Name",
|
||||
"permsServer": "Permissions this role has for these specified servers",
|
||||
"requireMFA": "Require role users to enable MFA (Multi-Factor Authentication)",
|
||||
"roleConfigArea": "Role Config Area",
|
||||
"roleDesc": "What would you like to call this role?",
|
||||
"roleName": "Role Name: ",
|
||||
@@ -407,7 +404,7 @@
|
||||
"myBackup": "My New Backup",
|
||||
"name": "Name",
|
||||
"newBackup": "Create New Backup",
|
||||
"no-backup": "No Backups. To make a new backup configuration please press. New Backup",
|
||||
"no-backup": "No Backups. To make a new backup configuration please press. New Backup",
|
||||
"options": "Options",
|
||||
"path": "Path",
|
||||
"radioRequired": "You must choose an option to restore your backup",
|
||||
@@ -415,6 +412,7 @@
|
||||
"restore": "Restore",
|
||||
"restoring": "Restoring Backup. This may take a while. Please be patient.",
|
||||
"run": "Run Backup",
|
||||
"snapshotDisclaimer": "Snapshot backups are currently in beta. Snapshot backups should only store one copy of a file and use much less disk space. Backup and restore do work well in testing but you may encounter unforeseen issues. You should not solely rely on this backup style for any mission critical backups. Please report any issues you encounter.",
|
||||
"save": "Save",
|
||||
"shutdown": "Shutdown server for duration of backup",
|
||||
"size": "Size",
|
||||
@@ -447,20 +445,20 @@
|
||||
"noDelete": "No, go back",
|
||||
"noDeleteFiles": "No, just remove from panel",
|
||||
"removeOldLogsAfter": "Remove Old Logs After",
|
||||
"removeOldLogsAfterDesc": "How many days will a log file has to be old to get deleted (0 is off)",
|
||||
"removeOldLogsAfterDesc": "How many days should we keep log files for? (0 is off)",
|
||||
"save": "Save",
|
||||
"sendingDelete": "Deleting Server",
|
||||
"sendingRequest": "Sending your request...",
|
||||
"serverAutoStart": "Server Auto Start",
|
||||
"serverAutostartDelay": "Server Autostart Delay",
|
||||
"serverAutostartDelayDesc": "Delay before auto starting (If enabled below)",
|
||||
"serverAutostartDelayDesc": "Delay before auto-starting (If enabled below)",
|
||||
"serverCrashDetection": "Server Crash Detection",
|
||||
"serverExecutable": "Server Executable",
|
||||
"serverExecutableDesc": "The server's executable file",
|
||||
"serverExecutionCommand": "Server Execution Command",
|
||||
"serverExecutionCommandDesc": "What will be launched in a hidden terminal",
|
||||
"serverIP": "Server IP",
|
||||
"serverIPDesc": "IP Crafty should connect to for stats (Try a real ip instead of 127.0.0.1 if you have issues)",
|
||||
"serverIPDesc": "IP Crafty should connect to for stats (Try a real IP instead of 127.0.0.1 if you have issues)",
|
||||
"serverLogLocation": "Server Log Location",
|
||||
"serverLogLocationDesc": "Path to the log file",
|
||||
"serverName": "Server Name",
|
||||
@@ -488,10 +486,10 @@
|
||||
"It is recommended to <code>NOT</code> change the paths of a server managed by Crafty.",
|
||||
"Changing paths <code>CAN</code> break things, especially on Linux type operating systems where file permissions are more locked down.",
|
||||
"<br /><br/>",
|
||||
"If you feel you have to change a where a server is located you may do so as long as you give the \"crafty\" user permission to read / write to the server path.",
|
||||
"If you feel you have to change where a server is located you may do so as long as you give the \"crafty\" user permission to read / write to the server path.",
|
||||
"<br />",
|
||||
"<br />",
|
||||
"On Linux this is best done by executing the following:<br />",
|
||||
"On Linux, this is best done by executing the following:<br />",
|
||||
"<code>",
|
||||
" sudo chown crafty:crafty /path/to/your/server -R<br />",
|
||||
" sudo chmod 2775 /path/to/your/server -R<br />",
|
||||
@@ -586,8 +584,8 @@
|
||||
"serverSchedules": {
|
||||
"action": "Action",
|
||||
"actionId": "Action Child",
|
||||
"backupPol": "Backup Policy",
|
||||
"areYouSure": "Delete Scheduled Task?",
|
||||
"backupPol": "Backup Policy",
|
||||
"cancel": "Cancel",
|
||||
"cannotSee": "Not seeing everything?",
|
||||
"cannotSeeOnMobile": "Try clicking on a scheduled task for full details.",
|
||||
@@ -632,7 +630,7 @@
|
||||
},
|
||||
"serverTerm": {
|
||||
"commandInput": "Enter your command",
|
||||
"delay-explained": "The service/agent has recently started and is delaying the start of the minecraft server instance",
|
||||
"delay-explained": "The service/agent has recently started and is delaying the start of the Minecraft server instance",
|
||||
"importing": "Importing...",
|
||||
"installing": "Installing...",
|
||||
"restart": "Restart",
|
||||
@@ -695,7 +693,7 @@
|
||||
"credits": "Credits",
|
||||
"dashboard": "Dashboard",
|
||||
"documentation": "Documentation",
|
||||
"inApp": "In App Docs",
|
||||
"inApp": "In-App Docs",
|
||||
"newServer": "Create New Server",
|
||||
"panelSettings": "Panel Settings",
|
||||
"servers": "Servers"
|
||||
@@ -720,7 +718,7 @@
|
||||
"configAreaDesc": "Here is where you change all of your user settings",
|
||||
"confirmDelete": "Are you sure you want to delete this user? This action is irreversible.",
|
||||
"craftyPermDesc": "Crafty permissions this user has ",
|
||||
"craftyPerms": "Crafty Permissons: ",
|
||||
"craftyPerms": "Crafty Permissions: ",
|
||||
"created": "Created: ",
|
||||
"delSuper": "You cannot delete a super user",
|
||||
"deleteUser": "Delete user: ",
|
||||
@@ -744,9 +742,10 @@
|
||||
"repeat": "Confirm Password",
|
||||
"roleName": "Role Name",
|
||||
"selectManager": "Select Manager for User",
|
||||
"selfDisable": "You cannot disable your own account",
|
||||
"super": "Super User",
|
||||
"totpHeader": "Multi-Factor Auth",
|
||||
"totpIdReq": "MFA ID Required for request",
|
||||
"totpHeader": "Multi Factor Auth",
|
||||
"userLang": "User Language",
|
||||
"userName": "User Name",
|
||||
"userNameDesc": "What do you want to call this user?",
|
||||
@@ -757,13 +756,15 @@
|
||||
"uses": "Number of uses allowed (-1==No Limit)"
|
||||
},
|
||||
"validators": {
|
||||
"2FAerror": "Multi Factor code must be 6 digits (i.e. 000000) or a backup code of 16 characters separated every 4 by - (i.e. ABCD-EFGH-IJKL-MNOP)",
|
||||
"2FAerror": "Multi-Factor code must be 6 digits (i.e. 000000) or a backup code of 16 characters separated every 4 by - (i.e. ABCD-EFGH-IJKL-MNOP)",
|
||||
"additionalProperties": "Additional Properties are not allowed to be passed",
|
||||
"backupName": "Backup name must be a string and a minimum length of 3.",
|
||||
"enumErr": "failed validating. Acceptable data includes: ",
|
||||
"filesPageLen": "length must be greater than 1 for property",
|
||||
"insufficientPerms": "Permission Error: Missing permissions for this resource",
|
||||
"mfaName": "Input must be of type string and a minimum of 3 characters for property",
|
||||
"passLength": "Password Too Short. Minimum Length: 8",
|
||||
"numbericPassword": "Numeric Password. Needs at least 1 alphabetic character",
|
||||
"roleManager": "Role manager must be of type integer (manager ID) or None",
|
||||
"roleName": "Role name must be a string that is greater than 1 character. It must not include any of the following symbols: [ ] , ",
|
||||
"roleServerId": "Server ID property must be a string with a minimum length of 1",
|
||||
@@ -779,8 +780,7 @@
|
||||
"typeInteger": "must be a number.",
|
||||
"typeList": "must be of type list/array ",
|
||||
"typeString": "must be of type string.",
|
||||
"userName": " must be of type string, all LOWERCASE, a minimum of 4 characters and a max of 20 characters",
|
||||
"mfaName": "Input must be of type string and a minumum of 3 characters for property"
|
||||
"userName": " must be of type string, all LOWERCASE, a minimum of 4 characters and a max of 20 characters"
|
||||
},
|
||||
"webhooks": {
|
||||
"areYouSureDel": "Are you sure you want to delete this webhook?",
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
"yes": "Si"
|
||||
},
|
||||
"base": {
|
||||
"doesNotWorkWithoutJavascript": "<strong>Aviso: </strong>¡Crafty no funciona correctamente cuando JavaScript no está habilitado!"
|
||||
"doesNotWorkWithoutJavascript": "<strong>Aviso: </strong>¡Crafty no funciona correctamente cuando JavaScript no está habilitado!",
|
||||
"getMFA": "Asegura tus servidores!",
|
||||
"createMFA": "Añade autentificación en dos factores a tu cuenta!"
|
||||
},
|
||||
"credits": {
|
||||
"developmentTeam": "Equipo de desarrollo",
|
||||
@@ -221,11 +223,20 @@
|
||||
"defaultPath": "La contraseña introducida es la ruta default de las credenciales, no la contraseña. Busca la contraseña accediendo a la carpeta de la ruta",
|
||||
"disabled": "Cuenta del usuario desactivada. Porfavor contacta al administrador para mas informacion.",
|
||||
"forgotPassword": "Olvidé mi contraseña",
|
||||
"incorrect": "El nombre de usuario o contraseña es incorrecto",
|
||||
"incorrect": "El nombre de usuario, la contraseña, o el A2F son incorrectos",
|
||||
"login": "Iniciar Sesión",
|
||||
"password": "Contraseña",
|
||||
"username": "Usuario",
|
||||
"viewStatus": "Ver página de estado público"
|
||||
"viewStatus": "Ver página de estado público",
|
||||
"2fa": "(Si A2F esta habilitado)",
|
||||
"2faRecovery": "Recuperar A2F",
|
||||
"accountDisabled": "Tu cuenta de usuario ha sido desactiva administrativamente. Por favor contacta al administrador de tu sistema para mas detalles.",
|
||||
"backupCodeTitle": "Código de respaldo",
|
||||
"backupCode": "Introduce el código de respaldo",
|
||||
"cancel": "Cancelar",
|
||||
"totpSelect": "Aplicación de autentificación",
|
||||
"passwordRecovery": "Recuperar contraseña",
|
||||
"totp": "Introducir código de autentificación"
|
||||
},
|
||||
"notify": {
|
||||
"activityLog": "Registros de actividad",
|
||||
@@ -235,7 +246,12 @@
|
||||
"finishedPreparing": "Terminamos la preparación de tus registros de soporte. Por favor presione el botón para descargar.",
|
||||
"logout": "Cerrar Sesión",
|
||||
"preparingLogs": " Por favor espere mientras preparamos los registros. Le enviaremos una notificación cuando estén listos. Esto puede tomar un rato en implementaciones grandes.",
|
||||
"supportLogs": "Registros de soporte"
|
||||
"supportLogs": "Registros de soporte",
|
||||
"schedule_desc": "Hemos detectado que algunas o todas tus tareas programadas no fueron transferidas con éxito durante la actualización. Por favor confirma tus tareas programas en la pestaña de Programar tareas.",
|
||||
"backup_desc": "Hemos detectado que la migración de respaldo puede haber fallado parcial o completamente. Por favor confirma tus registros de respaldo en la pestaña de respaldos.",
|
||||
"backup_title": "Advertencia sobre la migración de respaldo",
|
||||
"accountSettings": "Ajustes de la cuenta",
|
||||
"schedule_title": "Advertencia sobre la transferencia de las tareas programadas"
|
||||
},
|
||||
"offline": {
|
||||
"offline": "Desconectado",
|
||||
@@ -693,5 +709,17 @@
|
||||
"url": "URL de Webhook",
|
||||
"webhook_body": "Cuerpo del Webhook",
|
||||
"webhooks": "Webhooks"
|
||||
},
|
||||
"otp": {
|
||||
"2faCreate": "Añadir nuevo método de autentificación en dos factores",
|
||||
"backupCodes": "Por favor copia tus códigos de respaldo.",
|
||||
"name": "Nombre",
|
||||
"id": "ID (Identificación)",
|
||||
"config": "Configuración",
|
||||
"copyBackupCodes": "Copiar codigos",
|
||||
"delete": "Eliminar",
|
||||
"deleteConfirm": "¿Seguro que quieres eliminar este método de autentificación en dos factores?",
|
||||
"yes": "Si",
|
||||
"no": "No"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
},
|
||||
"accessDenied": {
|
||||
"accessDenied": "Accès interdit",
|
||||
"contact": "Contacter le Support de Crafty Control via Discord",
|
||||
"contact": "Contacter le support de Crafty Control via Discord",
|
||||
"contactAdmin": "Contactez l'administrateur du serveur pour avoir accès à cette ressource, ou si vous pensez que vous devriez avoir accès à cette ressource, contactez le support.",
|
||||
"noAccess": "Vous n'avez pas accès à cette ressource"
|
||||
},
|
||||
@@ -188,11 +188,11 @@
|
||||
"bigBucket1": "Échec de vérification de l'état de Big Bucket. Veuillez vérifier",
|
||||
"bigBucket2": "pour les informations les plus récentes.",
|
||||
"cancel": "Annuler",
|
||||
"contact": "Contacter le Support de Crafty Control via Discord",
|
||||
"contact": "Contacter le support de Crafty Control via Discord",
|
||||
"craftyStatus": "Page de statut de Crafty",
|
||||
"cronFormat": "Format Cron invalide détecté",
|
||||
"embarassing": "Oulà, c'est embarrassant.",
|
||||
"error": "Erreur !",
|
||||
"error": "Erreur !",
|
||||
"eulaAgree": "Êtes-vous d'accord ?",
|
||||
"eulaMsg": "Vous devez accepter le ",
|
||||
"eulaTitle": "Accepter le EULA",
|
||||
@@ -720,7 +720,8 @@
|
||||
"backupName": "Le nom de la sauvegarde doit être une chaîne de caractères d’au moins 3 caractères.",
|
||||
"enumErr": "Échec de la validation. Les données acceptables incluent : ",
|
||||
"filesPageLen": "La longueur doit être supérieure à 1 pour cette propriété",
|
||||
"insufficientPerms": "Erreur de permission : permissions manquantes pour cette ressource"
|
||||
"insufficientPerms": "Erreur de permission : permissions manquantes pour cette ressource",
|
||||
"mfaName": "La saisie doit être une chaîne de caractères d'au moins 3 caractères pour ce champ"
|
||||
},
|
||||
"webhooks": {
|
||||
"areYouSureDel": "Êtes-vous sûr de vouloir supprimer ce webhook ?",
|
||||
|
||||
287
app/translations/hu_HU.json
Normal file
287
app/translations/hu_HU.json
Normal file
@@ -0,0 +1,287 @@
|
||||
{
|
||||
"404": {
|
||||
"notFound": "Oldal nem található",
|
||||
"contact": "Kapcsolatfelvétel a Crafty Supporttal Discordon",
|
||||
"unableToFind": "Nem találjuk az oldalt amit keresel. Kérlek próbáld meg újra, vagy frissítsd az oldalt."
|
||||
},
|
||||
"accessDenied": {
|
||||
"noAccess": "Nincs hozzáférésed ehhez a fájlhoz",
|
||||
"accessDenied": "Hozzáférés megtagadva",
|
||||
"contact": "Kapcsolatfelvétel a Crafty Supporttal Discordon",
|
||||
"contactAdmin": "Vedd fel a kapcsolatot a szerver adminisztrátorával hogy hozzáférhess ehhez a forráshoz, vagy ha úgy gondolod ehhez a forráshoz hozzá kéne férned, vedd fel a kapcsolatot a Supporttal."
|
||||
},
|
||||
"apiKeys": {
|
||||
"apiKeys": "API kulcsok",
|
||||
"auth": "Engedélyezett? ",
|
||||
"buttons": "Gombok",
|
||||
"config": "Konfiguráció",
|
||||
"crafty": "Crafty: ",
|
||||
"createNew": "Új API Token létrehozás",
|
||||
"created": "Létrehozva",
|
||||
"deleteKeyConfirmation": "Törölni szeretnéd ezt az API kulcsot? Nem lehet visszavonni.",
|
||||
"deleteKeyConfirmationTitle": "Törlöd a ${keyId} API kulcsot?",
|
||||
"fullAccess": "Teljes hozzáférés",
|
||||
"getToken": "Szerezz egy Tokent",
|
||||
"name": "Név",
|
||||
"nameDesc": "Mi legyen a neve ennek az API Tokennek? ",
|
||||
"no": "Nem",
|
||||
"perms": "Engedélyek",
|
||||
"server": "Szerver: ",
|
||||
"yes": "Igen",
|
||||
"pageTitle": "Felhasználó API kulcs szerkesztés",
|
||||
"permName": "Engedély neve"
|
||||
},
|
||||
"base": {
|
||||
"getMFA": "Tedd biztonságossá szervered!",
|
||||
"createMFA": "Adj hozzá többlépcsős hitelesítést a fiókodhoz!",
|
||||
"doesNotWorkWithoutJavascript": "<strong>Figyelem: </strong>Crafty nem fog működni ha a JavaScript ki van kapcsolva!"
|
||||
},
|
||||
"credits": {
|
||||
"developmentTeam": "Fejlesztő csapat",
|
||||
"hugeDesc": "Egy nagy",
|
||||
"pageTitle": "Közreműködők",
|
||||
"patreonDesc": "Patreon / Ko-fi támogatónknak!",
|
||||
"patreonOther": "Egyéb",
|
||||
"patreonSupporter": "Patreon / Ko-fi támogatók",
|
||||
"patreonUpdate": "Utolsó frissítés:",
|
||||
"retiredStaff": "Visszavonult Staff",
|
||||
"subscriptionLevel": "Szint",
|
||||
"thankYou": "KÖSZÖNJÜK",
|
||||
"translationDesc": "közösségünknek akik fordítanak! [ Aktív = 🟢 Inaktív/Visszavonult = ⚪ ]",
|
||||
"translationName": "Név",
|
||||
"translationTitle": "Nyelv fordítás",
|
||||
"supportTeam": "Támogató és Dokumentációs Csapat",
|
||||
"subscriberName": "Név",
|
||||
"translator": "Fordítók",
|
||||
"pageDescription": "Ezek az emberek nélkül most nem létezne Crafty"
|
||||
},
|
||||
"customLogin": {
|
||||
"apply": "Jelentkezés",
|
||||
"customLoginPage": "Bejelentkező felület testreszabása",
|
||||
"delete": "Törlés",
|
||||
"labelLoginImage": "Válaszd ki a Bejelentkező háttérképet",
|
||||
"loginBackground": "Bejelentkező háttérkép",
|
||||
"loginOpacity": "Válaszd ki a bejelentkező ablak áttetszőségét",
|
||||
"pageTitle": "Egyedi bejelentkező felület",
|
||||
"preview": "Előnézet",
|
||||
"select": "Kiválasztás",
|
||||
"selectImage": "Válassz ki egy képet",
|
||||
"backgroundUpload": "Feltöltés a háttérben",
|
||||
"loginImage": "Bejelentkező háttérkép feltöltése."
|
||||
},
|
||||
"dashboard": {
|
||||
"actions": "Tevékenységek",
|
||||
"allServers": "Összes szerver",
|
||||
"avg": "Átlag",
|
||||
"backups": "Biztonsági mentések",
|
||||
"bePatientRestart": "Kérjük várj türelemmel, amíg újraindítjuk a szervert.<br /> Ez a képernyő egy pillanat múlva frissül",
|
||||
"bePatientStop": "Kérjük várj türelemmel, amíg leállítjuk a szervert.<br /> Ez a képernyő egy pillanat múlva frissül",
|
||||
"cannotSee": "Nem látsz mindent?",
|
||||
"cannotSeeOnMobile": "Nem látsz mindent telefonon?",
|
||||
"cannotSeeOnMobile2": "Próbáld meg oldalirányban görgetni a táblázatot.",
|
||||
"clone": "Klónozás",
|
||||
"cpuCores": "CPU magok",
|
||||
"cpuCurFreq": "CPU jelenlegi órajel",
|
||||
"cpuMaxFreq": "CPU maximum órajel",
|
||||
"cpuUsage": "CPU használat",
|
||||
"crashed": "Összeomlott",
|
||||
"dashboard": "Vezérlőpult",
|
||||
"delay-explained": "A szolgáltatás nemrég indult, és késlelteti a minecraft szerver indítását",
|
||||
"host": "Házigazda",
|
||||
"installing": "Telepítés...",
|
||||
"kill": "Folyamat azonnali megszakítása",
|
||||
"killing": "Folyamat megszakítása...",
|
||||
"lastBackup": "Utolsó:",
|
||||
"max": "Maximum",
|
||||
"memUsage": "Memória használat",
|
||||
"motd": "MOTD",
|
||||
"nextBackup": "Következő:",
|
||||
"no-servers": "Nincs elérhető szerver. A kezdéshez kattints",
|
||||
"offline": "Nem elérhető",
|
||||
"online": "Elérhető",
|
||||
"players": "Játékosok",
|
||||
"restart": "Újraindítás",
|
||||
"servers": "Szerverek",
|
||||
"size": "Szervermappa mérete",
|
||||
"start": "Indítás",
|
||||
"status": "Állapot",
|
||||
"stop": "Leállítás",
|
||||
"storage": "Tárhely",
|
||||
"version": "Verzió",
|
||||
"welcome": "Üdvözlünk a Crafty Controllerben",
|
||||
"newServer": "Új szerver létrehozása",
|
||||
"sendingCommand": "Parancs küldése",
|
||||
"server": "Szerver",
|
||||
"starting": "Késleltetett indítás",
|
||||
"bePatientClone": "Kérjük várj türelemmel, amíg klónozzuk a szervert.<br /> Ez a képernyő egy pillanat múlva frissül",
|
||||
"bePatientStart": "Kérjük várj türelemmel, amíg elindítjuk a szervert.<br /> Ez a képernyő egy pillanat múlva frissül",
|
||||
"cloneConfirm": "Biztos, hogy klónozni szeretnéd ezt a szervert? Ez a folyamat eltarthat egy ideig."
|
||||
},
|
||||
"datatables": {
|
||||
"i18n": {
|
||||
"aria": {
|
||||
"sortDescending": ": aktiválja az oszlop lefelé történő rendezéséhez",
|
||||
"sortAscending": ": aktiváld az oszlop felfelé történő rendezéséhez"
|
||||
},
|
||||
"buttons": {
|
||||
"collection": "Összegyújtés <span class='ui-button-icon-primary ui-icon ui-icon-triangle-1-s'/>",
|
||||
"colvis": "Oszlop láthatósága",
|
||||
"colvisRestore": "Láthatóság visszaállítása",
|
||||
"copy": "Másolás",
|
||||
"copySuccess": {
|
||||
"1": "Egy sor másolva a vágólapra",
|
||||
"_": "%d sor másolva a vágólapra"
|
||||
},
|
||||
"copyTitle": "Másolás a vágólapra",
|
||||
"csv": "CSV",
|
||||
"excel": "Excel",
|
||||
"pageLength": {
|
||||
"-1": "Összes sor mutatása",
|
||||
"1": "1 sor mutatása",
|
||||
"_": "%d sor mutatása"
|
||||
},
|
||||
"pdf": "PDF",
|
||||
"print": "Nyomtatás",
|
||||
"copyKeys": "Nyomd meg a ctrl vagy u2318 + C a táblázat adatainak a rendszer vágólapjára történő másolásához.<br><br>A megszakításhoz kattintson erre az üzenetre, vagy nyomja meg az escape billentyűt."
|
||||
},
|
||||
"decimal": ".",
|
||||
"emptyTable": "Nincs adat a táblában",
|
||||
"info": "_START_ to _END_ of _TOTAL_ bejegyzések megjelenítése",
|
||||
"infoFiltered": "(Szűrve a _MAX_ total bejegyzésből)",
|
||||
"infoPostFix": "|",
|
||||
"lengthMenu": "_MENU_ bejegyzés mutatása",
|
||||
"loadingRecords": "Betöltés...",
|
||||
"paginate": {
|
||||
"first": "Első",
|
||||
"last": "Utolsó",
|
||||
"next": "Következő",
|
||||
"previous": "Előző"
|
||||
},
|
||||
"processing": "Feldolgozás...",
|
||||
"search": "Keresés:",
|
||||
"select": {
|
||||
"cells": {
|
||||
"1": "%d cella kiválasztva",
|
||||
"_": "%d cellák kiválasztva",
|
||||
"0": "Kattints egy cellára a kiválasztáshoz"
|
||||
},
|
||||
"columns": {
|
||||
"0": "Kattints egy oszlopra a kiválasztáshoz",
|
||||
"1": "%d oszlop kiválasztva",
|
||||
"_": "%d oszlopok kiválasztva"
|
||||
},
|
||||
"rows": {
|
||||
"_": "%d sorok kiválasztva",
|
||||
"1": "%d sor kiválasztva",
|
||||
"0": "Kattints egy sorra a kiválasztáshoz"
|
||||
}
|
||||
},
|
||||
"thousands": ",",
|
||||
"zeroRecords": "Nem található adat",
|
||||
"infoEmpty": "0 bejegyzés mutatása"
|
||||
},
|
||||
"loadingRecords": "Betöltés..."
|
||||
},
|
||||
"error": {
|
||||
"agree": "Elfogadás",
|
||||
"bigBucket1": "Big Bucket állapotfelmérés sikertelen. Kérlek ellenőrizd",
|
||||
"bigBucket2": "a legtöbb friss információért.",
|
||||
"cancel": "Mégsem",
|
||||
"bedrockError": "Bedrock letöltés nem elérhető. Kérlek ellenőrizd",
|
||||
"not-downloaded": "Nem találtuk az indítható fájlt. Letöltött már? A jogok be vannak állítva, hogy letölthető legyen?",
|
||||
"contact": "Kapcsolatfelvétek a Crafty Supporttal Discordon",
|
||||
"craftyStatus": "Crafty státusz oldal",
|
||||
"cronFormat": "Érvénytelen Cron formátum érzékelve",
|
||||
"embarassing": "Hűha... ez kínos.",
|
||||
"error": "Hiba!",
|
||||
"eulaAgree": "Elfogadod?",
|
||||
"eulaMsg": "El kell fogadnod a ",
|
||||
"eulaTitle": "EULA elfogadása",
|
||||
"fileError": "Fájltípusnak képnek kell lennie.",
|
||||
"fileTooLarge": "Sikertelen feltöltés. Túl nagy fájlméret. Vedd fel a kapcsolatot a rendszergazdával.",
|
||||
"hereIsTheError": "Itt van a hiba",
|
||||
"duplicateId": "A kért tokennek tartalmaznia kell a session_id vagy token_id, de nem mindkettőt.",
|
||||
"installerJava": "Sikertelen telepítés {} : Forge szerverhez Java szükséges. A Java nincs telepítve. Kérlek töltsd le a Javat és telepítsd fel.",
|
||||
"internet": "Érzékeltük hogy a számítógép amin fut a Crafty, nincs internet hozzáférése. Kliens kapcsolatok a szerverrel limitáltak lehetnek.",
|
||||
"no-file": "Nem tudunk hozzáférni a kért fájlhoz. Duplakattints az elérési útvonalra. Van hozzáférése a Craftynak?",
|
||||
"noInternet": "Craftynak gondjai vannak az internet elérésével. A szerver létrehozása le lett tiltva. Kérjük, ellenőrizd az internetkapcsolatod, és frissítsd az oldalt.",
|
||||
"noJava": "Szerver {} nem indult el megfelelően a következő hiba miatt: A Java nincs telepíve. Kérlek töltsd le és telepítsd fel, majd indítsd el a szervert.",
|
||||
"privMsg": "és a ",
|
||||
"return": "Vissza a vezérlőpultba",
|
||||
"start-error": "Szerver {} nem tudott elindulni a következő hiba miatt: {}",
|
||||
"superError": "Szuper felhasznlónak kell lenne a feladat elvégzéséhez.",
|
||||
"terribleFailure": "Mekkora hiba!"
|
||||
},
|
||||
"login": {
|
||||
"backupCode": "Add meg a biztonsági kódot",
|
||||
"accountDisabled": "A felhasználói fiókodat adminisztrációval letiltották. További részletekért fordulj a rendszergazdához.",
|
||||
"burnedBackupCode": "Egyik biztonsági kódodat felhasználtad. Kérlek, generálj új biztonsági kódokat, hogy elkerüld a kizárást a fiókodból",
|
||||
"defaultPath": "A megadott jelszó az alapértelmezett hitelesítési útvonal, nem pedig a jelszó. Kérlek, keresd meg az alapértelmezett jelszót azon a helyen.",
|
||||
"disabled": "A felhasználói fiókod le lett tiltva. Kérlek, vedd fel a kapcsolatot a rendszergazdával további információért.",
|
||||
"incorrect": "Hibás felhasználónév, jelszó vagy többfaktoros hitelesítés",
|
||||
"viewStatus": "Nyisd meg a nyilvános állapotoldalt",
|
||||
"2fa": "(ha az MFA be van kapcsolva)",
|
||||
"2faRecovery": "MFA helyreállítása",
|
||||
"backupCodeTitle": "Biztonsági kód",
|
||||
"cancel": "Mégsem",
|
||||
"cooldown": "Várakozási idő van érvényben. Próbáld meg újra ennyi idő múlva",
|
||||
"forgotPassword": "Elfelejtett jelszó",
|
||||
"totpSelect": "Hitelesítő alkalmazás",
|
||||
"login": "Bejelentkezés",
|
||||
"password": "Jelszó",
|
||||
"passwordRecovery": "Jelszó helyreállítása",
|
||||
"username": "Felhasználónév",
|
||||
"totp": "Add meg az azonosító alkalmazás kódját"
|
||||
},
|
||||
"notify": {
|
||||
"backupComplete": "A mentés sikeresen befejeződött a(z) {} szerveren",
|
||||
"backup_desc": "Észleltük, hogy a mentés átvitele részben vagy teljesen sikertelen lehetett. Kérlek, ellenőrizd a mentéseid adatait a mentések fülön.",
|
||||
"preparingLogs": " Kérlek várj, amíg előkészítjük a naplóidat... Értesítünk, amikor készen vannak. Nagyobb rendszerek esetén ez eltarthat egy ideig.",
|
||||
"activityLog": "Tevékenységnaplók",
|
||||
"accountSettings": "Fiókbeállítások",
|
||||
"backupStarted": "A mentés elindult a(z) {} szerveren",
|
||||
"backup_title": "Figyelmeztetés a mentés átvitelével kapcsolatban",
|
||||
"downloadLogs": "Letöltöd a támogatási naplókat?",
|
||||
"finishedPreparing": "Elkészítettük a támogatási naplóidat. Kattints a letöltésre, hogy letöltsd őket",
|
||||
"logout": "Kijelentkezés",
|
||||
"schedule_desc": "Észleltük, hogy az ütemezett feladataid egy részét vagy mindegyiket nem sikerült átvinni a frissítés során. Kérlek, ellenőrizd az ütemezéseidet az ütemezések fülön.",
|
||||
"schedule_title": "Figyelmeztetés az ütemezések átvitelével kapcsolatban",
|
||||
"supportLogs": "Támogatási naplók"
|
||||
},
|
||||
"otp": {
|
||||
"backupCodesMax": "Már elérted a maximum 6 biztonsági kódot.",
|
||||
"verifyTitle": "Ellenőrizd a többfaktoros hitelesítési módszert az azonosító alkalmazással",
|
||||
"newName": "Adj egy könnyen megjegyezhető nevet ennek a többfaktoros hitelesítési módszernek",
|
||||
"deleteConfirm": "Biztos vagy benne, hogy törölni akarod ezt a többfaktoros hitelesítési módszert?",
|
||||
"mfaWarn": "A rendszer használatához engedélyezned kell a többfaktoros hitelesítést. Kérlek, hozz létre egy MFA kulcsot. Ha már létrehoztál kulcsot, de ezt az üzenetet látod, jelentkezz ki, majd jelentkezz be újra az MFA-val.",
|
||||
"name": "Név",
|
||||
"id": "ID",
|
||||
"config": "Konfiguráció",
|
||||
"copyBackupCodes": "Kódok másolása",
|
||||
"delete": "Törlés",
|
||||
"yes": "Igen",
|
||||
"no": "Nem",
|
||||
"2faCreate": "Új többfaktoros hitelesítési módszer hozzáadása",
|
||||
"backupCodes": "Kérlek, másold a biztonsági kódjaidat.",
|
||||
"backupOtp": "Biztonsági kódok kéréséhez szükséged van egy többfaktoros hitelesítési módszerre",
|
||||
"confirm2FA": "Kérlek, erősítsd meg az azonosító alkalmazás kódját",
|
||||
"otpTitle": "Többfaktoros hitelesítési módszerek",
|
||||
"renewRecovery": "Generáld újra a helyreállító kódokat",
|
||||
"newOTP": "Új többfaktoros hitelesítés hozzáadása",
|
||||
"verify": "Nem sikerült ellenőrizni a többfaktoros hitelesítést. Hibás kódot adtál meg.",
|
||||
"goToPage": "Ugrás a többfaktoros hitelesítés oldalra",
|
||||
"otpReq": "Ehhez a felhasználóhoz kötelező a többfaktoros hitelesítés. Nem törölheted az egyetlen MFA módszeredet."
|
||||
},
|
||||
"footer": {
|
||||
"allRightsReserved": "Minden jog fenntartva",
|
||||
"copyright": "Szerzői jog",
|
||||
"version": "Verzió"
|
||||
},
|
||||
"offline": {
|
||||
"offline": "Nem elérhető",
|
||||
"pleaseConnect": "Kérlek, csatlakozz az internetre, hogy használhasd a Craftyt."
|
||||
},
|
||||
"panelConfig": {
|
||||
"adminControls": "Adminisztrátori beállítások"
|
||||
}
|
||||
}
|
||||
@@ -135,7 +135,8 @@
|
||||
"userName": " 문자열이어야 하며, 모두 소문자로 입력해야 하고, 최소 4자에서 최대 20자까지 입력할 수 있습니다",
|
||||
"2FAerror": "다중 인증 코드는 6자리 숫자(예: 000000)여야 하거나, 16자 문자열을 4글자씩 하이픈(-)으로 구분한 백업 코드(예: ABCD-EFGH-IJKL-MNOP)여야 합니다.",
|
||||
"totp": "MFA 코드는 6자리 숫자여야 합니다.",
|
||||
"additionalProperties": "추가 속성을 전달할 수 없습니다"
|
||||
"additionalProperties": "추가 속성을 전달할 수 없습니다",
|
||||
"mfaName": "속성에 대한 입력은 문자열 형식이어야 하며 최소 3자 이상이어야 합니다"
|
||||
},
|
||||
"webhooks": {
|
||||
"trigger": "트리거",
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
"fullAccess": "Volledige Toegang"
|
||||
},
|
||||
"base": {
|
||||
"doesNotWorkWithoutJavascript": "<strong>Warning: </strong>Crafty werkt niet naar behoren als JavaScript niet ingeschakeld is!"
|
||||
"doesNotWorkWithoutJavascript": "<strong>Warning: </strong>Crafty werkt niet naar behoren als JavaScript niet ingeschakeld is!",
|
||||
"getMFA": "Beveilig je servers!",
|
||||
"createMFA": "Voeg Multi Factor Authenticatie toe aan je account!"
|
||||
},
|
||||
"credits": {
|
||||
"developmentTeam": "Ontwikkelingsteam",
|
||||
@@ -222,7 +224,8 @@
|
||||
"passwordRecovery": "Wachtwoord Herstellen",
|
||||
"totp": "Voer Authenticatiecode In",
|
||||
"backupCodeTitle": "Back-upcode",
|
||||
"backupCode": "Voeg Back-upcode In"
|
||||
"backupCode": "Voeg Back-upcode In",
|
||||
"accountDisabled": "Je gebruikersaccount is uitgeschakeld door een administrator. Neem contact op met je systeembeheerder voor meer info."
|
||||
},
|
||||
"notify": {
|
||||
"activityLog": "Activiteit-logboeken",
|
||||
@@ -293,7 +296,8 @@
|
||||
"serverAccess": "Toegang?",
|
||||
"serverName": "Servernaam",
|
||||
"serversDesc": "Servers waar deze rol toegang tot heeft",
|
||||
"selectManager": "Selecteer een manager voor deze rol"
|
||||
"selectManager": "Selecteer een manager voor deze rol",
|
||||
"requireMFA": "Gebruikers met een rol moeten MFA (Multi Factor Authenticatie) aanzetten"
|
||||
},
|
||||
"serverBackups": {
|
||||
"backupAtMidnight": "Automatische back-up bij middernacht?",
|
||||
@@ -657,7 +661,7 @@
|
||||
"editTOTP": "Gebruiker MFA bewerken",
|
||||
"changeUser": "Gebruikersnaam Veranderen",
|
||||
"hints": "Aanzetten Hints?",
|
||||
"totpIdReq": "TOTP ID Benodigd voor verzoek",
|
||||
"totpIdReq": "MFA ID Benodigd voor verzoek",
|
||||
"totpHeader": "Multi Factor Authenticatie"
|
||||
},
|
||||
"customLogin": {
|
||||
@@ -700,7 +704,9 @@
|
||||
"typeInteger": "moet een getal zijn.",
|
||||
"typeString": "moet van het type string zijn.",
|
||||
"2FAerror": "Multi-factorcode moet 6 cijfers (b.v.b. 000000) of een back-upcode of 16 karakters, elke vier gescheiden door - (b.v.b. ABCD-EFGH-IJKL-MNOP) zijn",
|
||||
"totp": "TOTP-code moet 6 nummers zijn"
|
||||
"totp": "MFA-code moet 6 nummers zijn",
|
||||
"mfaName": "Invoer moet van type String zijn van minstens 3 karakters voor deze eigenschap",
|
||||
"additionalProperties": "Aanvullende Eigenschappen mogen niet meegestuurd worden"
|
||||
},
|
||||
"serverMetrics": {
|
||||
"zoomHint1": "Om in te zoomen op de grafiek, houdt je de shift-toets ingedrukt en gebruik je vervolgens je scrollwiel.",
|
||||
@@ -764,7 +770,8 @@
|
||||
"renewRecovery": "Herstelcodes Vernieuwen",
|
||||
"newOTP": "Nieuwe Multi-Factor Authenticatie Toevoegen",
|
||||
"verifyTitle": "MFA-methode verifiëren met authenticator",
|
||||
"goToPage": "Ga naar MFA-pagina"
|
||||
"goToPage": "Ga naar MFA-pagina",
|
||||
"otpReq": "MFA is benodigd voor deze gebruikers. Je kan niet je enigste MFA-methode verwijderen."
|
||||
},
|
||||
"configJson": {
|
||||
"delete_default_json": "Verwijder standaard JSON-bestand tijdens opstarten",
|
||||
@@ -791,6 +798,12 @@
|
||||
"show_errors": "Toon webserver debug fouten",
|
||||
"stats_update_frequency_seconds": "Hoe vaak we jouw server moeten peilen voor statistieken (seconden)",
|
||||
"submit": "Indienen",
|
||||
"True": "Juist"
|
||||
"True": "Juist",
|
||||
"security": "Beveiliging",
|
||||
"general": "Algemeen",
|
||||
"logs": "Registratie",
|
||||
"monitoring": "Toezicht",
|
||||
"miscellaneous": "Overig",
|
||||
"superMFA": "Supergebruikers moeten MFA aanzetten"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"apiKeys": "Klucze API",
|
||||
"auth": "Zezwól? ",
|
||||
"buttons": "Akcje",
|
||||
"config": "Config",
|
||||
"config": "Ustawienia",
|
||||
"crafty": "Crafty: ",
|
||||
"createNew": "Stwórz nowy klucz API",
|
||||
"created": "Stworzono",
|
||||
@@ -32,12 +32,14 @@
|
||||
"yes": "Tak"
|
||||
},
|
||||
"base": {
|
||||
"doesNotWorkWithoutJavascript": "<strong>Uwaga: </strong>Crafty nie działa gdy JavaScript jest wyłączone!"
|
||||
"doesNotWorkWithoutJavascript": "<strong>Uwaga: </strong>Crafty nie działa gdy JavaScript jest wyłączone!",
|
||||
"getMFA": "Zabezpiecz swoje serwery!",
|
||||
"createMFA": "Dodaj logowanie wieloskładnikowe do swojego konta!"
|
||||
},
|
||||
"credits": {
|
||||
"developmentTeam": "Drużyna Deweloperów",
|
||||
"hugeDesc": "Wielkie",
|
||||
"pageDescription": "Bez tych ludzi nie miał być Craftiego!",
|
||||
"pageDescription": "Bez tych ludzi nie miał byś Craftiego!",
|
||||
"pageTitle": "Podziękowania",
|
||||
"patreonDesc": "dla naszych ludzi wsparcia z Patreon / Ko-fi!",
|
||||
"patreonOther": "Inne",
|
||||
@@ -54,7 +56,7 @@
|
||||
"translator": "Tłumacze"
|
||||
},
|
||||
"customLogin": {
|
||||
"apply": "Apply",
|
||||
"apply": "Zastosuj",
|
||||
"backgroundUpload": "Załącz tło",
|
||||
"customLoginPage": "Dostosuj stronę zalogowania",
|
||||
"delete": "Usuń",
|
||||
@@ -70,7 +72,7 @@
|
||||
"dashboard": {
|
||||
"actions": "Akcje",
|
||||
"allServers": "Wszystkie serwery",
|
||||
"avg": "Avg",
|
||||
"avg": "Śr.",
|
||||
"backups": "Backupy",
|
||||
"bePatientClone": "Poczekaj, gdy my klonujemy serwer.<br /> Strona za chwilę się odświeży",
|
||||
"bePatientRestart": "Poczekaj, gdy my restartujemy twój serwer.<br /> Strona za chwilę się odświeży",
|
||||
@@ -170,9 +172,9 @@
|
||||
"_": "%d kolumn zaznaczonych"
|
||||
},
|
||||
"rows": {
|
||||
"0": "Click on a row to select it",
|
||||
"1": "%d row selected",
|
||||
"_": "%d rows selected"
|
||||
"0": "Kliknij na rząd aby go zaznaczyć",
|
||||
"1": "%d rząd zaznaczony",
|
||||
"_": "%d rzędów zaznaczonych"
|
||||
}
|
||||
},
|
||||
"thousands": ",",
|
||||
@@ -188,8 +190,8 @@
|
||||
"cancel": "Anuluj",
|
||||
"contact": "Podrzebujesz pomocy? Zapraszamy na serwer discord Crafty Controler",
|
||||
"craftyStatus": "Status strony Craftyiego",
|
||||
"cronFormat": "Nieprawidłowy format Cron",
|
||||
"embarassing": "Oh, więc, to jest żenujące.",
|
||||
"cronFormat": "Wykryto nieprawidłowy format Cron",
|
||||
"embarassing": "Ojej, więc, to jest żenujące.",
|
||||
"error": "Błąd!",
|
||||
"eulaAgree": "Czy się zgadzasz?",
|
||||
"eulaMsg": "Musisz się zgodzić na EULA. Kopia EULA Minecraft jest zalinkowana pod tą wiadomością. ",
|
||||
@@ -210,7 +212,8 @@
|
||||
"selfHost": "Jeśli zarządasz tą repozytorią upewnij się że adres hest poprawny, w innym przypadku odwiedź strone rozwiązywania problemów.",
|
||||
"start-error": "Serwer {} nie mógł się odpalić z powodu: {}",
|
||||
"superError": "Potrzebujesz uprawnienie Adminitsratora aby zakończyć tę akcję.",
|
||||
"terribleFailure": "Okropny błąd!"
|
||||
"terribleFailure": "Okropny błąd!",
|
||||
"duplicateId": "Żądanie tokenów może zawierać tylko session_id lub token_id, nie oba jednocześnie."
|
||||
},
|
||||
"footer": {
|
||||
"allRightsReserved": "Wszelkie Prawa zastrzeżone",
|
||||
@@ -221,11 +224,22 @@
|
||||
"defaultPath": "Hasło które wprowadziłeś jest podstawową ścieżką w której przechowywane są dane logowania. Znajdź podstawowe hasło w tej lokalizacji.",
|
||||
"disabled": "Konto tego użytkownika jest wyłączone. Skontaktuj się z administratorem by uzyskać więcej informacji.",
|
||||
"forgotPassword": "Zapomniałem hasła",
|
||||
"incorrect": "Niepoprawny login lub hasło/Niepoprawna nazwa użytkownika lub hasło",
|
||||
"incorrect": "Niepoprawny login lub hasło/Niepoprawna nazwa użytkownika, hasło lub kod MFA (wieloskładnikowy/2FA)",
|
||||
"login": "Zaloguj się",
|
||||
"password": "Hasło",
|
||||
"username": "Nazwa użytkownika",
|
||||
"viewStatus": "Sprawdź status serwerów"
|
||||
"viewStatus": "Sprawdź status serwerów",
|
||||
"2fa": "(Jeśli logowanie wieloskładnikowe jest włączone)",
|
||||
"2faRecovery": "Odzyskaj klucz logowania wieloskładnikowego",
|
||||
"backupCodeTitle": "Kod zapasowy",
|
||||
"backupCode": "Wprowadź kod zapasowy",
|
||||
"burnedBackupCode": "Właśnie użyłeś jednego z twoich kodów zapasowych. Utwórz nowe abyś nie utracił dostępu",
|
||||
"cancel": "Anuluj",
|
||||
"cooldown": "Za dużo prób. Spróbuj ponownie za",
|
||||
"totpSelect": "Aplikacja do logowania wieloskładnikowego",
|
||||
"passwordRecovery": "Odzyskaj hasło",
|
||||
"accountDisabled": "Twoje konto zostało wyłączone przez administratora. Skontaktuj się z administratorem systemu w celu uzyskania dalszych informacji.",
|
||||
"totp": "Wprowadź kod wieloskładnikowy"
|
||||
},
|
||||
"notify": {
|
||||
"activityLog": "Logi Aktywności",
|
||||
@@ -236,7 +250,8 @@
|
||||
"logout": "Wyloguj się",
|
||||
"preparingLogs": " Poczekaj kiedy my przygotowujemy twoje logi ... Wyślemy ci powiadomienie kiedy będą gotowe. To trochę zajmie na dużych serwerach.",
|
||||
"supportLogs": "Logi Pomocnicze",
|
||||
"backup_desc": "Wykryliśmy, że migracja kopii zapasowych nie powiodła się, lub mogła zostać uszkodzona. Sprawdź status kopii sekcji backupów."
|
||||
"backup_desc": "Wykryliśmy, że migracja kopii zapasowych nie powiodła się, lub mogła zostać uszkodzona. Sprawdź status kopii sekcji backupów.",
|
||||
"accountSettings": "Ustawienia konta"
|
||||
},
|
||||
"offline": {
|
||||
"offline": "Offline",
|
||||
@@ -445,7 +460,7 @@
|
||||
"deleteItemQuestionMessage": "Usuwasz \\\"\" + path + \"\\\"!<br/><br/>Ta akcja jest nieodwracalna i zostanie usunięta na zawsze!",
|
||||
"download": "Pobierz",
|
||||
"editingFile": "Edytuj plik",
|
||||
"error": "Error while getting files",
|
||||
"error": "Błąd podczas wczytywania plików",
|
||||
"fileReadError": "Error odczytu pliku",
|
||||
"files": "Pliki",
|
||||
"keybindings": "Skróty klawiszowe",
|
||||
@@ -504,7 +519,7 @@
|
||||
},
|
||||
"serverSchedules": {
|
||||
"action": "Akcja",
|
||||
"actionId": "Zaznacz zadanie podwładne",
|
||||
"actionId": "Zadanie podwładne",
|
||||
"areYouSure": "Usuń zaplanowane (zadanie)?",
|
||||
"cancel": "Anuluj",
|
||||
"cannotSee": "Nie widzisz wszystkiego?",
|
||||
@@ -543,7 +558,7 @@
|
||||
"serverStatus": "Status serwera",
|
||||
"serverTime": "Czas UTC",
|
||||
"serverTimeZone": "Strefa czasowa serwera",
|
||||
"serverUptime": "Server Uptime",
|
||||
"serverUptime": "Czas pracy serwera",
|
||||
"starting": "Opóźniony-Start",
|
||||
"unableToConnect": "Nie można połączyć",
|
||||
"version": "Wersja"
|
||||
@@ -606,7 +621,7 @@
|
||||
"unsupported": "Wersje Minecrafta poniżej 1.8 nie są wspierane przez Crafty. Mimo to możesz je zainstalować - jednakże nie jesteśmy w stanie zagwarantować ich działanie.",
|
||||
"uploadButton": "Wgraj",
|
||||
"uploadZip": "Wgraj plik Zip pod importowanie serwera",
|
||||
"zipPath": "Server Path"
|
||||
"zipPath": "Ścieżka serwera"
|
||||
},
|
||||
"sidebar": {
|
||||
"contribute": "Wspomóż nas",
|
||||
@@ -640,8 +655,8 @@
|
||||
"delSuper": "Nie możesz usunąć SuperUżytkownika",
|
||||
"deleteUser": "Usuń użytkownika: ",
|
||||
"deleteUserB": "Usuń użytkownika",
|
||||
"enabled": "Enabled",
|
||||
"gravDesc": "To jest email tylko używany do Gravatar™. Crafty nie użyje pod żadnymi okolicznościami twojego adresu email, niż sprawdzenie ikony Gravatar™",
|
||||
"enabled": "Włączony?",
|
||||
"gravDesc": "To jest email tylko używany przez Gravatar™. Crafty nie użyje pod żadnymi okolicznościami twojego adresu email, niż sprawdzenie ikony Gravatar™",
|
||||
"gravEmail": "Gravatar™ Email",
|
||||
"lastIP": "Ostatnie IP: ",
|
||||
"lastLogin": "Ostatni login: ",
|
||||
@@ -665,7 +680,11 @@
|
||||
"userRolesDesc": "Role, które ten użytkownik posiada.",
|
||||
"userSettings": "Ustawienia użytkownika",
|
||||
"userTheme": "Wygląd interfejsu",
|
||||
"uses": "Ilość użyć (-1==Bez limitu)"
|
||||
"uses": "Ilość użyć (-1==Bez limitu)",
|
||||
"totpHeader": "Logowanie wieloskładnikowe",
|
||||
"hints": "Włącz porady?",
|
||||
"changePass": "Zmień hasło",
|
||||
"changeUser": "Zmień nazwę użytkownika"
|
||||
},
|
||||
"validators": {
|
||||
"passLength": "Hasło jest zbyt krótkie. Hasło musi mieć minimum 8 znaków."
|
||||
@@ -694,5 +713,33 @@
|
||||
"url": "Link Webhooka",
|
||||
"webhook_body": "Treść Webhooka",
|
||||
"webhooks": "Webhooki"
|
||||
},
|
||||
"otp": {
|
||||
"name": "Nazwa",
|
||||
"id": "ID",
|
||||
"copyBackupCodes": "Skopiuj kody",
|
||||
"delete": "Usuń",
|
||||
"deleteConfirm": "Czy jesteś pewny, że chcesz usunąć tą metodę logowania wieloskładnikowego?",
|
||||
"yes": "Tak",
|
||||
"no": "Nie",
|
||||
"backupCodes": "Skopiuj swoje kody zapasowe.",
|
||||
"backupCodesMax": "Max 6 kodów zapasowych został osiągnięty.",
|
||||
"saveWarn": "Upewnij się że zapiszesz te kody zapasowe, w przeciwnym wypadku możesz utracić dostęp",
|
||||
"renewRecovery": "Odnów kody zapasowe",
|
||||
"newOTP": "Dodaj nowy autoryzator logowania wieloskładnikowego",
|
||||
"2faCreate": "Dodaj nową metodę logowania wieloskładnikowego",
|
||||
"verify": "Nie udało się zweryfikować kodu wieloskładnikowego. Kod jest niepoprawny.",
|
||||
"backupOtp": "Metoda logowania wieloskładnikowego jest potrzebna aby uzyskać kody zapasowe/przywracania"
|
||||
},
|
||||
"configJson": {
|
||||
"general": "Ogólne",
|
||||
"security": "Bezpieczeństwo",
|
||||
"logs": "Logi",
|
||||
"monitoring": "Monitorowanie",
|
||||
"miscellaneous": "Inne",
|
||||
"disabled_language_files": "Systemowo wyłączone języki",
|
||||
"enable_user_self_delete": "Zezwalaj użytkownikom na usuwanie ich własnego konta",
|
||||
"True": "Prawda",
|
||||
"False": "Fałsz"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -720,7 +720,8 @@
|
||||
"typeList": "должен быть типа list/array ",
|
||||
"2FAerror": "Многофакторный код должен состоять из 6 цифр (например, 000000) или резервного кода из 16 символов, разделенных \"-\" через каждые 4 символа (например, ABCD-EFGH-IJKL-MNOP).",
|
||||
"totp": "MFA код должен состоять из 6 цифр",
|
||||
"additionalProperties": "Передача дополнительных свойств запрещена"
|
||||
"additionalProperties": "Передача дополнительных свойств запрещена",
|
||||
"mfaName": "Вводимые данные должны иметь тип string и содержать минимум 3 символа для свойства"
|
||||
},
|
||||
"webhooks": {
|
||||
"areYouSureDel": "Вы уверены, что хотите удалить этот вебхук?",
|
||||
|
||||
@@ -720,7 +720,8 @@
|
||||
"typeInteger": "bir sayı olmalıdır.",
|
||||
"typeString": "dize türünde olmalıdır.",
|
||||
"userName": " dize türünde, tümü KÜÇÜK HARF, en az 4 karakter ve en fazla 20 karakter olmalıdır",
|
||||
"additionalProperties": "Ek Özelliklerin geçirilmesine izin verilmez"
|
||||
"additionalProperties": "Ek Özelliklerin geçirilmesine izin verilmez",
|
||||
"mfaName": "Özellik için girdi, dize türünde olmalı ve en az 3 karakter içermelidir"
|
||||
},
|
||||
"webhooks": {
|
||||
"areYouSureDel": "Bu webhooku silmek istediğinizden emin misiniz?",
|
||||
|
||||
@@ -32,7 +32,9 @@
|
||||
"yes": "Так"
|
||||
},
|
||||
"base": {
|
||||
"doesNotWorkWithoutJavascript": "<strong>Увага: </strong>Crafty не працює належним чином коли JavaScript вимкненний!"
|
||||
"doesNotWorkWithoutJavascript": "<strong>Увага: </strong>Crafty не працює належним чином коли JavaScript вимкненний!",
|
||||
"getMFA": "Захистіть свої сервери!",
|
||||
"createMFA": "Додай Мультифакторну Автентифікацію для свого аккаунту!"
|
||||
},
|
||||
"credits": {
|
||||
"developmentTeam": "Команда Розробки",
|
||||
@@ -236,7 +238,8 @@
|
||||
"totp": "Введіть код автентифікації",
|
||||
"2fa": "(Якщо MFA включено)",
|
||||
"backupCodeTitle": "Код відновлення",
|
||||
"backupCode": "Введіть код відновлення"
|
||||
"backupCode": "Введіть код відновлення",
|
||||
"accountDisabled": "Твій аккаунт був вимкнений адміністратором. Напишіть адміністратору даного сайту для отримання доп. інформації."
|
||||
},
|
||||
"notify": {
|
||||
"activityLog": "Логи активностей",
|
||||
@@ -311,7 +314,8 @@
|
||||
"selectManager": "Виберіть менеджера для цієї ролі",
|
||||
"serverAccess": "Доступ?",
|
||||
"serverName": "Назва сервера",
|
||||
"serversDesc": "сервери які доступні для цієї ролі"
|
||||
"serversDesc": "сервери які доступні для цієї ролі",
|
||||
"requireMFA": "Вимагати у юзерів вмикати 2фа (Мультифакторна Автентифікація)"
|
||||
},
|
||||
"serverBackups": {
|
||||
"actions": "Дії",
|
||||
@@ -690,11 +694,12 @@
|
||||
"hints": "Включити підказки?",
|
||||
"changePass": "Змінити Пароль",
|
||||
"changeUser": "Змінити Ім'я Користувача",
|
||||
"totpIdReq": "TOTP ID Вимагається для запиту",
|
||||
"totpIdReq": "MFA ID Вимагається для запиту",
|
||||
"totpHeader": "Мульти-Факторна Аутентифікація"
|
||||
},
|
||||
"validators": {
|
||||
"passLength": "Пароль, надто короткий. Мінімальна довжина: 8 символів",
|
||||
"numbericPassword": "Пароль складається з цифр. Повинен містити хоча одну літеру",
|
||||
"enumErr": "помилка валідації. Прийнятні дані включають: ",
|
||||
"insufficientPerms": "Помилка доступу: Відсутній доступ до цього ресурсу",
|
||||
"roleServerId": "Властивість Server ID має бути рядком з мінімальною довжиною 1",
|
||||
@@ -710,12 +715,14 @@
|
||||
"roleServerPerms": "Дозволи сервера повинні бути 8-бітним рядком",
|
||||
"serverExeCommand": "Команда виконання сервера повинна бути рядком довжиною не менше 1.",
|
||||
"serverLogPath": "Шлях до журналу сервера має бути рядком з мінімальною довжиною 1",
|
||||
"totp": "Код TOTP повинен складатися з 6 цифр",
|
||||
"totp": "Код MFA повинен складатися з 6 цифр",
|
||||
"typeBool": "має бути true або false (тип boolean)",
|
||||
"typeEmail": "має бути типу email.",
|
||||
"typeInteger": "має бути номером.",
|
||||
"typeList": "має бути типу список/масив ",
|
||||
"typeString": "має бути типу string."
|
||||
"typeString": "має бути типу string.",
|
||||
"additionalProperties": "Додаткові властивості не можна передавати",
|
||||
"mfaName": "Вхідні дані повинні мати тип string і мінімум 3 символи для властивості"
|
||||
},
|
||||
"webhooks": {
|
||||
"areYouSureDel": "Ви впевнені, що хочете видалити цей Вебхук?",
|
||||
@@ -764,7 +771,8 @@
|
||||
"renewRecovery": "Поновити резервні коди",
|
||||
"newOTP": "Додати нову мультифакторну аутентифікацію",
|
||||
"verify": "Помилка верифікації MFA. Введений не правильний код.",
|
||||
"goToPage": "Перейти на сторінку MFA"
|
||||
"goToPage": "Перейти на сторінку MFA",
|
||||
"otpReq": "Мультифакторна автентифікація обов'язкова для цього юзера. Ви не можете видалити єдиний 2фа ключ."
|
||||
},
|
||||
"configJson": {
|
||||
"max_audit_entries": "Максимальна кількість записів в журналі Crafty",
|
||||
@@ -791,6 +799,12 @@
|
||||
"reset_secrets_on_next_boot": "Скинути секрети при наступному запуску",
|
||||
"show_contribute_link": "Показувати посилання спонсорів у панелі",
|
||||
"show_errors": "Показати дебаг помилки вебсервера",
|
||||
"submit": "Відправити"
|
||||
"submit": "Відправити",
|
||||
"general": "Загальне",
|
||||
"security": "Безпека",
|
||||
"logs": "Логування",
|
||||
"monitoring": "Моніторинг",
|
||||
"miscellaneous": "Різне",
|
||||
"superMFA": "Вимагати Суперюзерів вмикати 2фа"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -718,9 +718,10 @@
|
||||
"typeIntMinVal0": "必须为最小值为 0 的整数。",
|
||||
"typeInteger": "必须为数字。",
|
||||
"typeString": "必须为字符串类型。",
|
||||
"2FAerror": "多因素代码必须为 6 位(例如:000000)或一个以 - 分隔为每 4 位一段的备份代码(例如:ABCD-EFGH-IJKL-MNOP)",
|
||||
"2FAerror": "多因素代码必须为 6 位(例如:000000)或一个 16 字符的备份代码,以 - 分隔为每 4 个字符一段(例如:ABCD-EFGH-IJKL-MNOP)",
|
||||
"totp": "MFA 代码必须为 6 个数字",
|
||||
"additionalProperties": "不允许传递额外属性"
|
||||
"additionalProperties": "不允许传递额外属性",
|
||||
"mfaName": "输入的属性必须为字符串类型,且最短为 3 个字符"
|
||||
},
|
||||
"webhooks": {
|
||||
"areYouSureDel": "您确定要删除此 webhook 吗?",
|
||||
|
||||
4
main.py
4
main.py
@@ -9,10 +9,10 @@ import signal
|
||||
import peewee
|
||||
from packaging import version as pkg_version
|
||||
|
||||
from app.classes.shared.file_helpers import FileHelpers
|
||||
from app.classes.helpers.file_helpers import FileHelpers
|
||||
from app.classes.shared.import3 import Import3
|
||||
from app.classes.shared.console import Console
|
||||
from app.classes.shared.helpers import Helpers
|
||||
from app.classes.helpers.helpers import Helpers
|
||||
from app.classes.models.users import HelperUsers
|
||||
from app.classes.models.management import HelpersManagement
|
||||
from app.classes.shared.import_helper import ImportHelpers
|
||||
|
||||
@@ -22,4 +22,5 @@ prometheus-client==0.17.1
|
||||
pyotp==2.9.0
|
||||
pillow==10.4.0
|
||||
httpx==0.28.1
|
||||
aiofiles==24.1.0
|
||||
aiofiles==24.1.0
|
||||
anyio==4.9.0
|
||||
|
||||
131
tests/classes/helpers/test_cryptography_helper.py
Normal file
131
tests/classes/helpers/test_cryptography_helper.py
Normal file
@@ -0,0 +1,131 @@
|
||||
import pytest
|
||||
|
||||
from app.classes.helpers.cryptography_helper import CryptoHelper
|
||||
|
||||
|
||||
def test_blake2_hash_bytes_known_value() -> None:
|
||||
"""
|
||||
Test blake2_hash_bytes_known_value with known input and output values.
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
known_input: bytes = b"hello world"
|
||||
known_output: bytes = bytes.fromhex(
|
||||
"021ced8799296ceca557832ab941a50b4a11f83478cf141f51f933f653ab9fbcc05a037cddbed0"
|
||||
"6e309bf334942c4e58cdf1a46e237911ccd7fcf9787cbc7fd0"
|
||||
)
|
||||
assert CryptoHelper.blake2b_hash_bytes(known_input) == known_output
|
||||
|
||||
|
||||
def test_bytes_to_b64_known_value() -> None:
|
||||
"""
|
||||
Test bytes_to_b64 with known input and output values.
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
# Test 1
|
||||
known_value: bytes = b"hello world"
|
||||
known_output: str = "aGVsbG8gd29ybGQ="
|
||||
assert CryptoHelper.bytes_to_b64(known_value) == known_output
|
||||
|
||||
# Test 2
|
||||
known_value_2: bytes = bytes.fromhex(
|
||||
"ca2b62821a7e069b7048508f1f2b6947cb7d1e196008da1d43cb7b0c1971ce78bfc5bb7d2cb37f"
|
||||
"c23cfaec56c870582ebf99237038405cec8b1626c20756e5dd"
|
||||
)
|
||||
known_output_2: str = (
|
||||
"yitighp+BptwSFCPHytpR8t9HhlgCNodQ8t7DBlxzni/xbt9LLN/wjz67FbIcFguv5kjcDhAXOyLFi"
|
||||
"bCB1bl3Q=="
|
||||
)
|
||||
assert CryptoHelper.bytes_to_b64(known_value_2) == known_output_2
|
||||
|
||||
|
||||
def test_b64_to_bytes() -> None:
|
||||
known_input: bytes = bytes.fromhex(
|
||||
"69666a2bc393d582730e4db6974987707ba4c26211b78a174ab71b69bd446877"
|
||||
)
|
||||
known_output: str = "aWZqK8OT1YJzDk22l0mHcHukwmIRt4oXSrcbab1EaHc="
|
||||
assert CryptoHelper.bytes_to_b64(known_input) == known_output
|
||||
|
||||
known_input_2: bytes = bytes.fromhex("1b5a3db0ca943f041a6010498bad753fa4e51893")
|
||||
known_output_2: str = "G1o9sMqUPwQaYBBJi611P6TlGJM="
|
||||
assert CryptoHelper.bytes_to_b64(known_input_2) == known_output_2
|
||||
|
||||
|
||||
def test_bytes_to_hex_known_value() -> None:
|
||||
"""
|
||||
Test bytes_to_hex with known input and output values.
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
# Test 1
|
||||
known_value_1: bytes = b"hello world"
|
||||
known_output_1: str = "68656c6c6f20776f726c64"
|
||||
assert CryptoHelper.bytes_to_hex(known_value_1) == known_output_1
|
||||
|
||||
# Test 2
|
||||
known_value_2: bytes = b"boatisthebest"
|
||||
known_output_2: str = "626f6174697374686562657374"
|
||||
assert CryptoHelper.bytes_to_hex(known_value_2) == known_output_2
|
||||
|
||||
|
||||
def test_str_to_b64() -> None:
|
||||
"""
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
# Test 1
|
||||
known_value_1: str = "Hello World"
|
||||
known_output_1: str = "SGVsbG8gV29ybGQ="
|
||||
assert CryptoHelper.str_to_b64(known_value_1) == known_output_1
|
||||
|
||||
known_value_2: str = "I love Crafty! Yee haw!"
|
||||
known_output_2: str = "SSBsb3ZlIENyYWZ0eSEgWWVlIGhhdyE="
|
||||
assert CryptoHelper.str_to_b64(known_value_2) == known_output_2
|
||||
|
||||
|
||||
def test_b64_to_str() -> None:
|
||||
"""
|
||||
Test known input with b64_to_str
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
# Test 1
|
||||
known_value_1: str = "SGVsbG8gV29ybGQ="
|
||||
known_output_1: str = "Hello World"
|
||||
assert CryptoHelper.b64_to_str(known_value_1) == known_output_1
|
||||
|
||||
# Test 2
|
||||
known_value_2: str = "SSBsb3ZlIENyYWZ0eSEgWWVlIGhhdyE="
|
||||
known_output_2: str = "I love Crafty! Yee haw!"
|
||||
assert CryptoHelper.b64_to_str(known_value_2) == known_output_2
|
||||
|
||||
|
||||
def test_b64_to_str_not_b64() -> None:
|
||||
"""
|
||||
Test b64_to_str function when give a value that is not b64 encoded. Should return
|
||||
RuntimeError.
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
test_error_value: str = "!This is not B64 encoded text!"
|
||||
with pytest.raises(RuntimeError):
|
||||
_ = CryptoHelper.b64_to_str(test_error_value)
|
||||
|
||||
|
||||
def test_b64_to_str_not_unicode() -> None:
|
||||
"""
|
||||
Test b64_to_str with data that is not Unicode. Should return RuntimeError.
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
random_data: str = "gQ=="
|
||||
with pytest.raises(RuntimeError):
|
||||
_ = CryptoHelper.b64_to_str(random_data)
|
||||
Reference in New Issue
Block a user