diff --git a/.gitignore b/.gitignore
index d3b153ad..86cf2616 100644
--- a/.gitignore
+++ b/.gitignore
@@ -38,3 +38,4 @@ docker/*
!docker/docker-compose.yml
lang_sort_log.txt
lang_sort.txt
+app/migrations/status
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 7e86c021..35b6066d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,16 +1,24 @@
# Changelog
-## --- [4.4.4] - 2024/TBD
+## --- [4.4.5] - 2024/TBD
### New features
TBD
### Bug fixes
-- Fix logic issue causing bedrock wizard's root files buttons to not respond to user click events ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/797))
-- Reset crash detection counter after crash detection process detects successful start ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/798))
+TBD
### Tweaks
TBD
### Lang
TBD
+## --- [4.4.4] - 2024/10/03
+### Bug fixes
+- Migrations | Fix orphan schedule configurations crashing migration operation ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/796))
+- Fix logic issue causing bedrock wizard's root files buttons to not respond to user click events ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/797))
+- Reset crash detection counter after crash detection process detects successful start ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/798))
+- Update new bedrock DL url and correctly bubble up exception on DL fail - Thanks @sarcastron ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/802))
+- Bump cryptography for GHSA-h4gh-qq45-vh27 ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/803))
+
+
## --- [4.4.3] - 2024/08/08
### Bug fixes
- Fix schedules creation fail due to missing action ID ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/791))
diff --git a/README.md b/README.md
index c4b7ad56..5345299b 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.4.4
+# Crafty Controller 4.4.5
> Python based Control Panel for your Minecraft Server
## What is Crafty Controller?
diff --git a/app/classes/shared/helpers.py b/app/classes/shared/helpers.py
index e827d5b2..7378c9de 100644
--- a/app/classes/shared/helpers.py
+++ b/app/classes/shared/helpers.py
@@ -58,6 +58,7 @@ class Helpers:
def __init__(self):
self.root_dir = os.path.abspath(os.path.curdir)
+ self.read_annc = False
self.config_dir = os.path.join(self.root_dir, "app", "config")
self.webroot = os.path.join(self.root_dir, "app", "frontend")
self.servers_dir = os.path.join(self.root_dir, "servers")
@@ -79,6 +80,7 @@ class Helpers:
self.translation = Translation(self)
self.update_available = False
+ self.migration_notifications = []
self.ignored_names = ["crafty_managed.txt", "db_stats"]
self.crafty_starting = False
self.minimum_password_length = 8
@@ -128,24 +130,33 @@ class Helpers:
"Chrome/104.0.0.0 Safari/537.36"
),
}
- target_win = 'https://minecraft.azureedge.net/bin-win/[^"]*'
- target_linux = 'https://minecraft.azureedge.net/bin-linux/[^"]*'
-
+ target_win = 'https://www.minecraft.net/bedrockdedicatedserver/bin-win/[^"]*'
+ target_linux = (
+ 'https://www.minecraft.net/bedrockdedicatedserver/bin-linux/[^"]*'
+ )
try:
# Get minecraft server download page
# (hopefully the don't change the structure)
download_page = get(url, headers=headers, timeout=1)
-
+ download_page.raise_for_status()
# Search for our string targets
- win_download_url = re.search(target_win, download_page.text).group(0)
- linux_download_url = re.search(target_linux, download_page.text).group(0)
+ win_search_result = re.search(target_win, download_page.text)
+ linux_search_result = re.search(target_linux, download_page.text)
+ if win_search_result is None or linux_search_result is None:
+ raise RuntimeError(
+ "Could not determine download URL from minecraft.net."
+ )
+ win_download_url = win_search_result.group(0)
+ linux_download_url = linux_search_result.group(0)
+ print(win_download_url, linux_download_url)
if os.name == "nt":
return win_download_url
return linux_download_url
except Exception as e:
logger.error(f"Unable to resolve remote bedrock download url! \n{e}")
+ raise e
return False
def get_execution_java(self, value, execution_command):
@@ -614,11 +625,49 @@ class Helpers:
return version_data
- def get_announcements(self):
+ def check_migrations(self) -> None:
+ if self.read_annc is False:
+ self.read_annc = True
+ for file in os.listdir(
+ os.path.join(self.root_dir, "app", "migrations", "status")
+ ):
+ with open(
+ os.path.join(self.root_dir, "app", "migrations", "status", file),
+ "r",
+ encoding="utf-8",
+ ) as notif_file:
+ file_json = json.load(notif_file)
+ for notif in file_json:
+ if not file_json[notif].get("status"):
+ self.migration_notifications.append(file_json[notif])
+
+ def get_announcements(self, lang=None):
try:
data = []
response = requests.get("https://craftycontrol.com/notify", timeout=2)
data = json.loads(response.content)
+ if not lang:
+ lang = self.get_setting("language")
+ self.check_migrations()
+ for migration_warning in self.migration_notifications:
+ if not migration_warning.get("status"):
+ data.append(
+ {
+ "id": migration_warning.get("pid"),
+ "title": self.translation.translate(
+ "notify",
+ f"{migration_warning.get('type')}_title",
+ lang,
+ ),
+ "date": "",
+ "desc": self.translation.translate(
+ "notify",
+ f"{migration_warning.get('type')}_desc",
+ lang,
+ ),
+ "link": "",
+ }
+ )
if self.update_available:
data.append(self.update_available)
return data
diff --git a/app/classes/shared/import_helper.py b/app/classes/shared/import_helper.py
index 030feb56..d0063409 100644
--- a/app/classes/shared/import_helper.py
+++ b/app/classes/shared/import_helper.py
@@ -217,15 +217,16 @@ class ImportHelpers:
FileHelpers.del_dirs(temp_dir)
def download_bedrock_server(self, path, new_id):
+ bedrock_url = Helpers.get_latest_bedrock_url()
download_thread = threading.Thread(
target=self.download_threaded_bedrock_server,
daemon=True,
- args=(path, new_id),
+ args=(path, new_id, bedrock_url),
name=f"{new_id}_download",
)
download_thread.start()
- def download_threaded_bedrock_server(self, path, new_id):
+ def download_threaded_bedrock_server(self, path, new_id, bedrock_url):
"""
Downloads the latest Bedrock server, unzips it, sets necessary permissions.
@@ -236,10 +237,8 @@ class ImportHelpers:
This method handles exceptions and logs errors for each step of the process.
"""
try:
- bedrock_url = Helpers.get_latest_bedrock_url()
if bedrock_url:
file_path = os.path.join(path, "bedrock_server.zip")
-
success = FileHelpers.ssl_get_file(
bedrock_url, path, "bedrock_server.zip"
)
@@ -263,6 +262,7 @@ class ImportHelpers:
logger.critical(
f"Failed to download bedrock executable during server creation! \n{e}"
)
+ raise e
ServersController.finish_import(new_id)
server_users = PermissionsServers.get_server_user_list(new_id)
diff --git a/app/classes/web/routes/api/crafty/announcements/index.py b/app/classes/web/routes/api/crafty/announcements/index.py
index d66c4473..2cd80ba6 100644
--- a/app/classes/web/routes/api/crafty/announcements/index.py
+++ b/app/classes/web/routes/api/crafty/announcements/index.py
@@ -29,7 +29,7 @@ class ApiAnnounceIndexHandler(BaseApiHandler):
_,
) = auth_data
- data = self.helper.get_announcements()
+ data = self.helper.get_announcements(auth_data[4]["lang"])
if not data:
return self.finish_json(
424,
diff --git a/app/classes/web/routes/api/servers/index.py b/app/classes/web/routes/api/servers/index.py
index ca551326..ae0590a5 100644
--- a/app/classes/web/routes/api/servers/index.py
+++ b/app/classes/web/routes/api/servers/index.py
@@ -725,7 +725,19 @@ class ApiServersIndexHandler(BaseApiHandler):
405, {"status": "error", "error": "DATA CONSTRAINT FAILED"}
)
return
- new_server_id = self.controller.create_api_server(data, user["user_id"])
+ try:
+ new_server_id = self.controller.create_api_server(data, user["user_id"])
+ except Exception as e:
+ self.controller.servers.stats.record_stats()
+
+ self.finish_json(
+ 503,
+ {
+ "status": "error",
+ "error": "Could not create server",
+ "error_data": str(e),
+ },
+ )
self.controller.servers.stats.record_stats()
diff --git a/app/config/version.json b/app/config/version.json
index 4ae818ef..8fcaeff4 100644
--- a/app/config/version.json
+++ b/app/config/version.json
@@ -1,5 +1,5 @@
{
"major": 4,
"minor": 4,
- "sub": 4
+ "sub": 5
}
diff --git a/app/frontend/templates/server/bedrock_wizard.html b/app/frontend/templates/server/bedrock_wizard.html
index e7d7346a..64ae86be 100644
--- a/app/frontend/templates/server/bedrock_wizard.html
+++ b/app/frontend/templates/server/bedrock_wizard.html
@@ -619,7 +619,9 @@
if (responseData.status === "ok") {
window.location.href = '/panel/dashboard';
} else {
-
+ // Close the "be patient..." dialogue box
+ $('.bootbox-close-button').click();
+ // Alert the user that there was an issue.
bootbox.alert({
title: responseData.error,
message: responseData.error_data
@@ -778,4 +780,4 @@
-{% end %}
\ No newline at end of file
+{% end %}
diff --git a/app/migrations/20240308_multi-backup.py b/app/migrations/20240308_multi-backup.py
index 56e01a5c..64ff03e1 100644
--- a/app/migrations/20240308_multi-backup.py
+++ b/app/migrations/20240308_multi-backup.py
@@ -1,4 +1,5 @@
import os
+import json
import datetime
import uuid
import peewee
@@ -13,9 +14,9 @@ from app.classes.shared.file_helpers import FileHelpers
logger = logging.getLogger(__name__)
-def is_valid_backup(backup, all_servers):
+def is_valid_entry(entry, all_servers):
try:
- return str(backup.server_id) in all_servers
+ return str(entry.server_id) in all_servers
except (TypeError, peewee.DoesNotExist):
return False
@@ -24,6 +25,8 @@ def migrate(migrator: Migrator, database, **kwargs):
"""
Write your migrations here.
"""
+ backup_migration_status = True
+ schedule_migration_status = True
db = database
Console.info("Starting Backups migrations")
Console.info(
@@ -161,10 +164,20 @@ def migrate(migrator: Migrator, database, **kwargs):
row.server_id for row in Servers.select(Servers.server_id).distinct()
]
all_backups = Backups.select()
+ all_schedules = Schedules.select()
Console.info("Cleaning up orphan backups for all servers")
valid_backups = [
- backup for backup in all_backups if is_valid_backup(backup, all_servers)
+ backup for backup in all_backups if is_valid_entry(backup, all_servers)
]
+ if len(valid_backups) < len(all_backups):
+ backup_migration_status = False
+ print("Orphan backup found")
+ Console.info("Cleaning up orphan schedules for all servers")
+ valid_schedules = [
+ schedule for schedule in all_schedules if is_valid_entry(schedule, all_servers)
+ ]
+ if len(valid_schedules) < len(all_schedules):
+ schedule_migration_status = False
# Copy data from the existing backups table to the new one
for backup in valid_backups:
Console.info(f"Trying to get server for backup migration {backup.server_id}")
@@ -221,13 +234,20 @@ def migrate(migrator: Migrator, database, **kwargs):
Console.debug("Migrations: Dropping backup_path from servers table")
migrator.drop_columns("servers", ["backup_path"])
- for schedule in Schedules.select():
+ for schedule in valid_schedules:
action_id = None
if schedule.command == "backup_server":
Console.info(
f"Migrations: Adding backup ID to task with name {schedule.name}"
)
- backup = NewBackups.get(NewBackups.server_id == schedule.server_id)
+ try:
+ backup = NewBackups.get(NewBackups.server_id == schedule.server_id)
+ except:
+ schedule_migration_status = False
+ Console.error(
+ "Could not find backup with selected server ID. Omitting from register."
+ )
+ continue
action_id = backup.backup_id
NewSchedules.create(
schedule_id=schedule.schedule_id,
@@ -255,6 +275,34 @@ def migrate(migrator: Migrator, database, **kwargs):
# Rename the new table to backups
migrator.rename_table("new_schedules", "schedules")
+ with open(
+ os.path.join(
+ os.path.abspath(os.path.curdir),
+ "app",
+ "migrations",
+ "status",
+ "20240308_multi-backup.json",
+ ),
+ "w",
+ encoding="utf-8",
+ ) as file:
+ file.write(
+ json.dumps(
+ {
+ "backup_migration": {
+ "type": "backup",
+ "status": backup_migration_status,
+ "pid": str(uuid.uuid4()),
+ },
+ "schedule_migration": {
+ "type": "schedule",
+ "status": schedule_migration_status,
+ "pid": str(uuid.uuid4()),
+ },
+ }
+ )
+ )
+
def rollback(migrator: Migrator, database, **kwargs):
"""
diff --git a/app/translations/en_EN.json b/app/translations/en_EN.json
index ed35ae9c..f4b79bd0 100644
--- a/app/translations/en_EN.json
+++ b/app/translations/en_EN.json
@@ -231,10 +231,14 @@
"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_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_title": "Schedules Migration Warning",
"supportLogs": "Support Logs"
},
"offline": {
diff --git a/app/translations/lv_LV.json b/app/translations/lv_LV.json
index 96753531..64274784 100644
--- a/app/translations/lv_LV.json
+++ b/app/translations/lv_LV.json
@@ -235,10 +235,14 @@
"activityLog": "Aktivitātes Logi",
"backupComplete": "Dublējums veiksmīgi pabeigts priekš servera ",
"backupStarted": "Dublējums startēts priekš servera ",
+ "backup_desc": "Mēs noteicām ka dublējuma migrācija daļēji vai pilnīgi neizdevās. Lūdzu pārskatiet savus dublējumus dublējumu cilnē.",
+ "backup_title": "Dublējuma Migrācijas Brīdinājums",
"downloadLogs": "Lejupielādēt Atbalsta Log Failus?",
"finishedPreparing": "Mēs esam pabeiguši sagatavot jūsu atbalsta log datnes. Lūdzu nospiediet lejupielādet lai lejupielādētu",
"logout": "Iziet",
"preparingLogs": " Lūdzu uzgaidiet kamēr mēs sagatavojam jūsu log datnes... Mēs jums nosūtīsim paziņojumu kad tās būs gatavas. Tas var aizņemt kādu laiku priekš lielām instalācijām.",
+ "schedule_desc": "Mēs noteicām ka daži vai visi no jūsu darbību grafikiem nebija veiksmīgi pārnesti atjauninājuma laikā. Lūdzu pārskatiet savus grafikus grafiku cilnē.",
+ "schedule_title": "Grafiku Migrācijas Brīdinājums",
"supportLogs": "Atbalsta Logi"
},
"offline": {
diff --git a/app/translations/nl_BE.json b/app/translations/nl_BE.json
index d776a0aa..c75654f4 100644
--- a/app/translations/nl_BE.json
+++ b/app/translations/nl_BE.json
@@ -234,10 +234,14 @@
"activityLog": "Activiteitslogboeken",
"backupComplete": "Back-up succesvol voltooid voor server {}",
"backupStarted": "Backup gestart voor server {}",
+ "backup_desc": "We hebben gedetecteerd dat de back-upmigratie mogelijk gedeeltelijk of volledig is mislukt. Controleer uw back-uprecords op het tabblad Backups.",
+ "backup_title": "Waarschuwing voor back-upmigratie",
"downloadLogs": "Ondersteuningslogboeken downloaden?",
"finishedPreparing": "We zijn klaar met het voorbereiden van uw ondersteuningslogboeken. Klik op download om te downloaden",
"logout": "Uitloggen",
"preparingLogs": " Een ogenblik geduld alstublieft terwijl wij uw logboeken voorbereiden... We sturen een bericht als ze klaar zijn. Dit kan een tijdje duren voor grote implementaties.",
+ "schedule_desc": "We hebben gedetecteerd dat sommige of alle geplande taken niet succesvol zijn overgedragen tijdens de upgrade. Controleer uw schema's op het tabblad Schema's.",
+ "schedule_title": "Waarschuwing voor schemamigratie",
"supportLogs": "Ondersteuningslogboeken"
},
"offline": {
diff --git a/app/translations/uk_UA.json b/app/translations/uk_UA.json
index aae489ad..ac6e10d9 100644
--- a/app/translations/uk_UA.json
+++ b/app/translations/uk_UA.json
@@ -234,10 +234,14 @@
"activityLog": "Логи активностей",
"backupComplete": "Бекап успішно завершено для сервера {}",
"backupStarted": "Бекап успішно розпочато для сервера {}",
+ "backup_desc": "Ми зафіксували, що міграція бекапів можливо частково або повністю провалилась. Будь ласка перевірте ваші записи бекапів у відповідній вкладці.",
+ "backup_title": "Увага міграція бекапів",
"downloadLogs": "Завантажити логи для підтримки?",
"finishedPreparing": "Ми підготували логи. Будь ласка натисніть завантажити",
"logout": "Вихід",
"preparingLogs": "Будь ласка зачекайте поки ми підготуємо для вас логи... Ми надішлемо вам сповіщення коли усе буде готово. Це може зайняти трішки часу для великих проєктів.",
+ "schedule_desc": "Ми зафіксували, що деякі або всі ваші заплановані завдання не вдалось успішно перенести поки робиться оновлення. Будь ласка перевірте ваші заплановані завдання у відповідній вкладці.",
+ "schedule_title": "Увага міграція запланованих завдань",
"supportLogs": "Логи для підтримки"
},
"offline": {
diff --git a/main.py b/main.py
index a62a8d27..69cc0043 100644
--- a/main.py
+++ b/main.py
@@ -115,6 +115,23 @@ def controller_setup():
controller.clear_support_status()
+def get_migration_notifications():
+ migration_notifications = []
+ for file in os.listdir(
+ os.path.join(APPLICATION_PATH, "app", "migrations", "status")
+ ):
+ if os.path.isfile(file):
+ with open(
+ os.path.join(APPLICATION_PATH, "app", "migrations", "status", file),
+ encoding="utf-8",
+ ) as status_file:
+ status_json = json.load(status_file)
+ for item in status_json:
+ if not status_json[item].get("status"):
+ migration_notifications.append(item)
+ return migration_notifications
+
+
def tasks_starter():
"""
Method starts stats recording, app scheduler, and
@@ -350,6 +367,9 @@ if __name__ == "__main__":
helper.db_path, pragmas={"journal_mode": "wal", "cache_size": -1024 * 10}
)
database_proxy.initialize(database)
+ Helpers.ensure_dir_exists(
+ os.path.join(APPLICATION_PATH, "app", "migrations", "status")
+ )
migration_manager = MigrationManager(database, helper)
migration_manager.up() # Automatically runs migrations
@@ -408,7 +428,7 @@ if __name__ == "__main__":
controller.set_project_root(APPLICATION_PATH)
tasks_manager = TasksManager(helper, controller, file_helper)
import3 = Import3(helper, controller)
-
+ helper.migration_notifications = get_migration_notifications()
# Check to see if client config.json version is different than the
# Master config.json in helpers.py
Console.info("Checking for remote changes to config.json")
diff --git a/requirements.txt b/requirements.txt
index 743da0a8..494b12a2 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,13 +4,13 @@ argon2-cffi==23.1.0
cached_property==1.5.2
colorama==0.4.6
croniter==1.4.1
-cryptography==42.0.4
+cryptography==43.0.1
libgravatar==1.0.4
nh3==0.2.14
packaging==23.2
peewee==3.13
psutil==5.9.5
-pyOpenSSL==24.0.0
+pyOpenSSL==24.2.1
pyjwt==2.8.0
PyYAML==6.0.1
requests==2.32.0
diff --git a/sonar-project.properties b/sonar-project.properties
index 466f0a01..114926e2 100644
--- a/sonar-project.properties
+++ b/sonar-project.properties
@@ -3,7 +3,7 @@ sonar.organization=crafty-controller
# This is the name and version displayed in the SonarCloud UI.
sonar.projectName=Crafty 4
-sonar.projectVersion=4.4.4
+sonar.projectVersion=4.4.5
sonar.python.version=3.9, 3.10, 3.11
sonar.exclusions=app/migrations/**, app/frontend/static/assets/vendors/**