Merge branch 'dev' into dev-ImprovingMobileDisplay

This commit is contained in:
Zedifus 2022-06-21 20:23:48 +01:00
commit 70e9290627
24 changed files with 586 additions and 183 deletions

View File

@ -49,6 +49,7 @@ win-prod-build:
paths: paths:
- .venv/ - .venv/
rules: rules:
- if: "$CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH"
- if: $CI_COMMIT_TAG - if: $CI_COMMIT_TAG
environment: environment:
name: production name: production

View File

@ -1,15 +1,18 @@
# Changelog # Changelog
## --- [4.0.4] - 2022/TBD ## --- [4.0.4] - 2022/06/21
### New features ### New features
None - Add shutdown on backup feature ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/373))
- Add detection and dropdown of java versions ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/375))
### Bug fixes ### Bug fixes
None - Backup/Config.json rework for API key hardening ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/369))
- Fix stack on ping result being falsy ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/371))
- Fix sec bug with server creation roles ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/376))
### Tweaks ### Tweaks
Spelling mistake fixed in German lang file ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/370)) - Spelling mistake fixed in German lang file ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/370))
- Fixing Display on server wizard when used on small screens - Backup failure warning (Tab text goes red) ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/373))
- Rework server list on dashboard display for use on small screens - Rework server list on dashboard display for use on small screens ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/372))
<br><br> <br><br>
## --- [4.0.3] - 2022/06/18 ## --- [4.0.3] - 2022/06/18
@ -33,10 +36,6 @@ Spelling mistake fixed in German lang file ([Merge Request](https://gitlab.com/c
### Bug fixes ### Bug fixes
- Fix winreg import pass on non-NT systems ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/344)) - Fix winreg import pass on non-NT systems ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/344))
- Make the WebSocket automatically reconnect. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/345)) - Make the WebSocket automatically reconnect. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/345))
- Fix an error when there are no servers ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/346))
- Use relative paths for the jarfile and logs ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/347))
- Flatten all instances of username creation or editing, usernames should be lower case.
- - ([Merge Request 1](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/342))
- - ([Merge Request 2](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/351)) - - ([Merge Request 2](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/351))
- Add version inheretence & config check ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/353)) - Add version inheretence & config check ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/353))
- Fix support log temp file deletion issue/hang ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/354)) - Fix support log temp file deletion issue/hang ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/354))
@ -49,6 +48,3 @@ Spelling mistake fixed in German lang file ([Merge Request](https://gitlab.com/c
- Remove session.lock warning ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/338)) - Remove session.lock warning ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/338))
- Correct Dutch Spacing Issue ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/340)) - Correct Dutch Spacing Issue ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/340))
- Remove no-else-* pylint exemptions and tidy code. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/342)) - Remove no-else-* pylint exemptions and tidy code. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/342))
- Make unRAID more readable, and flatten path to lower, to fit standard practice. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/337))
- Fix Java Pathing issues on windows ([Commit](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/343/diffs?commit_id=cda2120579083d447db5dbeb5489822880f4cae7))

View File

@ -2,11 +2,11 @@
[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black)
[![Supported Python Versions](https://shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20-blue)](https://www.python.org) [![Supported Python Versions](https://shields.io/badge/python-3.8%20%7C%203.9%20%7C%203.10%20-blue)](https://www.python.org)
[![Version(temp-hardcoded)](https://img.shields.io/badge/release-v4.0.3--beta-orange)](https://gitlab.com/crafty-controller/crafty-4/-/releases) [![Version(temp-hardcoded)](https://img.shields.io/badge/release-v4.0.4--beta-orange)](https://gitlab.com/crafty-controller/crafty-4/-/releases)
[![Code Quality(temp-hardcoded)](https://img.shields.io/badge/code%20quality-10-brightgreen)](https://gitlab.com/crafty-controller/crafty-4) [![Code Quality(temp-hardcoded)](https://img.shields.io/badge/code%20quality-10-brightgreen)](https://gitlab.com/crafty-controller/crafty-4)
[![Build Status](https://gitlab.com/crafty-controller/crafty-4/badges/master/pipeline.svg)](https://gitlab.com/crafty-controller/crafty-4/-/commits/master) [![Build Status](https://gitlab.com/crafty-controller/crafty-4/badges/master/pipeline.svg)](https://gitlab.com/crafty-controller/crafty-4/-/commits/master)
# Crafty Controller 4.0.3-beta # Crafty Controller 4.0.4-beta
> Python based Control Panel for your Minecraft Server > Python based Control Panel for your Minecraft Server
## What is Crafty Controller? ## What is Crafty Controller?

View File

@ -17,6 +17,14 @@ class ManagementController:
def get_latest_hosts_stats(): def get_latest_hosts_stats():
return HelpersManagement.get_latest_hosts_stats() return HelpersManagement.get_latest_hosts_stats()
@staticmethod
def set_crafty_api_key(key):
HelpersManagement.set_secret_api_key(key)
@staticmethod
def get_crafty_api_key():
return HelpersManagement.get_secret_api_key()
# ********************************************************************************** # **********************************************************************************
# Commands Methods # Commands Methods
# ********************************************************************************** # **********************************************************************************
@ -128,9 +136,10 @@ class ManagementController:
max_backups: int = None, max_backups: int = None,
excluded_dirs: list = None, excluded_dirs: list = None,
compress: bool = False, compress: bool = False,
shutdown: bool = False,
): ):
return self.management_helper.set_backup_config( return self.management_helper.set_backup_config(
server_id, backup_path, max_backups, excluded_dirs, compress server_id, backup_path, max_backups, excluded_dirs, compress, shutdown
) )
@staticmethod @staticmethod

View File

@ -5,6 +5,7 @@ import json
import typing as t import typing as t
from app.classes.controllers.roles_controller import RolesController from app.classes.controllers.roles_controller import RolesController
from app.classes.shared.file_helpers import FileHelpers
from app.classes.shared.singleton import Singleton from app.classes.shared.singleton import Singleton
from app.classes.shared.server import ServerInstance from app.classes.shared.server import ServerInstance
@ -28,8 +29,9 @@ logger = logging.getLogger(__name__)
class ServersController(metaclass=Singleton): class ServersController(metaclass=Singleton):
servers_list: ServerInstance servers_list: ServerInstance
def __init__(self, helper, servers_helper, management_helper): def __init__(self, helper, servers_helper, management_helper, file_helper):
self.helper: Helpers = helper self.helper: Helpers = helper
self.file_helper: FileHelpers = file_helper
self.servers_helper: HelperServers = servers_helper self.servers_helper: HelperServers = servers_helper
self.management_helper = management_helper self.management_helper = management_helper
self.servers_list = [] self.servers_list = []
@ -189,6 +191,7 @@ class ServersController(metaclass=Singleton):
self.helper, self.helper,
self.management_helper, self.management_helper,
self.stats, self.stats,
self.file_helper,
), ),
"server_settings": settings.props, "server_settings": settings.props,
} }

View File

@ -218,7 +218,7 @@ class Stats:
return level_total_size return level_total_size
def get_server_players(self, server_id): # pylint: disable=no-self-use def get_server_players(self, server_id):
server = HelperServers.get_server_data_by_id(server_id) server = HelperServers.get_server_data_by_id(server_id)
@ -265,15 +265,24 @@ class Stats:
logger.info( logger.info(
"Unable to read the server icon due to the following error:", exc_info=e "Unable to read the server icon due to the following error:", exc_info=e
) )
if ping_obj:
ping_data = { ping_data = {
"online": online_stats.get("online", 0), "online": online_stats.get("online", 0),
"max": online_stats.get("max", 0), "max": online_stats.get("max", 0),
"players": online_stats.get("players", 0), "players": online_stats.get("players", 0),
"server_description": ping_obj.description, "server_description": ping_obj.description,
"server_version": ping_obj.version, "server_version": ping_obj.version,
"server_icon": server_icon, "server_icon": server_icon,
} }
else:
ping_data = {
"online": online_stats.get("online", 0),
"max": online_stats.get("max", 0),
"players": online_stats.get("players", 0),
"server_description": "",
"server_version": "",
"server_icon": server_icon,
}
return ping_data return ping_data

View File

@ -38,6 +38,16 @@ class AuditLog(BaseModel):
table_name = "audit_log" table_name = "audit_log"
# **********************************************************************************
# Crafty Settings Class
# **********************************************************************************
class CraftySettings(BaseModel):
secret_api_key = CharField(default="")
class Meta:
table_name = "crafty_settings"
# ********************************************************************************** # **********************************************************************************
# Host_Stats Class # Host_Stats Class
# ********************************************************************************** # **********************************************************************************
@ -118,6 +128,7 @@ class Backups(BaseModel):
max_backups = IntegerField() max_backups = IntegerField()
server_id = ForeignKeyField(Servers, backref="backups_server") server_id = ForeignKeyField(Servers, backref="backups_server")
compress = BooleanField(default=False) compress = BooleanField(default=False)
shutdown = BooleanField(default=False)
class Meta: class Meta:
table_name = "backups" table_name = "backups"
@ -231,6 +242,17 @@ class HelpersManagement:
else: else:
return return
@staticmethod
def set_secret_api_key(key):
CraftySettings.insert(secret_api_key=key).execute()
@staticmethod
def get_secret_api_key():
settings = CraftySettings.select(CraftySettings.secret_api_key).where(
CraftySettings.id == 1
)
return settings[0].secret_api_key
# ********************************************************************************** # **********************************************************************************
# Schedules Methods # Schedules Methods
# ********************************************************************************** # **********************************************************************************
@ -330,6 +352,7 @@ class HelpersManagement:
"max_backups": row.max_backups, "max_backups": row.max_backups,
"server_id": row.server_id_id, "server_id": row.server_id_id,
"compress": row.compress, "compress": row.compress,
"shutdown": row.shutdown,
} }
except IndexError: except IndexError:
conf = { conf = {
@ -338,6 +361,7 @@ class HelpersManagement:
"max_backups": 0, "max_backups": 0,
"server_id": server_id, "server_id": server_id,
"compress": False, "compress": False,
"shutdown": False,
} }
return conf return conf
@ -348,6 +372,7 @@ class HelpersManagement:
max_backups: int = None, max_backups: int = None,
excluded_dirs: list = None, excluded_dirs: list = None,
compress: bool = False, compress: bool = False,
shutdown: bool = False,
): ):
logger.debug(f"Updating server {server_id} backup config with {locals()}") logger.debug(f"Updating server {server_id} backup config with {locals()}")
if Backups.select().where(Backups.server_id == server_id).exists(): if Backups.select().where(Backups.server_id == server_id).exists():
@ -359,6 +384,7 @@ class HelpersManagement:
"max_backups": 0, "max_backups": 0,
"server_id": server_id, "server_id": server_id,
"compress": False, "compress": False,
"shutdown": False,
} }
new_row = True new_row = True
if max_backups is not None: if max_backups is not None:
@ -367,6 +393,7 @@ class HelpersManagement:
dirs_to_exclude = ",".join(excluded_dirs) dirs_to_exclude = ",".join(excluded_dirs)
conf["excluded_dirs"] = dirs_to_exclude conf["excluded_dirs"] = dirs_to_exclude
conf["compress"] = compress conf["compress"] = compress
conf["shutdown"] = shutdown
if not new_row: if not new_row:
with self.database.atomic(): with self.database.atomic():
if backup_path is not None: if backup_path is not None:

