diff --git a/CHANGELOG.md b/CHANGELOG.md
index 56b9730d..4b2417a0 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,24 @@
# Changelog
+## --- [4.0.20] - 2022/01/29
+### New features
+- Add option to run command before backup. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/536))
+- Make Config.json editable from panel. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/532))
+- Managed config.json refector (See MR for details). ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/485))
+### Bug fixes
+- Fix local java server imports. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/529))
+- Fix Schedule Restore | Add Backup Config Preservation. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/533))
+- Rework `/public` Route. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/538))
+### Tweaks
+- Hide stats DB directory from files tree. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/530))
+- Make it so file tree doesn't reload on upload/delete. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/541))
+- Add upload completed feedback to file upload. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/541))
+- Added further login screen customisation settings. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/531))
+- Set backup filename to use same time as schedule. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/534))
+- Move Schedules to from DB to Queue Datatype. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/535))
+- Move raknet icon failure to a debug log. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/537))
+- Add Default redirection to Dashboard if the user is connected. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/540))
+
+
## --- [4.0.19] - 2022/01/07
### Bug fixes
- Fix port tooltip not showing on dash while server online. ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/503))
diff --git a/README.md b/README.md
index f9a19ae1..059df365 100644
--- a/README.md
+++ b/README.md
@@ -1,5 +1,5 @@
[![Crafty Logo](app/frontend/static/assets/images/logo_long.svg)](https://craftycontrol.com)
-# Crafty Controller 4.0.19
+# Crafty Controller 4.0.20
> Python based Control Panel for your Minecraft Server
## What is Crafty Controller?
diff --git a/app/classes/controllers/management_controller.py b/app/classes/controllers/management_controller.py
index 47860fe1..2811dce4 100644
--- a/app/classes/controllers/management_controller.py
+++ b/app/classes/controllers/management_controller.py
@@ -1,4 +1,5 @@
import logging
+import queue
from app.classes.models.management import HelpersManagement
from app.classes.models.servers import HelperServers
@@ -9,6 +10,26 @@ logger = logging.getLogger(__name__)
class ManagementController:
def __init__(self, management_helper):
self.management_helper = management_helper
+ self.command_queue = queue.Queue()
+
+ # **********************************************************************************
+ # Config Methods
+ # **********************************************************************************
+ @staticmethod
+ def set_login_image(path):
+ HelpersManagement.set_login_image(path)
+
+ @staticmethod
+ def get_login_image():
+ return HelpersManagement.get_login_image()
+
+ @staticmethod
+ def set_login_opacity(opacity):
+ return HelpersManagement.set_login_opacity(opacity)
+
+ @staticmethod
+ def get_login_opacity():
+ return HelpersManagement.get_login_opacity()
# **********************************************************************************
# Host_Stats Methods
@@ -25,12 +46,17 @@ class ManagementController:
def get_crafty_api_key():
return HelpersManagement.get_secret_api_key()
+ @staticmethod
+ def set_cookie_secret(key):
+ HelpersManagement.set_cookie_secret(key)
+
+ @staticmethod
+ def add_crafty_row():
+ HelpersManagement.create_crafty_row()
+
# **********************************************************************************
# Commands Methods
# **********************************************************************************
- @staticmethod
- def get_unactioned_commands():
- return HelpersManagement.get_unactioned_commands()
def send_command(self, user_id, server_id, remote_ip, command):
server_name = HelperServers.get_server_friendly_name(server_id)
@@ -42,11 +68,12 @@ class ManagementController:
server_id,
remote_ip,
)
- HelpersManagement.add_command(server_id, user_id, remote_ip, command)
+ self.queue_command(
+ {"server_id": server_id, "user_id": user_id, "command": command}
+ )
- @staticmethod
- def mark_command_complete(command_id=None):
- return HelpersManagement.mark_command_complete(command_id)
+ def queue_command(self, command_data):
+ self.command_queue.put(command_data)
# **********************************************************************************
# Audit_Log Methods
@@ -78,6 +105,10 @@ class ManagementController:
command,
name,
enabled=True,
+ one_time=False,
+ cron_string="* * * * *",
+ parent=None,
+ delay=0,
):
return HelpersManagement.create_scheduled_task(
server_id,
@@ -88,20 +119,16 @@ class ManagementController:
command,
name,
enabled,
+ one_time,
+ cron_string,
+ parent,
+ delay,
)
@staticmethod
def delete_scheduled_task(schedule_id):
return HelpersManagement.delete_scheduled_task(schedule_id)
- @staticmethod
- def set_login_image(path):
- HelpersManagement.set_login_image(path)
-
- @staticmethod
- def get_login_image():
- return HelpersManagement.get_login_image()
-
@staticmethod
def update_scheduled_task(schedule_id, updates):
return HelpersManagement.update_scheduled_task(schedule_id, updates)
@@ -145,9 +172,18 @@ class ManagementController:
excluded_dirs: list = None,
compress: bool = False,
shutdown: bool = False,
+ before: str = "",
+ after: str = "",
):
return self.management_helper.set_backup_config(
- server_id, backup_path, max_backups, excluded_dirs, compress, shutdown
+ server_id,
+ backup_path,
+ max_backups,
+ excluded_dirs,
+ compress,
+ shutdown,
+ before,
+ after,
)
@staticmethod
diff --git a/app/classes/minecraft/stats.py b/app/classes/minecraft/stats.py
index e3611509..07dd9c0d 100644
--- a/app/classes/minecraft/stats.py
+++ b/app/classes/minecraft/stats.py
@@ -300,7 +300,7 @@ class Stats:
server_icon = base64.encodebytes(ping_obj["icon"])
except Exception as e:
server_icon = False
- logger.info(
+ logger.debug(
"Unable to read the server icon due to the following error:", exc_info=e
)
ping_data = {
diff --git a/app/classes/models/management.py b/app/classes/models/management.py
index 55c86bb7..c2b5afde 100644
--- a/app/classes/models/management.py
+++ b/app/classes/models/management.py
@@ -13,7 +13,7 @@ from peewee import (
from playhouse.shortcuts import model_to_dict
from app.classes.models.base_model import BaseModel
-from app.classes.models.users import Users, HelperUsers
+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.main_models import DatabaseShortcuts
@@ -43,7 +43,9 @@ class AuditLog(BaseModel):
# **********************************************************************************
class CraftySettings(BaseModel):
secret_api_key = CharField(default="")
+ cookie_secret = CharField(default="")
login_photo = CharField(default="login_1.jpg")
+ login_opacity = IntegerField(default=100)
class Meta:
table_name = "crafty_settings"
@@ -68,22 +70,6 @@ class HostStats(BaseModel):
table_name = "host_stats"
-# **********************************************************************************
-# Commands Class
-# **********************************************************************************
-class Commands(BaseModel):
- command_id = AutoField()
- created = DateTimeField(default=datetime.datetime.now)
- server_id = ForeignKeyField(Servers, backref="server", index=True)
- user = ForeignKeyField(Users, backref="user", index=True)
- source_ip = CharField(default="127.0.0.1")
- command = CharField(default="")
- executed = BooleanField(default=False)
-
- class Meta:
- table_name = "commands"
-
-
# **********************************************************************************
# Webhooks Class
# **********************************************************************************
@@ -131,6 +117,8 @@ class Backups(BaseModel):
server_id = ForeignKeyField(Servers, backref="backups_server")
compress = BooleanField(default=False)
shutdown = BooleanField(default=False)
+ before = CharField(default="")
+ after = CharField(default="")
class Meta:
table_name = "backups"
@@ -150,33 +138,6 @@ class HelpersManagement:
query = HostStats.select().order_by(HostStats.id.desc()).get()
return model_to_dict(query)
- # **********************************************************************************
- # Commands Methods
- # **********************************************************************************
- @staticmethod
- def add_command(server_id, user_id, remote_ip, command):
- Commands.insert(
- {
- Commands.server_id: server_id,
- Commands.user: user_id,
- Commands.source_ip: remote_ip,
- Commands.command: command,
- }
- ).execute()
-
- @staticmethod
- def get_unactioned_commands():
- query = Commands.select().where(Commands.executed == 0)
- return query
-
- @staticmethod
- def mark_command_complete(command_id=None):
- if command_id is not None:
- logger.debug(f"Marking Command {command_id} completed")
- Commands.update({Commands.executed: True}).where(
- Commands.command_id == command_id
- ).execute()
-
# **********************************************************************************
# Audit_Log Methods
# **********************************************************************************
@@ -244,9 +205,22 @@ class HelpersManagement:
else:
return
+ @staticmethod
+ def create_crafty_row():
+ CraftySettings.insert(
+ {
+ CraftySettings.secret_api_key: "",
+ CraftySettings.cookie_secret: "",
+ CraftySettings.login_photo: "login_1.jpg",
+ CraftySettings.login_opacity: 100,
+ }
+ ).execute()
+
@staticmethod
def set_secret_api_key(key):
- CraftySettings.insert(secret_api_key=key).execute()
+ CraftySettings.update({CraftySettings.secret_api_key: key}).where(
+ CraftySettings.id == 1
+ ).execute()
@staticmethod
def get_secret_api_key():
@@ -255,6 +229,22 @@ class HelpersManagement:
)
return settings[0].secret_api_key
+ @staticmethod
+ def get_cookie_secret():
+ settings = CraftySettings.select(CraftySettings.cookie_secret).where(
+ CraftySettings.id == 1
+ )
+ return settings[0].cookie_secret
+
+ @staticmethod
+ def set_cookie_secret(key):
+ CraftySettings.update({CraftySettings.cookie_secret: key}).where(
+ CraftySettings.id == 1
+ ).execute()
+
+ # **********************************************************************************
+ # Config Methods
+ # **********************************************************************************
@staticmethod
def get_login_image():
settings = CraftySettings.select(CraftySettings.login_photo).where(
@@ -268,6 +258,19 @@ class HelpersManagement:
CraftySettings.id == 1
).execute()
+ @staticmethod
+ def get_login_opacity():
+ settings = CraftySettings.select(CraftySettings.login_opacity).where(
+ CraftySettings.id == 1
+ )
+ return settings[0].login_opacity
+
+ @staticmethod
+ def set_login_opacity(opacity):
+ CraftySettings.update({CraftySettings.login_opacity: opacity}).where(
+ CraftySettings.id == 1
+ ).execute()
+
# **********************************************************************************
# Schedules Methods
# **********************************************************************************
@@ -369,6 +372,8 @@ class HelpersManagement:
"server_id": row.server_id_id,
"compress": row.compress,
"shutdown": row.shutdown,
+ "before": row.before,
+ "after": row.after,
}
except IndexError:
conf = {
@@ -378,6 +383,8 @@ class HelpersManagement:
"server_id": server_id,
"compress": False,
"shutdown": False,
+ "before": "",
+ "after": "",
}
return conf
@@ -393,6 +400,8 @@ class HelpersManagement:
excluded_dirs: list = None,
compress: bool = False,
shutdown: bool = False,
+ before: str = "",
+ after: str = "",
):
logger.debug(f"Updating server {server_id} backup config with {locals()}")
if Backups.select().where(Backups.server_id == server_id).exists():
@@ -405,6 +414,8 @@ class HelpersManagement:
"server_id": server_id,
"compress": False,
"shutdown": False,
+ "before": "",
+ "after": "",
}
new_row = True
if max_backups is not None:
@@ -414,6 +425,8 @@ class HelpersManagement:
conf["excluded_dirs"] = dirs_to_exclude
conf["compress"] = compress
conf["shutdown"] = shutdown
+ conf["before"] = before
+ conf["after"] = after
if not new_row:
with self.database.atomic():
if backup_path is not None:
@@ -473,9 +486,3 @@ class HelpersManagement:
f"Not removing {dir_to_del} from excluded directories - "
f"not in the excluded directory list for server ID {server_id}"
)
-
- @staticmethod
- def clear_unexecuted_commands():
- Commands.update({Commands.executed: True}).where(
- Commands.executed == False # pylint: disable=singleton-comparison
- ).execute()
diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py
index c5238ae8..92226425 100644
--- a/app/classes/shared/helpers.py
+++ b/app/classes/shared/helpers.py
@@ -78,6 +78,7 @@ class Helpers:
self.websocket_helper = WebSocketHelper(self)
self.translation = Translation(self)
self.update_available = False
+ self.ignored_names = ["crafty_managed.txt", "db_stats"]
@staticmethod
def auto_installer_fix(ex):
@@ -376,6 +377,64 @@ class Helpers:
return default_return
+ def set_settings(self, data):
+ try:
+ with open(self.settings_file, "w", encoding="utf-8") as f:
+ json.dump(data, f, indent=4)
+
+ except Exception as e:
+ logger.critical(
+ f"Config File Error: Unable to read {self.settings_file} due to {e}"
+ )
+ Console.critical(
+ f"Config File Error: Unable to read {self.settings_file} due to {e}"
+ )
+ return False
+
+ return True
+
+ @staticmethod
+ def get_master_config():
+ # Make changes for users' local config.json files here. As of 4.0.20
+ # Config.json was removed from the repo to make it easier for users
+ # To make non-breaking changes to the file.
+ return {
+ "http_port": 8000,
+ "https_port": 8443,
+ "language": "en_EN",
+ "cookie_expire": 30,
+ "show_errors": True,
+ "history_max_age": 7,
+ "stats_update_frequency": 30,
+ "delete_default_json": False,
+ "show_contribute_link": True,
+ "virtual_terminal_lines": 70,
+ "max_log_lines": 700,
+ "max_audit_entries": 300,
+ "disabled_language_files": [],
+ "stream_size_GB": 1,
+ "keywords": ["help", "chunk"],
+ "allow_nsfw_profile_pictures": False,
+ "enable_user_self_delete": False,
+ "reset_secrets_on_next_boot": False,
+ }
+
+ def get_all_settings(self):
+ try:
+ with open(self.settings_file, "r", encoding="utf-8") as f:
+ data = json.load(f)
+
+ except Exception as e:
+ data = {}
+ logger.critical(
+ f"Config File Error: Unable to read {self.settings_file} due to {e}"
+ )
+ Console.critical(
+ f"Config File Error: Unable to read {self.settings_file} due to {e}"
+ )
+
+ return data
+
@staticmethod
def is_subdir(server_path, root_dir):
server_path = os.path.realpath(server_path)
@@ -947,8 +1006,7 @@ class Helpers:
return data
- @staticmethod
- def generate_tree(folder, output=""):
+ def generate_tree(self, folder, output=""):
dir_list = []
unsorted_files = []
file_list = os.listdir(folder)
@@ -965,18 +1023,22 @@ class Helpers:
rel = os.path.join(folder, raw_filename)
dpath = os.path.join(folder, filename)
if os.path.isdir(rel):
- output += f"""