Compare commits

...

27 Commits

Author SHA1 Message Date
Volodymyr
1b0bf1def3 Merge branch 'feature/file-tree-checkboxes' into 'dev'
Some checks failed
Lint / Lint (push) Has been cancelled
Added check boxes to file tree

See merge request crafty-controller/crafty-4!866
2025-09-01 02:18:26 +00:00
Zedifus
cfe845686a Prepare 4.5.3 release base
Some checks failed
Build Docker Images / Build Docker Images (push) Has been cancelled
Lint / Lint (push) Has been cancelled
Build pyinstaller apps / Build Packages (pyinstaller -F main.py --name "crafty4" \ --distpath . \ --hidden-import cryptography \ --hidden-import cffi \ --hidden-import apscheduler \ --collect-all tzlocal \ --collect-all tzdata \ --collect-all pytz \ --collect-a… (push) Has been cancelled
Build pyinstaller apps / Build Packages (pyinstaller -F main.py --name "crafty4" ` --distpath . ` --icon app\frontend\static\assets\images\Crafty_4-0_Logo_square.ico ` --hidden-import cryptography ` --hidden-import cffi ` --hidden-import apscheduler ` --collect-all… (push) Has been cancelled
2025-08-31 23:19:16 +01:00
Zedifus
f5a772c562 Close changelog 4.5.2 2025-08-31 23:04:45 +01:00
Iain Powrie
c33a14e426 Merge branch 'tweak/neoforge-regex' into 'dev'
Modify Neoforge Regex for Beta Tag

See merge request crafty-controller/crafty-4!886
2025-08-31 22:00:35 +00:00
Zedifus
b24eef3b80 Upgrade changelog !886
Some checks failed
Lint / Lint (push) Has been cancelled
2025-08-31 22:54:25 +01:00
Zedifus
47756ca410 Merge branch 'dev' into tweak/neoforge-regex 2025-08-31 22:51:02 +01:00
Iain Powrie
98578708bc Merge branch 'bugfix/directory-calculation' into 'dev'
Fix Human Readable Sizes on Dashboard

See merge request crafty-controller/crafty-4!885
2025-08-31 21:44:35 +00:00
Zedifus
3c02df35f6 Update changelog !885
Some checks failed
Lint / Lint (push) Has been cancelled
2025-08-31 22:29:36 +01:00
Zedifus
5ca51045a8 Merge branch 'dev' into bugfix/directory-calculation 2025-08-31 22:26:40 +01:00
Iain Powrie
49c1e6800f Merge branch 'bugfix/bedrock-builder' into 'dev'
Fix Bedrock Server Builder

See merge request crafty-controller/crafty-4!884
2025-08-31 21:23:22 +00:00
Zedifus
5c720d5210 Upgrade changelog !884
Some checks failed
Lint / Lint (push) Has been cancelled
2025-08-31 22:14:47 +01:00
=
8f8aacf4a9 Cleanup update URL for modded servers 2025-08-27 15:01:00 -04:00
=
0e21d50716 Modify neoforge regex for startup command 2025-08-27 15:00:51 -04:00
=
2eee82172c Calculate human readable outside of dir size method 2025-08-25 13:28:57 -04:00
=
2fe4320db6 use self to call filehelpers instead of static 2025-08-25 12:44:47 -04:00
Volodymyr Zuyev
d231c128de Console log cleanup
Some checks failed
Lint / Lint (push) Has been cancelled
2025-06-20 16:08:14 -05:00
Volodymyr Zuyev
f6e578315b Possible button seperation 2025-06-20 15:55:24 -05:00
Volodymyr Zuyev
256dd2713e Fixed bug: files would try to uncheck the 'root' dict 2025-06-20 13:54:55 -05:00
Volodymyr Zuyev
6f262b39ef Restored original translations, besides Ukrainian and English 2025-06-20 13:15:37 -05:00
Volodymyr Zuyev
4a9d81bab7 Removed redundant comprehension
Some checks failed
Lint / Lint (push) Has been cancelled
2025-06-17 10:11:27 -05:00
Volodymyr Zuyev
9a2774a181 Removed one iteration of looping over filenames. Files sorted at the same time as being checked if they exist 2025-06-17 10:07:03 -05:00
Volodymyr Zuyev
738d148c2a Refactored so translations would be closer to the way ther originally were 2025-06-17 09:55:06 -05:00
Volodymyr Zuyev
99d6d374b4 Bugfix: fixed a typo in filename 2025-06-17 05:11:17 +00:00
Volodymyr Zuyev
c597164a1a Fixed bug where selecting nested files, and their root dir would throw a ui error by filtering paths to delete 2025-06-17 05:11:17 +00:00
Volodymyr Zuyev
61b2be2e8e Forgot to delete old translation in Polish 2025-06-17 05:11:17 +00:00
Volodymyr Zuyev
9d7f511775 Updated traslations to match new layout 2025-06-17 05:11:17 +00:00
Volodymyr Zuyev
0d371a0bb5 Added responsive checkboxes for mass file deletion. Confirmation window works as well 2025-06-17 05:11:17 +00:00
12 changed files with 224 additions and 105 deletions

View File

@@ -1,5 +1,5 @@
# Changelog
## --- [4.5.2] - 2025/TBD
## --- [4.5.3] - 2025/TBD
### New features
TBD
### Bug fixes
@@ -10,6 +10,13 @@ TBD
TBD
<br><br>
## --- [4.5.2] - 2025/08/31
### Bug fixes
- Bedrock Builder | Utilize self call instead of static call for unzipping bedrock archives ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/884))
- Fix Human Readable Sizes on Dashboard ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/885))
- Correct Builder to support new Neoforge versioning scheme ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/886))
<br><br>
## --- [4.5.1] - 2025/08/25
### Bug fixes
- Fix bug where all file methods that were not `GET` methods, were returning "method not allowed" ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/882))