View File

@ -5,6 +5,7 @@ import jwt
from jwt import PyJWTError from jwt import PyJWTError
from app.classes.models.users import HelperUsers, ApiKeys from app.classes.models.users import HelperUsers, ApiKeys
from app.classes.controllers.management_controller import ManagementController
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -13,11 +14,14 @@ class Authentication:
def __init__(self, helper): def __init__(self, helper):
self.helper = helper self.helper = helper
self.secret = "my secret" self.secret = "my secret"
self.secret = self.helper.get_setting("apikey_secret", None) try:
self.secret = ManagementController.get_crafty_api_key()
if self.secret is None or self.secret == "random": if self.secret == "":
self.secret = self.helper.random_string_generator(64)
ManagementController.set_crafty_api_key(str(self.secret))
except:
self.secret = self.helper.random_string_generator(64) self.secret = self.helper.random_string_generator(64)
self.helper.set_setting("apikey_secret", self.secret) ManagementController.set_crafty_api_key(str(self.secret))
def generate(self, user_id, extra=None): def generate(self, user_id, extra=None):
if extra is None: if extra is None:

View File

@ -2,14 +2,22 @@ import os
import shutil import shutil
import logging import logging
import pathlib import pathlib
import tempfile
import zipfile
from zipfile import ZipFile, ZIP_DEFLATED from zipfile import ZipFile, ZIP_DEFLATED
from app.classes.shared.helpers import Helpers
from app.classes.shared.console import Console
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class FileHelpers: class FileHelpers:
allowed_quotes = ['"', "'", "`"] allowed_quotes = ['"', "'", "`"]
def __init__(self, helper):
self.helper: Helpers = helper
@staticmethod @staticmethod
def del_dirs(path): def del_dirs(path):
path = pathlib.Path(path) path = pathlib.Path(path)
@ -82,7 +90,6 @@ class FileHelpers:
f"Error backing up: {os.path.join(root, file)}!" f"Error backing up: {os.path.join(root, file)}!"
f" - Error was: {e}" f" - Error was: {e}"
) )
return True return True
@staticmethod @staticmethod
@ -113,3 +120,173 @@ class FileHelpers:
) )
return True return True
def make_compressed_backup(
self, path_to_destination, path_to_zip, excluded_dirs, server_id
):
# 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),
}
self.helper.websocket_helper.broadcast_page_params(
"/panel/server_detail",
{"id": str(server_id)},
"backup_status",
results,
)
with ZipFile(path_to_destination, "w", ZIP_DEFLATED) as zip_file:
for root, dirs, files in os.walk(path_to_zip, topdown=True):
for l_dir in dirs:
if str(os.path.join(root, l_dir)).replace("\\", "/") in ex_replace:
dirs.remove(l_dir)
ziproot = path_to_zip
for file in files:
if (
str(os.path.join(root, file)).replace("\\", "/")
not in ex_replace
and file != "crafty.sqlite"
):
try:
logger.info(f"backing up: {os.path.join(root, file)}")
if os.name == "nt":
zip_file.write(
os.path.join(root, file),
os.path.join(root.replace(ziproot, ""), file),
)
else:
zip_file.write(
os.path.join(root, file),
os.path.join(root.replace(ziproot, "/"), file),
)
except Exception as e:
logger.warning(
f"Error backing up: {os.path.join(root, file)}!"
f" - Error was: {e}"
)
total_bytes += os.path.getsize(os.path.join(root, file))
percent = round((total_bytes / dir_bytes) * 100, 2)
results = {
"percent": percent,
"total_files": self.helper.human_readable_file_size(dir_bytes),
}
self.helper.websocket_helper.broadcast_page_params(
"/panel/server_detail",
{"id": str(server_id)},
"backup_status",
results,
)
return True
def make_backup(self, path_to_destination, path_to_zip, excluded_dirs, server_id):
# 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),
}
self.helper.websocket_helper.broadcast_page_params(
"/panel/server_detail",
{"id": str(server_id)},
"backup_status",
results,
)
with ZipFile(path_to_destination, "w") as zip_file:
for root, dirs, files in os.walk(path_to_zip, topdown=True):
for l_dir in dirs:
if str(os.path.join(root, l_dir)).replace("\\", "/") in ex_replace:
dirs.remove(l_dir)
ziproot = path_to_zip
for file in files:
if (
str(os.path.join(root, file)).replace("\\", "/")
not in ex_replace
and file != "crafty.sqlite"
):
try:
logger.info(f"backing up: {os.path.join(root, file)}")
if os.name == "nt":
zip_file.write(
os.path.join(root, file),
os.path.join(root.replace(ziproot, ""), file),
)
else:
zip_file.write(
os.path.join(root, file),
os.path.join(root.replace(ziproot, "/"), file),
)
except Exception as e:
logger.warning(
f"Error backing up: {os.path.join(root, file)}!"
f" - Error was: {e}"
)
total_bytes += os.path.getsize(os.path.join(root, file))
percent = round((total_bytes / dir_bytes) * 100, 2)
results = {
"percent": percent,
"total_files": self.helper.human_readable_file_size(dir_bytes),
}
self.helper.websocket_helper.broadcast_page_params(
"/panel/server_detail",
{"id": str(server_id)},
"backup_status",
results,
)
return True
@staticmethod
def unzip_file(zip_path):
new_dir_list = zip_path.split("/")
new_dir = ""
for i in range(len(new_dir_list) - 1):
if i == 0:
new_dir += new_dir_list[i]
else:
new_dir += "/" + new_dir_list[i]
if Helpers.check_file_perms(zip_path) and os.path.isfile(zip_path):
Helpers.ensure_dir_exists(new_dir)
temp_dir = tempfile.mkdtemp()
try:
with zipfile.ZipFile(zip_path, "r") as zip_ref:
zip_ref.extractall(temp_dir)
for i in enumerate(zip_ref.filelist):
if len(zip_ref.filelist) > 1 or not zip_ref.filelist[
i
].filename.endswith("/"):
break
full_root_path = temp_dir
for item in os.listdir(full_root_path):
if os.path.isdir(os.path.join(full_root_path, item)):
try:
FileHelpers.move_dir(
os.path.join(full_root_path, 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(full_root_path, 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

View File

@ -15,6 +15,8 @@ import html
import zipfile import zipfile
import pathlib import pathlib
import ctypes import ctypes
import subprocess
import itertools
from datetime import datetime from datetime import datetime
from socket import gethostname from socket import gethostname
from contextlib import redirect_stderr, suppress from contextlib import redirect_stderr, suppress
@ -22,7 +24,6 @@ from contextlib import redirect_stderr, suppress
from app.classes.shared.null_writer import NullWriter from app.classes.shared.null_writer import NullWriter
from app.classes.shared.console import Console from app.classes.shared.console import Console
from app.classes.shared.installer import installer from app.classes.shared.installer import installer
from app.classes.shared.file_helpers import FileHelpers
from app.classes.shared.translation import Translation from app.classes.shared.translation import Translation
from app.classes.web.websocket_helper import WebSocketHelper from app.classes.web.websocket_helper import WebSocketHelper
@ -81,6 +82,60 @@ class Helpers:
print(f"Import Error: Unable to load {ex.name} module") print(f"Import Error: Unable to load {ex.name} module")
installer.do_install() installer.do_install()
@staticmethod
def find_java_installs():
# If we're windows return oracle java versions,
# otherwise java vers need to be manual.
if os.name == "nt":
# Adapted from LeeKamentsky >>>
# https://github.com/LeeKamentsky/python-javabridge/blob/master/javabridge/locate.py
jdk_key_paths = (
"SOFTWARE\\JavaSoft\\JDK",
"SOFTWARE\\JavaSoft\\Java Development Kit",
)
java_paths = []
for jdk_key_path in jdk_key_paths:
try:
with suppress(OSError), winreg.OpenKey(
winreg.HKEY_LOCAL_MACHINE, jdk_key_path
) as kjdk:
for i in itertools.count():
version = winreg.EnumKey(kjdk, i)
kjdk_current = winreg.OpenKey(
winreg.HKEY_LOCAL_MACHINE,
jdk_key_path,
)
kjdk_current = winreg.OpenKey(
winreg.HKEY_LOCAL_MACHINE,
jdk_key_path + "\\" + version,
)
kjdk_current_values = dict( # pylint: disable=consider-using-dict-comprehension
[
winreg.EnumValue(kjdk_current, i)[:2]
for i in range(winreg.QueryInfoKey(kjdk_current)[1])
]
)
java_paths.append(kjdk_current_values["JavaHome"])
except OSError as e:
if e.errno == 2:
continue
raise
return java_paths
# If we get here we're linux so we will use 'update-alternatives'
# (If distro does not have update-alternatives then manual input.)
try:
paths = subprocess.check_output(
["/usr/bin/update-alternatives", "--list", "java"], encoding="utf8"
)
if re.match("^(/[^/ ]*)+/?$", paths):
return paths.split("\n")
except Exception as e:
print("Java Detect Error: ", e)
logger.error(f"Java Detect Error: {e}")
@staticmethod @staticmethod
def float_to_string(gbs: float): def float_to_string(gbs: float):
s = str(float(gbs) * 1000).rstrip("0").rstrip(".") s = str(float(gbs) * 1000).rstrip("0").rstrip(".")
@ -441,53 +496,6 @@ class Helpers:
return ctypes.windll.shell32.IsUserAnAdmin() == 1 return ctypes.windll.shell32.IsUserAnAdmin() == 1
return os.geteuid() == 0 return os.geteuid() == 0
@staticmethod
def unzip_file(zip_path):
new_dir_list = zip_path.split("/")
new_dir = ""
for i in range(len(new_dir_list) - 1):
if i == 0:
new_dir += new_dir_list[i]
else:
new_dir += "/" + new_dir_list[i]
if Helpers.check_file_perms(zip_path) and os.path.isfile(zip_path):
Helpers.ensure_dir_exists(new_dir)
temp_dir = tempfile.mkdtemp()
try:
with zipfile.ZipFile(zip_path, "r") as zip_ref:
zip_ref.extractall(temp_dir)
for i in enumerate(zip_ref.filelist):
if len(zip_ref.filelist) > 1 or not zip_ref.filelist[
i
].filename.endswith("/"):
break
full_root_path = temp_dir
for item in os.listdir(full_root_path):
if os.path.isdir(os.path.join(full_root_path, item)):
try:
FileHelpers.move_dir(
os.path.join(full_root_path, 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(full_root_path, 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 ensure_logging_setup(self): def ensure_logging_setup(self):
log_file = os.path.join(os.path.curdir, "logs", "commander.log") log_file = os.path.join(os.path.curdir, "logs", "commander.log")
session_log_file = os.path.join(os.path.curdir, "logs", "session.log") session_log_file = os.path.join(os.path.curdir, "logs", "session.log")
@ -832,7 +840,7 @@ class Helpers:
for item in file_list: for item in file_list:
if os.path.isdir(os.path.join(folder, item)): if os.path.isdir(os.path.join(folder, item)):
dir_list.append(item) dir_list.append(item)
else: elif str(item) != "crafty.sqlite":
unsorted_files.append(item) unsorted_files.append(item)
file_list = sorted(dir_list, key=str.casefold) + sorted( file_list = sorted(dir_list, key=str.casefold) + sorted(
unsorted_files, key=str.casefold unsorted_files, key=str.casefold
@ -863,13 +871,14 @@ class Helpers:
@staticmethod @staticmethod
def generate_dir(folder, output=""): def generate_dir(folder, output=""):
dir_list = [] dir_list = []
unsorted_files = [] unsorted_files = []
file_list = os.listdir(folder) file_list = os.listdir(folder)
for item in file_list: for item in file_list:
if os.path.isdir(os.path.join(folder, item)): if os.path.isdir(os.path.join(folder, item)):
dir_list.append(item) dir_list.append(item)
else: elif str(item) != "crafty.sqlite":
unsorted_files.append(item) unsorted_files.append(item)
file_list = sorted(dir_list, key=str.casefold) + sorted( file_list = sorted(dir_list, key=str.casefold) + sorted(
unsorted_files, key=str.casefold unsorted_files, key=str.casefold
@ -986,14 +995,6 @@ class Helpers:
[parent_path, child_path] [parent_path, child_path]
) )
@staticmethod
def copy_files(source, dest):
if os.path.isfile(source):
FileHelpers.copy_file(source, dest)
logger.info("Copying jar %s to %s", source, dest)
else:
logger.info("Source jar does not exist.")
@staticmethod @staticmethod
def download_file(executable_url, jar_path): def download_file(executable_url, jar_path):
try: try:

View File

@ -34,8 +34,9 @@ logger = logging.getLogger(__name__)
class Controller: class Controller:
def __init__(self, database, helper): def __init__(self, database, helper, file_helper):
self.helper: Helpers = helper self.helper: Helpers = helper
self.file_helper: FileHelpers = file_helper
self.server_jars: ServerJars = ServerJars(helper) self.server_jars: ServerJars = ServerJars(helper)
self.users_helper: HelperUsers = HelperUsers(database, self.helper) self.users_helper: HelperUsers = HelperUsers(database, self.helper)
self.roles_helper: HelperRoles = HelperRoles(database) self.roles_helper: HelperRoles = HelperRoles(database)
@ -53,7 +54,7 @@ class Controller:
) )
self.server_perms: ServerPermsController = ServerPermsController() self.server_perms: ServerPermsController = ServerPermsController()
self.servers: ServersController = ServersController( self.servers: ServersController = ServersController(
self.helper, self.servers_helper, self.management_helper self.helper, self.servers_helper, self.management_helper, self.file_helper
) )
self.users: UsersController = UsersController( self.users: UsersController = UsersController(
self.helper, self.users_helper, self.authentication self.helper, self.users_helper, self.authentication

View File

@ -17,8 +17,6 @@ class DatabaseBuilder:
logger.info("Fresh Install Detected - Creating Default Settings") logger.info("Fresh Install Detected - Creating Default Settings")
Console.info("Fresh Install Detected - Creating Default Settings") Console.info("Fresh Install Detected - Creating Default Settings")
default_data = self.helper.find_default_password() default_data = self.helper.find_default_password()
# Reset this value if the DB has been dumped
self.helper.set_setting("apikey_secret", "random")
username = default_data.get("username", "admin") username = default_data.get("username", "admin")
password = default_data.get("password", "crafty") password = default_data.get("password", "crafty")

View File

@ -9,7 +9,6 @@ import threading
import logging.config import logging.config
import subprocess import subprocess
import html import html
import tempfile
# TZLocal is set as a hidden import on win pipeline # TZLocal is set as a hidden import on win pipeline
from tzlocal import get_localzone from tzlocal import get_localzone
@ -102,12 +101,14 @@ class ServerOutBuf:
class ServerInstance: class ServerInstance:
server_object: Servers server_object: Servers
helper: Helpers helper: Helpers
file_helper: FileHelpers
management_helper: HelpersManagement management_helper: HelpersManagement
stats: Stats stats: Stats
stats_helper: HelperServerStats stats_helper: HelperServerStats
def __init__(self, server_id, helper, management_helper, stats): def __init__(self, server_id, helper, management_helper, stats, file_helper):
self.helper = helper self.helper = helper
self.file_helper = file_helper
self.management_helper = management_helper self.management_helper = management_helper
# holders for our process # holders for our process
self.process = None self.process = None
@ -126,6 +127,7 @@ class ServerInstance:
self.stats = stats self.stats = stats
self.server_object = HelperServers.get_server_obj(self.server_id) self.server_object = HelperServers.get_server_obj(self.server_id)
self.stats_helper = HelperServerStats(self.server_id) self.stats_helper = HelperServerStats(self.server_id)
self.last_backup_failed = False
try: try:
tz = get_localzone() tz = get_localzone()
except ZoneInfoNotFoundError: except ZoneInfoNotFoundError:
@ -846,6 +848,7 @@ class ServerInstance:
"backup_reload", "backup_reload",
{"percent": 0, "total_files": 0}, {"percent": 0, "total_files": 0},
) )
was_server_running = None
logger.info(f"Starting server {self.name} (ID {self.server_id}) backup") logger.info(f"Starting server {self.name} (ID {self.server_id}) backup")
server_users = PermissionsServers.get_server_user_list(self.server_id) server_users = PermissionsServers.get_server_user_list(self.server_id)
for user in server_users: for user in server_users:
@ -858,6 +861,15 @@ class ServerInstance:
) )
time.sleep(3) time.sleep(3)
conf = HelpersManagement.get_backup_config(self.server_id) conf = HelpersManagement.get_backup_config(self.server_id)
if conf["shutdown"]:
logger.info(
"Found shutdown preference. Delaying"
+ "backup start. Shutting down server."
)
if self.check_running():
self.stop_server()
was_server_running = True
self.helper.ensure_dir_exists(self.settings["backup_path"]) self.helper.ensure_dir_exists(self.settings["backup_path"])
try: try:
backup_filename = ( backup_filename = (
@ -869,62 +881,27 @@ class ServerInstance:
f" (ID#{self.server_id}, path={self.server_path}) " f" (ID#{self.server_id}, path={self.server_path}) "
f"at '{backup_filename}'" f"at '{backup_filename}'"
) )
temp_dir = tempfile.mkdtemp()
self.server_scheduler.add_job(
self.backup_status,
"interval",
seconds=1,
id="backup_" + str(self.server_id),
args=[temp_dir + "/", backup_filename + ".zip"],
)
# pylint: disable=unexpected-keyword-arg
try:
FileHelpers.copy_dir(self.server_path, temp_dir, dirs_exist_ok=True)
except shutil.Error as e:
logger.error(f"Failed to fully complete backup due to shutil error {e}")
excluded_dirs = HelpersManagement.get_excluded_backup_dirs(self.server_id) excluded_dirs = HelpersManagement.get_excluded_backup_dirs(self.server_id)
server_dir = Helpers.get_os_understandable_path(self.settings["path"]) server_dir = Helpers.get_os_understandable_path(self.settings["path"])
for my_dir in excluded_dirs:
# Take the full path of the excluded dir and replace the
# server path with the temp path, this is so that we're
# only deleting excluded dirs from the temp path
# and not the server path
excluded_dir = Helpers.get_os_understandable_path(my_dir).replace(
server_dir, Helpers.get_os_understandable_path(temp_dir)
)
# Next, check to see if it is a directory
if os.path.isdir(excluded_dir):
# If it is a directory,
# recursively delete the entire directory from the backup
try:
FileHelpers.del_dirs(excluded_dir)
except FileNotFoundError:
Console.error(
f"Excluded dir {excluded_dir} not found. Moving on..."
)
else:
# If not, just remove the file
try:
os.remove(excluded_dir)
except:
Console.error(
f"Excluded dir {excluded_dir} not found. Moving on..."
)
if conf["compress"]: if conf["compress"]:
logger.debug( logger.debug(
"Found compress backup to be true. Calling compressed archive" "Found compress backup to be true. Calling compressed archive"
) )
FileHelpers.make_compressed_archive( self.file_helper.make_compressed_backup(
Helpers.get_os_understandable_path(backup_filename), temp_dir Helpers.get_os_understandable_path(backup_filename),
server_dir,
excluded_dirs,
self.server_id,
) )
else: else:
logger.debug( logger.debug(
"Found compress backup to be false. Calling NON-compressed archive" "Found compress backup to be false. Calling NON-compressed archive"
) )
FileHelpers.make_archive( self.file_helper.make_backup(
Helpers.get_os_understandable_path(backup_filename), temp_dir Helpers.get_os_understandable_path(backup_filename),
server_dir,
excluded_dirs,
self.server_id,
) )
while ( while (
@ -939,7 +916,6 @@ class ServerInstance:
self.is_backingup = False self.is_backingup = False
logger.info(f"Backup of server: {self.name} completed") logger.info(f"Backup of server: {self.name} completed")
self.server_scheduler.remove_job("backup_" + str(self.server_id))
results = {"percent": 100, "total_files": 0, "current_file": 0} results = {"percent": 100, "total_files": 0, "current_file": 0}
if len(self.helper.websocket_helper.clients) > 0: if len(self.helper.websocket_helper.clients) > 0:
self.helper.websocket_helper.broadcast_page_params( self.helper.websocket_helper.broadcast_page_params(
@ -959,12 +935,17 @@ class ServerInstance:
HelperUsers.get_user_lang_by_id(user), HelperUsers.get_user_lang_by_id(user),
).format(self.name), ).format(self.name),
) )
if was_server_running:
logger.info(
"Backup complete. User had shutdown preference. Starting server."
)
self.start_server(HelperUsers.get_user_id_by_name("system"))
time.sleep(3) time.sleep(3)
self.last_backup_failed = False
except: except:
logger.exception( logger.exception(
f"Failed to create backup of server {self.name} (ID {self.server_id})" f"Failed to create backup of server {self.name} (ID {self.server_id})"
) )
self.server_scheduler.remove_job("backup_" + str(self.server_id))
results = {"percent": 100, "total_files": 0, "current_file": 0} results = {"percent": 100, "total_files": 0, "current_file": 0}
if len(self.helper.websocket_helper.clients) > 0: if len(self.helper.websocket_helper.clients) > 0:
self.helper.websocket_helper.broadcast_page_params( self.helper.websocket_helper.broadcast_page_params(
@ -974,8 +955,12 @@ class ServerInstance:
results, results,
) )
self.is_backingup = False self.is_backingup = False
finally: if was_server_running:
FileHelpers.del_dirs(temp_dir) logger.info(
"Backup complete. User had shutdown preference. Starting server."
)
self.start_server(HelperUsers.get_user_id_by_name("system"))
self.last_backup_failed = True
def backup_status(self, source_path, dest_path): def backup_status(self, source_path, dest_path):
results = Helpers.calc_percent(source_path, dest_path) results = Helpers.calc_percent(source_path, dest_path)
@ -988,6 +973,9 @@ class ServerInstance:
results, results,
) )
def last_backup_status(self):
return self.last_backup_failed
def send_backup_status(self): def send_backup_status(self):
try: try:
return self.backup_stats return self.backup_stats
@ -1093,7 +1081,7 @@ class ServerInstance:
) )
# copies to backup dir # copies to backup dir
Helpers.copy_files(current_executable, backup_executable) FileHelpers.copy_file(current_executable, backup_executable)
# boolean returns true for false for success # boolean returns true for false for success
downloaded = Helpers.download_file( downloaded = Helpers.download_file(

View File

@ -220,7 +220,7 @@ class FileHandler(BaseHandler):
path = Helpers.get_os_understandable_path(self.get_argument("path", None)) path = Helpers.get_os_understandable_path(self.get_argument("path", None))
if Helpers.is_os_windows(): if Helpers.is_os_windows():
path = Helpers.wtol_path(path) path = Helpers.wtol_path(path)
Helpers.unzip_file(path) FileHelpers.unzip_file(path)
self.redirect(f"/panel/server_detail?id={server_id}&subpage=files") self.redirect(f"/panel/server_detail?id={server_id}&subpage=files")
return return

View File

@ -6,6 +6,7 @@ import typing as t
import json import json
import logging import logging
import threading import threading
import shlex
import bleach import bleach
import libgravatar import libgravatar
import requests import requests
@ -497,6 +498,10 @@ class PanelHandler(BaseHandler):
if server_id is None: if server_id is None:
return return
server_obj = self.controller.servers.get_server_instance_by_id(server_id)
page_data["backup_failed"] = server_obj.last_backup_status()
server_obj = None
valid_subpages = [ valid_subpages = [
"term", "term",
"logs", "logs",
@ -627,6 +632,18 @@ class PanelHandler(BaseHandler):
"/panel/error?error=Unauthorized access Server Config" "/panel/error?error=Unauthorized access Server Config"
) )
return return
page_data["java_versions"] = Helpers.find_java_installs()
server_obj: Servers = self.controller.servers.get_server_obj(server_id)
page_java = []
page_data["java_versions"].append("java")
for version in page_data["java_versions"]:
if os.name == "nt":
page_java.append(version)
else:
if len(version) > 0:
page_java.append(version)
page_data["java_versions"] = page_java
if subpage == "files": if subpage == "files":
if ( if (
@ -1342,6 +1359,8 @@ class PanelHandler(BaseHandler):
if Helpers.is_os_windows(): if Helpers.is_os_windows():
log_path.replace(" ", "^ ") log_path.replace(" ", "^ ")
log_path = Helpers.wtol_path(log_path) log_path = Helpers.wtol_path(log_path)
if not self.helper.validate_traversal(server_obj.path, log_path):
log_path = ""
executable = self.get_argument("executable", None) executable = self.get_argument("executable", None)
execution_command = self.get_argument("execution_command", None) execution_command = self.get_argument("execution_command", None)
server_ip = self.get_argument("server_ip", None) server_ip = self.get_argument("server_ip", None)
@ -1355,11 +1374,46 @@ class PanelHandler(BaseHandler):
auto_start = int(float(self.get_argument("auto_start", "0"))) auto_start = int(float(self.get_argument("auto_start", "0")))
crash_detection = int(float(self.get_argument("crash_detection", "0"))) crash_detection = int(float(self.get_argument("crash_detection", "0")))
logs_delete_after = int(float(self.get_argument("logs_delete_after", "0"))) logs_delete_after = int(float(self.get_argument("logs_delete_after", "0")))
java_selection = self.get_argument("java_selection", None)
# subpage = self.get_argument('subpage', None) # subpage = self.get_argument('subpage', None)
server_id = self.check_server_id() server_id = self.check_server_id()
if server_id is None: if server_id is None:
return return
if java_selection:
try:
execution_list = shlex.split(execution_command)
except ValueError:
self.redirect(
"/panel/error?error=Invalid execution command. Java path"
" must be surrounded by quotes."
" (Are you missing a closing quote?)"
)
if not any(
java_selection in path for path in Helpers.find_java_installs()
):
self.redirect(
"/panel/error?error=Attack attempted."
+ " A copy of this report is being sent to server owner."
)
self.controller.management.add_to_audit_log_raw(
exec_user["username"],
exec_user["user_id"],
server_id,
f"Attempted to send bad java path for {server_id}."
+ " Possible attack. Act accordingly.",
self.get_remote_ip(),
)
if java_selection != "java":
if self.helper.is_os_windows():
execution_list[0] = '"' + java_selection + '/bin/java"'
else:
execution_list[0] = '"' + java_selection + '"'
else:
execution_list[0] = "java"
execution_command = ""
for item in execution_list:
execution_command += item + " "
server_obj: Servers = self.controller.servers.get_server_obj(server_id) server_obj: Servers = self.controller.servers.get_server_obj(server_id)
stale_executable = server_obj.executable stale_executable = server_obj.executable
@ -1389,7 +1443,7 @@ class PanelHandler(BaseHandler):
server_obj.path = server_obj.path server_obj.path = server_obj.path
server_obj.log_path = server_obj.log_path server_obj.log_path = server_obj.log_path
server_obj.executable = server_obj.executable server_obj.executable = server_obj.executable
server_obj.execution_command = server_obj.execution_command server_obj.execution_command = execution_command
server_obj.server_ip = server_obj.server_ip server_obj.server_ip = server_obj.server_ip
server_obj.server_port = server_obj.server_port server_obj.server_port = server_obj.server_port
server_obj.executable_update_url = server_obj.executable_update_url server_obj.executable_update_url = server_obj.executable_update_url
@ -1433,6 +1487,7 @@ class PanelHandler(BaseHandler):
server_obj = self.controller.servers.get_server_obj(server_id) server_obj = self.controller.servers.get_server_obj(server_id)
compress = self.get_argument("compress", False) compress = self.get_argument("compress", False)
shutdown = self.get_argument("shutdown", False)
check_changed = self.get_argument("changed") check_changed = self.get_argument("changed")
if str(check_changed) == str(1): if str(check_changed) == str(1):
checked = self.get_body_arguments("root_path") checked = self.get_body_arguments("root_path")
@ -1455,6 +1510,7 @@ class PanelHandler(BaseHandler):
max_backups=max_backups, max_backups=max_backups,
excluded_dirs=checked, excluded_dirs=checked,
compress=bool(compress), compress=bool(compress),
shutdown=bool(shutdown),
) )
self.controller.management.add_to_audit_log( self.controller.management.add_to_audit_log(
@ -1941,7 +1997,10 @@ class PanelHandler(BaseHandler):
self.redirect("/panel/error?error=Invalid Key ID") self.redirect("/panel/error?error=Invalid Key ID")
return return
if key.user_id != exec_user["user_id"]: if (
str(key.user_id) != str(exec_user["user_id"])
and not exec_user["superuser"]
):
self.redirect( self.redirect(
"/panel/error?error=You are not authorized to access this key." "/panel/error?error=You are not authorized to access this key."
) )

View File

@ -17,6 +17,15 @@ logger = logging.getLogger(__name__)
class ServerHandler(BaseHandler): class ServerHandler(BaseHandler):
def get_user_roles(self):
user_roles = {}
for user_id in self.controller.users.get_all_user_ids():
user_roles_list = self.controller.users.get_user_roles_names(user_id)
# user_servers =
# self.controller.servers.get_authorized_servers(user.user_id)
user_roles[user_id] = user_roles_list
return user_roles
@tornado.web.authenticated @tornado.web.authenticated
def get(self, page): def get(self, page):
( (
@ -271,11 +280,19 @@ class ServerHandler(BaseHandler):
) )
if page == "step1": if page == "step1":
if not superuser and not self.controller.crafty_perms.can_create_server(
exec_user["user_id"]
):
self.redirect(
"/panel/error?error=Unauthorized access: "
"not a server creator or server limit reached"
)
return
if not superuser: if not superuser:
user_roles = self.controller.roles.get_all_roles() user_roles = self.controller.roles.get_all_roles()
else: else:
user_roles = self.controller.roles.get_all_roles() user_roles = self.get_user_roles()
server = bleach.clean(self.get_argument("server", "")) server = bleach.clean(self.get_argument("server", ""))
server_name = bleach.clean(self.get_argument("server_name", "")) server_name = bleach.clean(self.get_argument("server_name", ""))
min_mem = bleach.clean(self.get_argument("min_memory", "")) min_mem = bleach.clean(self.get_argument("min_memory", ""))
@ -396,6 +413,14 @@ class ServerHandler(BaseHandler):
self.redirect("/panel/dashboard") self.redirect("/panel/dashboard")
if page == "bedrock_step1": if page == "bedrock_step1":
if not superuser and not self.controller.crafty_perms.can_create_server(
exec_user["user_id"]
):
self.redirect(
"/panel/error?error=Unauthorized access: "
"not a server creator or server limit reached"
)
return
if not superuser: if not superuser:
user_roles = self.controller.roles.get_all_roles() user_roles = self.controller.roles.get_all_roles()
else: else:

View File

@ -1,6 +1,6 @@
{ {
"major": 4, "major": 4,
"minor": 0, "minor": 0,
"sub": 3, "sub": 4,
"meta": "beta" "meta": "beta"
} }

View File

@ -19,11 +19,18 @@
</li> </li>
{% end %} {% end %}
{% if data['permissions']['Backup'] in data['user_permissions'] %} {% if data['permissions']['Backup'] in data['user_permissions'] %}
{% if data['backup_failed'] %}
<li class="nav-item term-nav-item">
<a style="color: red !important;" class="nav-link {% if data['active_link'] == 'backup' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=backup" role="tab" aria-selected="false">
<i class="fas fa-save"></i>{{ translate('serverDetails', 'backup', data['lang']) }}&nbsp; <i class="fas fa-exclamation-triangle"> </i></a>
</li>
{% else %}
<li class="nav-item term-nav-item"> <li class="nav-item term-nav-item">
<a class="nav-link {% if data['active_link'] == 'backup' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=backup" role="tab" aria-selected="false"> <a class="nav-link {% if data['active_link'] == 'backup' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=backup" role="tab" aria-selected="false">
<i class="fas fa-save"></i>{{ translate('serverDetails', 'backup', data['lang']) }}</a> <i class="fas fa-save"></i>{{ translate('serverDetails', 'backup', data['lang']) }}</a>
</li> </li>
{% end %} {% end %}
{% end %}
{% if data['permissions']['Files'] in data['user_permissions'] %} {% if data['permissions']['Files'] in data['user_permissions'] %}
<li class="nav-item term-nav-item"> <li class="nav-item term-nav-item">
<a class="nav-link {% if data['active_link'] == 'files' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=files" role="tab" aria-selected="false"> <a class="nav-link {% if data['active_link'] == 'files' %}active{% end %}" href="/panel/server_detail?id={{ data['server_stats']['server_id']['server_id'] }}&subpage=files" role="tab" aria-selected="false">

View File

@ -14,7 +14,8 @@
<div class="col-12"> <div class="col-12">
<div class="page-header"> <div class="page-header">
<h4 class="page-title"> <h4 class="page-title">
{{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{ data['server_stats']['server_id']['server_name'] }} {{ translate('serverDetails', 'serverDetails', data['lang']) }} - {{
data['server_stats']['server_id']['server_name'] }}
<br /> <br />
<small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small> <small>UUID: {{ data['server_stats']['server_id']['server_uuid'] }}</small>
</h4> </h4>
@ -51,55 +52,85 @@
{% if data['backing_up'] %} {% if data['backing_up'] %}
<div class="progress" style="height: 15px;"> <div class="progress" style="height: 15px;">
<div class="progress-bar progress-bar-striped progress-bar-animated" id="backup_progress_bar" role="progressbar" style="width:{{data['backup_stats']['percent']}}%;" aria-valuenow="{{data['backup_stats']['percent']}}" aria-valuemin="0" aria-valuemax="100">{{ data['backup_stats']['percent'] }}%</div> <div class="progress-bar progress-bar-striped progress-bar-animated" id="backup_progress_bar"
role="progressbar" style="width:{{data['backup_stats']['percent']}}%;"
aria-valuenow="{{data['backup_stats']['percent']}}" aria-valuemin="0" aria-valuemax="100">{{
data['backup_stats']['percent'] }}%</div>
</div> </div>
<p>Backing up <span id="total_files">{{data['backup_stats']['total_files']}}</span> Files</p> <p>Backing up <i class="fas fa-spin fa-spinner"></i> <span
id="total_files">{{data['server_stats']['world_size']}}</span></p>
{% end %} {% end %}
<br> <br>
{% if not data['backing_up'] %} {% if not data['backing_up'] %}
<div id="backup_button" class="form-group"> <div id="backup_button" class="form-group">
<button class="btn btn-primary" id="backup_now_button">{{ translate('serverBackups', 'backupNow', data['lang']) }}</button> <button class="btn btn-primary" id="backup_now_button">{{ translate('serverBackups', 'backupNow',
data['lang']) }}</button>
</div> </div>
{% end %} {% end %}
<div class="form-group"> <div class="form-group">
{% if data['super_user'] %} {% if data['super_user'] %}
<label for="server_name">{{ translate('serverBackups', 'storageLocation', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverBackups', 'storageLocationDesc', data['lang']) }}</small> </label> <label for="server_name">{{ translate('serverBackups', 'storageLocation', data['lang']) }} <small
<input type="text" class="form-control" name="backup_path" id="backup_path" value="{{ data['server_stats']['server_id']['backup_path'] }}" placeholder="{{ translate('serverBackups', 'storageLocation', data['lang']) }}"> class="text-muted ml-1"> - {{ translate('serverBackups', 'storageLocationDesc', data['lang'])
}}</small> </label>
<input type="text" class="form-control" name="backup_path" id="backup_path"
value="{{ data['server_stats']['server_id']['backup_path'] }}"
placeholder="{{ translate('serverBackups', 'storageLocation', data['lang']) }}">
{% end %} {% end %}
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="server_path">{{ translate('serverBackups', 'maxBackups', data['lang']) }} <small class="text-muted ml-1"> - {{ translate('serverBackups', 'maxBackupsDesc', data['lang']) }}</small> </label> <label for="server_path">{{ translate('serverBackups', 'maxBackups', data['lang']) }} <small
<input type="text" class="form-control" name="max_backups" id="max_backups" value="{{ data['backup_config']['max_backups'] }}" placeholder="{{ translate('serverBackups', 'maxBackups', data['lang']) }}"> class="text-muted ml-1"> - {{ translate('serverBackups', 'maxBackupsDesc', data['lang'])
}}</small> </label>
<input type="text" class="form-control" name="max_backups" id="max_backups"
value="{{ data['backup_config']['max_backups'] }}"
placeholder="{{ translate('serverBackups', 'maxBackups', data['lang']) }}">
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="compress" class="form-check-label ml-4 mb-4"></label> <label for="compress" class="form-check-label ml-4 mb-4"></label>
{% if data['backup_config']['compress'] %} {% if data['backup_config']['compress'] %}
<input type="checkbox" class="form-check-input" id="compress" name="compress" <input type="checkbox" class="form-check-input" id="compress" name="compress" checked=""
checked="" value="True">{{ translate('serverBackups', 'compress', data['lang']) }} value="True">{{ translate('serverBackups', 'compress', data['lang']) }}
{% else %} {% else %}
<input type="checkbox" class="form-check-input" id="compress" name="compress" <input type="checkbox" class="form-check-input" id="compress" name="compress" value="True">{{
value="True">{{ translate('serverBackups', 'compress', data['lang']) }} translate('serverBackups', 'compress', data['lang']) }}
{% end %} {% end %}
</div> </div>
<div class="form-group"> <div class="form-group">
<label for="server">{{ translate('serverBackups', 'exclusionsTitle', data['lang']) }} <small> - {{ translate('serverBackups', 'excludedChoose', data['lang']) }}</small></label> <label for="shutdown" class="form-check-label ml-4 mb-4"></label>
<br> {% if data['backup_config']['shutdown'] %}
<button class="btn btn-primary mr-2" id="root_files_button" data-server_path="{{ data['server_stats']['server_id']['path']}}" type="button">{{ translate('serverBackups', 'clickExclude', data['lang']) }}</button> <input type="checkbox" class="form-check-input" id="shutdown" name="shutdown" checked=""
value="True">{{ translate('serverBackups', 'shutdown', data['lang']) }}
{% else %}
<input type="checkbox" class="form-check-input" id="shutdown" name="shutdown" value="True">{{
translate('serverBackups', 'shutdown', data['lang']) }}
{% end %}
</div> </div>
<input type="number" class="form-control" name="changed" id="changed" value="0" style="visibility: hidden;"></input> <div class="form-group">
<div class="modal fade" id="dir_select" tabindex="-1" role="dialog" aria-labelledby="dir_select" aria-hidden="true"> <label for="server">{{ translate('serverBackups', 'exclusionsTitle', data['lang']) }} <small> - {{
translate('serverBackups', 'excludedChoose', data['lang']) }}</small></label>
<br>
<button class="btn btn-primary mr-2" id="root_files_button"
data-server_path="{{ data['server_stats']['server_id']['path']}}" type="button">{{
translate('serverBackups', 'clickExclude', data['lang']) }}</button>
</div>
<input type="number" class="form-control" name="changed" id="changed" value="0"
style="visibility: hidden;"></input>
<div class="modal fade" id="dir_select" tabindex="-1" role="dialog" aria-labelledby="dir_select"
aria-hidden="true">
<div class="modal-dialog" role="document"> <div class="modal-dialog" role="document">
<div class="modal-content"> <div class="modal-content">
<div class="modal-header"> <div class="modal-header">
<h5 class="modal-title" id="exampleModalLongTitle">{{ translate('serverBackups', 'excludedChoose', data['lang']) }}</h5> <h5 class="modal-title" id="exampleModalLongTitle">{{ translate('serverBackups',
'excludedChoose', data['lang']) }}</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close"> <button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">&times;</span> <span aria-hidden="true">&times;</span>
</button> </button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<div class="tree-ctx-item" id="main-tree-div" data-path="" style="overflow: scroll; max-height:75%;"> <div class="tree-ctx-item" id="main-tree-div" data-path=""
style="overflow: scroll; max-height:75%;">
<input type="checkbox" id="main-tree-input" name="root_path" value="" disabled> <input type="checkbox" id="main-tree-input" name="root_path" value="" disabled>
<span id="main-tree" class="files-tree-title tree-caret-down root-dir" data-path=""> <span id="main-tree" class="files-tree-title tree-caret-down root-dir" data-path="">
<i class="far fa-folder"></i> <i class="far fa-folder"></i>
@ -110,15 +141,19 @@
</div> </div>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
<button type="button" id="modal-cancel" class="btn btn-secondary" data-dismiss="modal">{{ translate('serverBackups', 'cancel', data['lang']) }}</button> <button type="button" id="modal-cancel" class="btn btn-secondary" data-dismiss="modal">{{
<button type="button" id="modal-okay" data-dismiss="modal" class="btn btn-primary">{{ translate('serverWizard', 'save', data['lang']) }}</button> translate('serverBackups', 'cancel', data['lang']) }}</button>
<button type="button" id="modal-okay" data-dismiss="modal" class="btn btn-primary">{{
translate('serverWizard', 'save', data['lang']) }}</button>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<button type="submit" class="btn btn-success mr-2">{{ translate('serverBackups', 'save', data['lang']) }}</button> <button type="submit" class="btn btn-success mr-2">{{ translate('serverBackups', 'save', data['lang'])
<button type="reset" class="btn btn-light">{{ translate('serverBackups', 'cancel', data['lang']) }}</button> }}</button>
<button type="reset" class="btn btn-light">{{ translate('serverBackups', 'cancel', data['lang'])
}}</button>
</form> </form>
</div> </div>
@ -138,13 +173,15 @@
{% for backup in data['backup_list'] %} {% for backup in data['backup_list'] %}
<tr> <tr>
<td> <td>
<a href="/panel/download_backup?file={{ backup['path'] }}&id={{ data['server_stats']['server_id']['server_id'] }}" class="btn btn-primary"> <a href="/panel/download_backup?file={{ backup['path'] }}&id={{ data['server_stats']['server_id']['server_id'] }}"
class="btn btn-primary">
<i class="fas fa-download" aria-hidden="true"></i> <i class="fas fa-download" aria-hidden="true"></i>
{{ translate('serverBackups', 'download', data['lang']) }} {{ translate('serverBackups', 'download', data['lang']) }}
</a> </a>
<br> <br>
<br> <br>
<button data-file="{{ backup['path'] }}" data-backup_path="{{ data['backup_path'] }}" class="btn btn-danger del_button"> <button data-file="{{ backup['path'] }}" data-backup_path="{{ data['backup_path'] }}"
class="btn btn-danger del_button">
<i class="fas fa-trash" aria-hidden="true"></i> <i class="fas fa-trash" aria-hidden="true"></i>
{{ translate('serverBackups', 'delete', data['lang']) }} {{ translate('serverBackups', 'delete', data['lang']) }}
</button> </button>
@ -168,7 +205,8 @@
<br> <br>
<br> <br>
<div class="card-header header-sm d-flex justify-content-between align-items-center"> <div class="card-header header-sm d-flex justify-content-between align-items-center">
<h4 class="card-title"><i class="fas fa-server"></i> {{ translate('serverBackups', 'excludedBackups', data['lang']) }} <small class="text-muted ml-1"></small> </h4> <h4 class="card-title"><i class="fas fa-server"></i> {{ translate('serverBackups', 'excludedBackups',
data['lang']) }} <small class="text-muted ml-1"></small> </h4>
</div> </div>
<br> <br>
<ul> <ul>

View File

@ -36,7 +36,7 @@
<div class="row"> <div class="row">
<div class="col-md-6 col-sm-12"> <div class="col-md-6 col-sm-12">
<form class="forms-sample" method="post" action="/panel/server_detail"> <form class="forms-sample" method="post" id="config_form" action="/panel/server_detail">
{% raw xsrf_form_html() %} {% raw xsrf_form_html() %}
<input type="hidden" name="id" value="{{ data['server_stats']['server_id']['server_id'] }}"> <input type="hidden" name="id" value="{{ data['server_stats']['server_id']['server_id'] }}">
<input type="hidden" name="subpage" value="config"> <input type="hidden" name="subpage" value="config">
@ -50,8 +50,8 @@
placeholder="{{ translate('serverConfig', 'serverName', data['lang']) }}" required> placeholder="{{ translate('serverConfig', 'serverName', data['lang']) }}" required>
</div> </div>
{% if data['super_user'] %}
<div class="form-group"> <div class="form-group">
{% if data['super_user'] %}
<label for="server_path">{{ translate('serverConfig', 'serverPath', data['lang']) }} <small <label for="server_path">{{ translate('serverConfig', 'serverPath', data['lang']) }} <small
class="text-muted ml-1"> - {{ translate('serverConfig', 'serverPathDesc', data['lang']) }}</small> class="text-muted ml-1"> - {{ translate('serverConfig', 'serverPathDesc', data['lang']) }}</small>
</label> </label>
@ -78,7 +78,24 @@
value="{{ data['server_stats']['server_id']['executable'] }}" value="{{ data['server_stats']['server_id']['executable'] }}"
placeholder="{{ translate('serverConfig', 'serverExecutable', data['lang']) }}" required> placeholder="{{ translate('serverConfig', 'serverExecutable', data['lang']) }}" required>
</div> </div>
{% end %}
{% if data['server_stats']['server_type'] == "minecraft-java" %}
<div class="form-group">
<label for="java_selection">{{ translate('serverConfig', 'javaVersion', data['lang']) }}
<small class="text-muted ml-1">{{ translate('serverConfig', 'javaVersionDesc', data['lang']) }}</small>
</label>
<select class="form-select form-control form-control-lg select-css" id="java_selection"
name="java_selection" form="config_form">
<option value="">{{ translate('serverConfig',
'javaNoChange', data['lang'])}}</option>
{% for path in data['java_versions'] %}
<option value="{{path}}">{{path}}</option>
{% end %}
</select>
</div>
{% end %}
{% if data['super_user'] %}
<div class="form-group"> <div class="form-group">
<label for="execution_command">{{ translate('serverConfig', 'serverExecutionCommand', data['lang']) }} <label for="execution_command">{{ translate('serverConfig', 'serverExecutionCommand', data['lang']) }}
<small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverExecutionCommandDesc', <small class="text-muted ml-1"> - {{ translate('serverConfig', 'serverExecutionCommandDesc',
@ -86,8 +103,14 @@
<input type="text" class="form-control" name="execution_command" id="execution_command" <input type="text" class="form-control" name="execution_command" id="execution_command"
value="{{ data['server_stats']['server_id']['execution_command'] }}" value="{{ data['server_stats']['server_id']['execution_command'] }}"
placeholder="{{ translate('serverConfig', 'serverExecutionCommand', data['lang']) }}" required> placeholder="{{ translate('serverConfig', 'serverExecutionCommand', data['lang']) }}" required>
{% end %}
</div> </div>
{% else %}
<label for="execution_command">{{ translate('serverConfig', 'serverExecutionCommand', data['lang']) }}
<div class="card-header header-sm d-flex justify-content-between align-items-center">
<span style="color: gray;">{{ data['server_stats']['server_id']['execution_command'] }}</span> 🔒
</div>
<br>
{% end %}
<div class="form-group"> <div class="form-group">
<label for="stop_command">{{ translate('serverConfig', 'serverStopCommand', data['lang']) }} <small <label for="stop_command">{{ translate('serverConfig', 'serverStopCommand', data['lang']) }} <small

View File

@ -0,0 +1,16 @@
import peewee
import datetime
def migrate(migrator, db):
class CraftySettings(peewee.Model):
secret_api_key = peewee.CharField(default="")
class Meta:
table_name = "crafty_settings"
migrator.create_table(CraftySettings)
def rollback(migrator, db):
migrator.drop_table("crafty_settings")

View File

@ -0,0 +1,16 @@
# Generated by database migrator
import peewee
def migrate(migrator, database, **kwargs):
migrator.add_columns("backups", shutdown=peewee.BooleanField(default=False))
"""
Write your migrations here.
"""
def rollback(migrator, database, **kwargs):
migrator.drop_columns("backups", ["shutdown"])
"""
Write your rollback migrations here.
"""

View File

@ -271,7 +271,8 @@
"save": "Save", "save": "Save",
"size": "Size", "size": "Size",
"storageLocation": "Storage Location", "storageLocation": "Storage Location",
"storageLocationDesc": "Where do you want to store backups?" "storageLocationDesc": "Where do you want to store backups?",
"shutdown": "Shutdown server for duration of backup"
}, },
"serverConfig": { "serverConfig": {
"bePatientDelete": "Please be patient while we remove your server from the Crafty panel. This screen will close in a few moments.", "bePatientDelete": "Please be patient while we remove your server from the Crafty panel. This screen will close in a few moments.",
@ -300,6 +301,9 @@
"serverCrashDetection": "Server Crash Detection", "serverCrashDetection": "Server Crash Detection",
"serverExecutable": "Server Executable", "serverExecutable": "Server Executable",
"serverExecutableDesc": "The server's executable file", "serverExecutableDesc": "The server's executable file",
"javaVersion": "Override current Java Version",
"javaVersionDesc": "If you're going to override java. Make sure your current java path in 'execution command' is wrapped in quotes (default 'java' variable excluded)",
"javaNoChange": "Do Not Override",
"serverExecutionCommand": "Server Execution Command", "serverExecutionCommand": "Server Execution Command",
"serverExecutionCommandDesc": "What will be launched in a hidden terminal", "serverExecutionCommandDesc": "What will be launched in a hidden terminal",
"serverIP": "Server IP", "serverIP": "Server IP",

View File

@ -7,6 +7,7 @@ import argparse
import logging.config import logging.config
import signal import signal
import peewee import peewee
from app.classes.shared.file_helpers import FileHelpers
from app.classes.shared.import3 import Import3 from app.classes.shared.import3 import Import3
from app.classes.shared.console import Console from app.classes.shared.console import Console
@ -132,9 +133,9 @@ if __name__ == "__main__":
installer.default_settings() installer.default_settings()
else: else:
Console.debug("Existing install detected") Console.debug("Existing install detected")
file_helper = FileHelpers(helper)
# now the tables are created, we can load the tasks_manager and server controller # now the tables are created, we can load the tasks_manager and server controller
controller = Controller(database, helper) controller = Controller(database, helper, file_helper)
import3 = Import3(helper, controller) import3 = Import3(helper, controller)
tasks_manager = TasksManager(helper, controller) tasks_manager = TasksManager(helper, controller)
tasks_manager.start_webserver() tasks_manager.start_webserver()