View File

@@ -1,5 +1,5 @@
[![Crafty Logo](app/frontend/static/assets/images/logo_long.svg)](https://craftycontrol.com)
# Crafty Controller 4.5.2
# Crafty Controller 4.5.3
> Python based Control Panel for your Minecraft Server
## What is Crafty Controller?

View File

@@ -1059,21 +1059,25 @@ class FileHelpers:
return zlib.decompress(bytes_to_decompress)
@staticmethod
def get_dir_size(server_path, raw_bytes=True):
def get_dir_size(server_path):
"""Recursively calculates dir size. Returns size in bytes. Must calculate human
readable based on returned data
Args:
server_path (str): Path to calculate size
Returns:
_type_: Integer
"""
# because this is a recursive function, we will return bytes,
# and set human readable later
total = 0
for entry in os.scandir(server_path):
if entry.is_dir(follow_symlinks=False):
total += FileHelpers.get_dir_size(entry.path, raw_bytes=raw_bytes)
total += FileHelpers.get_dir_size(entry.path)
else:
total += entry.stat(follow_symlinks=False).st_size
if raw_bytes:
return total
level_total_size = Helpers.human_readable_file_size(total)
return level_total_size
return total
@staticmethod
def get_drive_free_space(file_location: Path):

View File

@@ -244,7 +244,7 @@ class ImportHelpers:
unzip_path = self.helper.wtol_path(file_path)
# unzips archive that was downloaded.
FileHelpers.unzip_file(unzip_path)
self.file_helper.unzip_file(unzip_path)
# adjusts permissions for execution if os is not windows
if not self.helper.is_os_windows():

View File

@@ -577,7 +577,7 @@ class Controller:
if data["create_type"] == "minecraft_java":
if root_create_data["create_type"] == "download_jar":
# modded update urls from server jars will only update the installer
if create_data["type"] != "forge-installer":
if create_data["type"] not in MODDED_TYPES:
server_obj = self.servers.get_server_obj(new_server_id)
url = self.big_bucket.get_fetch_url(
create_data["category"],

View File

@@ -775,14 +775,15 @@ class ServerInstance:
# We get the server command parameters from forge script
server_command = re.findall(
r"java @([a-zA-Z0-9_\.]+)"
r" @([a-z.\/\-]+)([0-9.\-]+)"
r"\/\b([a-z_0-9]+\.txt)\b( .{2,4})?",
r" @([a-z./\-]+)"
r"([0-9.\-]+(?:-[a-zA-Z0-9]+)?)"
r"\/\b([a-z_0-9]+\.txt)\b"
r"( .{2,4})?",
run_file_text,
)[0]
version = server_command[2]
executable_path = f"{server_command[1]}{server_command[2]}/"
# Let's set the proper server executable
server_obj.executable = os.path.join(
f"{executable_path}{version_info[0][0]}-{version}-server.jar"
@@ -1476,7 +1477,9 @@ class ServerInstance:
def start_dir_calc_task(self):
server_dt = HelperServers.get_server_data_by_id(self.server_id)
self.server_size = self.file_helper.get_dir_size(server_dt["path"])
self.server_size = Helpers.human_readable_file_size(
self.file_helper.get_dir_size(server_dt["path"])
)
self.dir_scheduler.add_job(
self.calc_dir_size,
"interval",
@@ -1492,7 +1495,9 @@ class ServerInstance:
def calc_dir_size(self):
server_dt = HelperServers.get_server_data_by_id(self.server_id)
self.server_size = self.file_helper.get_dir_size(server_dt["path"])
self.server_size = Helpers.human_readable_file_size(
self.file_helper.get_dir_size(server_dt["path"])
)
# **********************************************************************************
# Minecraft Servers Statistics

View File

@@ -107,15 +107,10 @@ files_rename_schema = {
file_delete_schema = {
"type": "object",
"properties": {
"filename": {
"type": "string",
"minLength": 5,
"error": "typeString",
"fill": True,
},
"filenames": {"type": "array", "items": {"type": "string", "minLength": 5}}
},
"required": ["filenames"],
"additionalProperties": False,
"minProperties": 1,
}
@@ -330,30 +325,49 @@ class ApiServersServerFilesIndexHandler(BaseApiHandler):
"error_data": f"{str(err)}",
},
)
if not Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"],
data["filename"],
):
return self.finish_json(
400,
{
"status": "error",
"error": "TRAVERSAL DETECTED",
"error_data": str(e),
},
)
if os.path.isdir(data["filename"]):
proc = FileHelpers.del_dirs(data["filename"])
else:
proc = FileHelpers.del_file(data["filename"])
# disabling pylint because return value could be truthy
# but not a true boolean value
if proc == True: # pylint: disable=singleton-comparison
return self.finish_json(200, {"status": "ok"})
return self.finish_json(
500, {"status": "error", "error": "SERVER RUNNING", "error_data": str(proc)}
)
# Sort paths in assending ASCII order
normalized_paths = sorted(data["filenames"])
# extract only unique paths so at to not delete files "implicitly"
unique_paths = []
last_path = None
for path in normalized_paths:
if not Helpers.validate_traversal(
self.controller.servers.get_server_data_by_id(server_id)["path"],
path,
):
return self.finish_json(
400,
{
"status": "error",
"error": "TRAVERSAL DETECTED",
"error_data": str(e),
},
)
if last_path and path.startswith(last_path + os.sep):
continue
unique_paths.append(path)
last_path = path
for path in unique_paths:
if os.path.isdir(path):
proc = FileHelpers.del_dirs(path)
else:
proc = FileHelpers.del_file(path)
# disabling pylint because return value could be truthy
# but not a true boolean value
if proc != True: # pylint: disable=singleton-comparison
return self.finish_json(
500,
{
"status": "error",
"error": "SERVER RUNNING",
"error_data": str(proc),
},
)
return self.finish_json(200, {"status": "ok"})
def patch(self, server_id: str, _backup_id):
auth_data = self.authenticate_user()
@@ -902,9 +916,7 @@ class ApiServersServerFileDownload(BaseApiHandler):
)
archive_path.parent.mkdir(parents=True, exist_ok=True)
target_total_size = self.file_helper.get_dir_size(
Path(download_path), raw_bytes=True
)
target_total_size = self.file_helper.get_dir_size(Path(download_path))
free_drive_storage = self.file_helper.get_drive_free_space(
Path(download_path)
)

View File

@@ -1,5 +1,5 @@
{
"major": 4,
"minor": 5,
"sub": 2
"sub": 3
}

View File

@@ -87,6 +87,14 @@ end %} {% block content %}
</ul>
</li>
</ul>
<br/>
<div class="d-none" id="massActions">
<span>Mass actions: </span>
<br/>
<button class="btn btn-danger mr-2" id="massActionButtonDelete" onclick="deleteMultiFileE(event)"> Delete </button>
</div>
</div>
<div id="editor_container" class="col-md-6 col-sm-12">
@@ -95,8 +103,7 @@ end %} {% block content %}
<div class="editorManager">
<h2 id="fileError"></h2>
<div id="editorParent">
{{ translate('serverFiles', 'editingFile', data['lang']) }}
<span id="editingFile"></span>
{{ translate('serverFiles', 'editingFile', data['lang']) }} <span id="editingFile"></span>
<div id="editor" onresize="editor.resize()" style="resize: both; width: 100%">
file_contents
</div>
@@ -293,7 +300,37 @@ end %} {% block content %}
});
}
}
function updateFileCheckBox(event) {
if (!event.target.checked){
updateTreeUnchecked(event.target, event.target.getAttribute('pdir'));
} else {
updateTreeChecked(event.target.getAttribute("value"))
}
const checkedBoxes = document.querySelectorAll('input.file-check-deletion:checked');
const deleteBtn = document.getElementById('massActions');
if (checkedBoxes.length > 0) {
deleteBtn.classList.remove('d-none');
} else {
deleteBtn.classList.add('d-none');
}
}
function updateTreeUnchecked(checkBoxElem, parentDir) {
if (parentDir == "root"){
return;
}
if (!checkBoxElem.checked) {
const pElem = document.getElementById(parentDir+"delBox");
pElem.checked = false;
updateTreeUnchecked(pElem, pElem.getAttribute("pdir"))
}
}
function updateTreeChecked(pdir) {
const children = document.querySelectorAll(`[pdir="${pdir}"]`);
children.forEach(function(elem) {
elem.checked = true;
updateTreeChecked(elem.getAttribute("value"))
});
}
function setFileName(name) {
let fileName = name || "default.txt";
document.getElementById("editingFile").innerText = fileName;
@@ -527,26 +564,29 @@ end %} {% block content %}
}
}
async function deleteItem(path, el, callback) {
async function deleteItems(files, el, callback) {
const token = getCookie("_xsrf");
let res = await fetch(`/api/v2/servers/${serverId}/files`, {
method: "DELETE",
headers: {
"X-XSRFToken": token,
},
body: JSON.stringify({ filename: path }),
body: JSON.stringify({ filenames: files }),
});
let responseData = await res.json();
if (responseData.status === "ok") {
el = document.getElementById(path + "li");
$(el).remove();
document.getElementById("files-tree-nav").style.display = "none";
files.forEach(function(path) {
el = document.getElementById(path + "li");
$(el).remove();
document.getElementById("files-tree-nav").style.display = "none";
})
} else {
bootbox.alert({
title: responseData.error,
message: responseData.error_data
});
}
updateFileCheckBox();
}
async function unZip(path, callback) {
@@ -714,38 +754,57 @@ end %} {% block content %}
function process_tree_response(response) {
let path = response.data.root_path.path;
let text = ``;
let pdir = 'root'
let checked = "";
if (!response.data.root_path.top) {
text = `<ul class="tree-nested d-block" id="${path}ul">`;
pdir = path
if (document.getElementById(`${path}delBox`).checked) {
checked = "checked";
}
}
Object.entries(response.data).forEach(([key, value]) => {
if (key === "root_path" || key === "db_stats") {
//continue is not valid in for each. Return acts as a continue.
return;
}
let checked = "";
let excluded = "";
let dpath = value.path;
let filename = key;
if (value.dir) {
if (value.excluded) {
checked = "checked";
excluded = "checked";
}
text += `<li class="tree-item" id="${dpath}li" data-path="${dpath}">
\n<div id="${dpath}" data-path="${dpath}" data-name="${filename}" class="tree-caret tree-ctx-item tree-folder">
<input type="checkbox" class="checkBoxClass d-none file-check" name="root_path" value="${dpath}" ${checked}>
<span id="${dpath}span" class="files-tree-title" data-path="${dpath}" data-name="${filename}" onclick="getDirView(event)">
<i class="far fa-folder text-info"></i>
<i class="far fa-folder-open text-info"></i>
${filename}
</span>
</input></div></li>`;
text += `
<li class="tree-item" id="${dpath}li" data-path="${dpath}">
<div id="${dpath}" data-path="${dpath}" data-name="${filename}" class="tree-caret tree-ctx-item tree-folder">
<input type="checkbox" class="checkBoxClass d-none file-check" name="root_path" value="${dpath}" ${excluded}>
<input type="checkbox" id="${dpath}delBox" class="checkBoxClass file-check-deletion" name="root_path" value="${dpath}" onclick="updateFileCheckBox(event)" pdir="${pdir}" ${checked}>
<span id="${dpath}span" class="files-tree-title" data-path="${dpath}" data-name="${filename}" onclick="getDirView(event)" data-path="${dpath}" data-name="${filename}">
<i class="far fa-folder text-info"></i>
<i class="far fa-folder-open text-info"></i>
${filename}
</span>
</input>
</input>
</div>
</li>
`;
} else {
text += `<li
class="d-block tree-ctx-item tree-file"
data-path="${dpath}"
data-name="${filename}"
onclick="clickOnFile(event)" id="${dpath}li"><input type='checkbox' class="checkBoxClass d-none file-check" name='root_path' value="${dpath}" ${checked}><span style="margin-right: 6px;">
<i class="far fa-file"></i></span></input>${filename}</li>`;
text += `
<li class="d-block tree-ctx-item tree-file" data-path="${dpath}" data-name="${filename}" id="${dpath}li">
<input type='checkbox' class="checkBoxClass d-none file-check" name='root_path' value="${dpath}" ${excluded}>
<input type='checkbox' id="${dpath}delBox" class="checkBoxClass file-check-deletion" name='root_path' value="${dpath}" onclick="updateFileCheckBox(event)" pdir="${pdir}" ${checked}>
<span style="margin-right: 6px;" onclick="clickOnFile(event)" data-path="${dpath}" data-name="${filename}" >
<i class="far fa-file"></i>
${filename}
</span>
</input>
</input>
</li>
`;
}
});
if (!response.data.root_path.top) {
@@ -762,9 +821,10 @@ end %} {% block content %}
}
} else {
try {
document.getElementById(path + "span").classList.add("tree-caret-down");
document.getElementById(path).innerHTML += text;
document.getElementById(path).classList.add("clicked");
const parentElement = document.getElementById(path);
parentElement.querySelector('.files-tree-title').classList.add("tree-caret-down");
parentElement.insertAdjacentHTML('beforeend', text);
parentElement.classList.add("clicked");
} catch {
console.log("Bad");
}
@@ -786,6 +846,9 @@ end %} {% block content %}
setTreeViewContext();
}, 1000);
}
function getNumberFilesChecked(){
const checkedBoxes = document.querySelectorAll('input.file-check:checked');
}
function getToggleMain(event) {
path = event.target.parentElement.getAttribute("data-path");
document.getElementById("files-tree").classList.toggle("d-block");
@@ -957,28 +1020,56 @@ end %} {% block content %}
function deleteFileE(event) {
path = event.target.parentElement.getAttribute("data-path");
name = event.target.parentElement.getAttribute("data-name");
bootbox.confirm({
size: "",
title:
"{% raw translate('serverFiles', 'deleteItemQuestion', data['lang']) %}",
closeButton: false,
message:
"{% raw translate('serverFiles', 'deleteItemQuestionMessage', data['lang']) %}",
buttons: {
confirm: {
label: "{{ translate('serverFiles', 'yesDelete', data['lang']) }}",
className: "btn-danger",
},
cancel: {
label: "{{ translate('serverFiles', 'noDelete', data['lang']) }}",
className: "btn-link",
},
},
callback: function (result) {
if (!result) return;
deleteItem(path);
},
files = {path}
renderDeleteConfimationModel(files)
}
function deleteMultiFileE(event) {
const filesToDelete = document.querySelectorAll('input.file-check-deletion:checked');
const files = [];
filesToDelete.forEach(fileInput => {
const parentElement = fileInput.parentElement;
const path = parentElement.getAttribute("data-path");
files.push(path);
});
renderDeleteConfimationModel(files)
}
function renderDeleteConfimationModel(files) {
path = renderDeleteList(files);
bootbox.confirm({
size: "",
title:
"{% raw translate('serverFiles', 'deleteItemQuestion', data['lang']) %}",
closeButton: false,
message:
"{% raw translate('serverFiles', 'deleteItemQuestionMessage', data['lang']) %}",
buttons: {
confirm: {
label: "{{ translate('serverFiles', 'yesDelete', data['lang']) }}",
className: "btn-danger",
},
cancel: {
label: "{{ translate('serverFiles', 'noDelete', data['lang']) }}",
className: "btn-link",
},
},
callback: function (result) {
if (!result) return;
deleteItems(files);
},
});
}
function renderDeleteList(files) {
list = "<ul>";
files.forEach(file => {
list += `<li> ${file} </li>`;
});
list += `</ul>`;
return list;
}
getTreeView($("#root_dir").data("path"));
@@ -1006,4 +1097,4 @@ end %} {% block content %}
}
</script>
<script src="../../static/assets/js/shared/upload.js"></script>
{% end %}
{% end %}

View File

@@ -521,7 +521,7 @@
"default": "Default",
"delete": "Delete",
"deleteItemQuestion": "Are you sure you want to delete \" + name + \"?",
"deleteItemQuestionMessage": "You are deleting \\\"\" + path + \"\\\"!<br/><br/>This action will be irreversible and it'll be lost forever!",
"deleteItemQuestionMessage": "You are deleting \" + path + \"<br/><br/>This action will be irreversible and it'll be lost forever!",
"download": "Download",
"editingFile": "Editing file",
"error": "Error while getting files",
@@ -807,4 +807,4 @@
"webhook_body": "Webhook Body",
"webhooks": "Webhooks"
}
}
}

View File

@@ -463,7 +463,7 @@
"default": "За замовчуванням",
"delete": "Видалити",
"deleteItemQuestion": "Ви впевнені що бажаєте видалити \" + name + \"?",
"deleteItemQuestionMessage": "Ви видаляєте \\\"\" + path + \"\\\"!<br/><br/>Це незворотня дія!",
"deleteItemQuestionMessage": "Ви видаляєте \" + path + \"<br/><br/>Це незворотня дія!",
"download": "Завантажити",
"editingFile": "Редагувати файл",
"error": "Помилка отримання файлів",

View File

@@ -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.5.2
sonar.projectVersion=4.5.3
sonar.python.version=3.9, 3.10, 3.11
sonar.exclusions=app/migrations/**, app/frontend/static/assets/vendors/**