Merge branch 'refactor/upload-api' into 'dev'

Chunked Uploads | Fix Upload Authentication

See merge request crafty-controller/crafty-4!762
This commit is contained in:
Iain Powrie 2024-07-09 02:32:51 +00:00
commit b71b7cb1c4
13 changed files with 630 additions and 632 deletions

View File

@ -4,6 +4,10 @@
TBD
### Refactor
- Backups | Allow multiple backup configurations ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/711))
- UploadAPI | Use Crafty's JWT authentication for file uploads ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/762))
- UploadAPI | Splice files on the frontend to allow chunked uploads as well as bulk uploads ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/762))
- UploadAPI | Enhance upload progress feedback on all upload pages ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/762))
- UploadAPI | Consolidate and improve speed on uploads, supporting 100mb+ uploads through Cloudflare(Free) ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/762))
### Bug fixes
- Fix zip imports so the root dir selection is functional ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/764))
- Fix bug where full access gives minimal access ([Merge Request](https://gitlab.com/crafty-controller/crafty-4/-/merge_requests/768))

View File

@ -4,6 +4,9 @@ import logging
import pathlib
import tempfile
import zipfile
import hashlib
from typing import BinaryIO
import mimetypes
from zipfile import ZipFile, ZIP_DEFLATED, ZIP_STORED
import urllib.request
import ssl
@ -22,6 +25,7 @@ class FileHelpers:
def __init__(self, helper):
self.helper: Helpers = helper
self.mime_types = mimetypes.MimeTypes()
@staticmethod
def ssl_get_file(
@ -142,6 +146,32 @@ class FileHelpers:
logger.error(f"Path specified is not a file or does not exist. {path}")
return e
def check_mime_types(self, file_path):
m_type, _value = self.mime_types.guess_type(file_path)
return m_type
@staticmethod
def calculate_file_hash(file_path: str) -> str:
"""
Takes one parameter of file path.
It will generate a SHA256 hash for the path and return it.
"""
sha256_hash = hashlib.sha256()
with open(file_path, "rb") as f:
for byte_block in iter(lambda: f.read(4096), b""):
sha256_hash.update(byte_block)
return sha256_hash.hexdigest()
@staticmethod
def calculate_buffer_hash(buffer: BinaryIO) -> str:
"""
Takes one argument of a stream buffer. Will return a
sha256 hash of the buffer
"""
sha256_hash = hashlib.sha256()
sha256_hash.update(buffer)
return sha256_hash.hexdigest()
@staticmethod
def copy_dir(src_path, dest_path, dirs_exist_ok=False):
# pylint: disable=unexpected-keyword-arg

View File

@ -508,7 +508,6 @@ class Helpers:
"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,

View File

@ -799,6 +799,18 @@ class TasksManager:
self.helper.ensure_dir_exists(
os.path.join(self.controller.project_root, "import", "upload")
)
self.helper.ensure_dir_exists(
os.path.join(self.controller.project_root, "temp")
)
for file in os.listdir(os.path.join(self.controller.project_root, "temp")):
if self.helper.is_file_older_than_x_days(
os.path.join(self.controller.project_root, "temp", file)
):
try:
os.remove(os.path.join(file))
except FileNotFoundError:
logger.debug("Could not clear out file from temp directory")
for file in os.listdir(
os.path.join(self.controller.project_root, "import", "upload")
):
@ -807,7 +819,7 @@ class TasksManager:
):
try:
os.remove(os.path.join(file))
except:
except FileNotFoundError:
logger.debug("Could not clear out file from import directory")
def log_watcher(self):

View File

@ -45,6 +45,7 @@ from app.classes.web.routes.api.servers.server.files import (
ApiServersServerFilesCreateHandler,
ApiServersServerFilesZipHandler,
)
from app.classes.web.routes.api.crafty.upload.index import ApiFilesUploadHandler
from app.classes.web.routes.api.servers.server.tasks.task.children import (
ApiServersServerTasksTaskChildrenHandler,
)
@ -238,6 +239,21 @@ def api_handlers(handler_args):
ApiServersServerFilesZipHandler,
handler_args,
),
(
r"/api/v2/crafty/admin/upload/?",
ApiFilesUploadHandler,
handler_args,
),
(
r"/api/v2/servers/import/upload/?",
ApiFilesUploadHandler,
handler_args,
),
(
r"/api/v2/servers/([a-z0-9-]+)/files/upload/?",
ApiFilesUploadHandler,
handler_args,
),
(
r"/api/v2/servers/([a-z0-9-]+)/files(?:/([a-zA-Z0-9-]+))?/?",
ApiServersServerFilesIndexHandler,

View File

@ -0,0 +1,308 @@
import os
import logging
import shutil
from app.classes.models.server_permissions import EnumPermissionsServer
from app.classes.shared.helpers import Helpers
from app.classes.web.base_api_handler import BaseApiHandler
logger = logging.getLogger(__name__)
IMAGE_MIME_TYPES = [
"image/bmp",
"image/cis-cod",
"image/gif",
"image/ief",
"image/jpeg",
"image/pipeg",
"image/svg+xml",
"image/tiff",
"image/x-cmu-raster",
"image/x-cmx",
"image/x-icon",
"image/x-portable-anymap",
"image/x-portable-bitmap",
"image/x-portable-graymap",
"image/x-portable-pixmap",
"image/x-rgb",
"image/x-xbitmap",
"image/x-xpixmap",
"image/x-xwindowdump",
"image/png",
"image/webp",
]
ARCHIVE_MIME_TYPES = ["application/zip"]
class ApiFilesUploadHandler(BaseApiHandler):
async def post(self, server_id=None):
auth_data = self.authenticate_user()
if not auth_data:
return
upload_type = self.request.headers.get("type")
accepted_types = []
if server_id:
# Check to make sure user is authorized for the server
if server_id not in [str(x["server_id"]) for x in auth_data[0]]:
# if the user doesn't have access to the server, return an error
return self.finish_json(
400, {"status": "error", "error": "NOT_AUTHORIZED"}
)
mask = self.controller.server_perms.get_lowest_api_perm_mask(
self.controller.server_perms.get_user_permissions_mask(
auth_data[4]["user_id"], server_id
),
auth_data[5],
)
# Make sure user has file access for the server
server_permissions = self.controller.server_perms.get_permissions(mask)
if EnumPermissionsServer.FILES not in server_permissions:
# if the user doesn't have Files permission, return an error
return self.finish_json(
400, {"status": "error", "error": "NOT_AUTHORIZED"}
)
u_type = "server_upload"
# Make sure user is a super user if they're changing panel settings
elif auth_data[4]["superuser"] and upload_type == "background":
u_type = "admin_config"
self.upload_dir = os.path.join(
self.controller.project_root,
"app/frontend/static/assets/images/auth/custom",
)
accepted_types = IMAGE_MIME_TYPES
elif upload_type == "import":
# Check that user can make servers
if (
not self.controller.crafty_perms.can_create_server(
auth_data[4]["user_id"]
)
and not auth_data[4]["superuser"]
):
return self.finish_json(
400,
{
"status": "error",
"error": "NOT_AUTHORIZED",
"data": {"message": ""},
},
)
# Set directory to upload import dir
self.upload_dir = os.path.join(
self.controller.project_root, "import", "upload"
)
u_type = "server_import"
accepted_types = ARCHIVE_MIME_TYPES
else:
return self.finish_json(
400,
{
"status": "error",
"error": "NOT_AUTHORIZED",
"data": {"message": ""},
},
)
# Get the headers from the request
self.chunk_hash = self.request.headers.get("chunkHash", 0)
self.file_id = self.request.headers.get("fileId")
self.chunked = self.request.headers.get("chunked", False)
self.filename = self.request.headers.get("fileName", None)
try:
file_size = int(self.request.headers.get("fileSize", None))
total_chunks = int(self.request.headers.get("totalChunks", 0))
except TypeError:
return self.finish_json(
400, {"status": "error", "error": "TYPE ERROR", "data": {}}
)
self.chunk_index = self.request.headers.get("chunkId")
if u_type == "server_upload":
self.upload_dir = self.request.headers.get("location", None)
self.temp_dir = os.path.join(self.controller.project_root, "temp", self.file_id)
if u_type == "server_upload":
# If this is an upload from a server the path will be what
# Is requested
full_path = os.path.join(self.upload_dir, self.filename)
# Check to make sure the requested path is inside the server's directory
if not self.helper.is_subdir(
full_path,
Helpers.get_os_understandable_path(
self.controller.servers.get_server_data_by_id(server_id)["path"]
),
):
return self.finish_json(
400,
{
"status": "error",
"error": "NOT AUTHORIZED",
"data": {"message": "Traversal detected"},
},
)
# Check to make sure the file type we're being sent is what we're expecting
if (
self.file_helper.check_mime_types(self.filename) not in accepted_types
and u_type != "server_upload"
):
return self.finish_json(
422,
{
"status": "error",
"error": "INVALID FILE TYPE",
"data": {
"message": f"Invalid File Type only accepts {accepted_types}"
},
},
)
_total, _used, free = shutil.disk_usage(self.upload_dir)
# Check to see if we have enough space
if free <= file_size:
return self.finish_json(
507,
{
"status": "error",
"error": "NO STORAGE SPACE",
"data": {"message": "Out Of Space!"},
},
)
# If this has no chunk index we know it's the inital request
if self.chunked and not self.chunk_index:
return self.finish_json(
200, {"status": "ok", "data": {"file-id": self.file_id}}
)
# Create the upload and temp directories if they don't exist
os.makedirs(self.upload_dir, exist_ok=True)
# Check for chunked header. We will handle this request differently
# if it doesn't exist
if not self.chunked:
# Write the file directly to the upload dir
with open(os.path.join(self.upload_dir, self.filename), "wb") as file:
chunk = self.request.body
if chunk:
file.write(chunk)
# We'll check the file hash against the sent hash once the file is
# written. We cannot check this buffer.
calculated_hash = self.file_helper.calculate_file_hash(
os.path.join(self.upload_dir, self.filename)
)
logger.info(
f"File upload completed. Filename: {self.filename} Type: {u_type}"
)
return self.finish_json(
200,
{
"status": "completed",
"data": {"message": "File uploaded successfully"},
},
)
# Since this is a chunked upload we'll create the temp dir for parts.
os.makedirs(self.temp_dir, exist_ok=True)
# Read headers and query parameters
content_length = int(self.request.headers.get("Content-Length"))
if content_length <= 0:
logger.error(
f"File upload failed. Filename: {self.filename}"
f"Type: {u_type} Error: INVALID CONTENT LENGTH"
)
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID CONTENT LENGTH",
"data": {"message": "Invalid content length"},
},
)
# At this point filename, chunk index and total chunks are required
# in the request
if not self.filename or self.chunk_index is None:
logger.error(
f"File upload failed. Filename: {self.filename}"
f"Type: {u_type} Error: CHUNK INDEX NOT FOUND"
)
return self.finish_json(
400,
{
"status": "error",
"error": "INDEX ERROR",
"data": {
"message": "Filename, chunk_index,"
" and total_chunks are required"
},
},
)
# Calculate the hash of the buffer and compare it against the expected hash
calculated_hash = self.file_helper.calculate_buffer_hash(self.request.body)
if str(self.chunk_hash) != str(calculated_hash):
logger.error(
f"File upload failed. Filename: {self.filename}"
f"Type: {u_type} Error: INVALID HASH"
)
return self.finish_json(
400,
{
"status": "error",
"error": "INVALID_HASH",
"data": {
"message": "Hash recieved does not match reported sent hash.",
"chunk_id": self.chunk_index,
},
},
)
# File paths
file_path = os.path.join(self.upload_dir, self.filename)
chunk_path = os.path.join(
self.temp_dir, f"{self.filename}.part{self.chunk_index}"
)
# Save the chunk
with open(chunk_path, "wb") as f:
f.write(self.request.body)
# Check if all chunks are received
received_chunks = [
f
for f in os.listdir(self.temp_dir)
if f.startswith(f"{self.filename}.part")
]
# When we've reached the total chunks we'll
# Compare the hash and write the file
if len(received_chunks) == total_chunks:
with open(file_path, "wb") as outfile:
for i in range(total_chunks):
chunk_file = os.path.join(self.temp_dir, f"{self.filename}.part{i}")
with open(chunk_file, "rb") as infile:
outfile.write(infile.read())
os.remove(chunk_file)
logger.info(
f"File upload completed. Filename: {self.filename}"
f" Path: {file_path} Type: {u_type}"
)
self.controller.management.add_to_audit_log(
auth_data[4]["user_id"],
f"Uploaded file {self.filename}",
server_id,
self.request.remote_ip,
)
self.finish_json(
200,
{
"status": "completed",
"data": {"message": "File uploaded successfully"},
},
)
else:
self.finish_json(
200,
{
"status": "partial",
"data": {"message": f"Chunk {self.chunk_index} received"},
},
)

View File

@ -24,7 +24,6 @@ from app.classes.web.routes.metrics.metrics_handlers import metrics_handlers
from app.classes.web.server_handler import ServerHandler
from app.classes.web.websocket_handler import WebSocketHandler
from app.classes.web.static_handler import CustomStaticHandler
from app.classes.web.upload_handler import UploadHandler
from app.classes.web.status_handler import StatusHandler
@ -142,7 +141,6 @@ class Webserver:
(r"/panel/(.*)", PanelHandler, handler_args),
(r"/server/(.*)", ServerHandler, handler_args),
(r"/ws", WebSocketHandler, handler_args),
(r"/upload", UploadHandler, handler_args),
(r"/status", StatusHandler, handler_args),
# API Routes V2
*api_handlers(handler_args),

View File

@ -1,331 +0,0 @@
import logging
import os
import time
import urllib.parse
import tornado.web
import tornado.options
import tornado.httpserver
from app.classes.models.crafty_permissions import EnumPermissionsCrafty
from app.classes.models.server_permissions import EnumPermissionsServer
from app.classes.shared.console import Console
from app.classes.shared.helpers import Helpers
from app.classes.shared.main_controller import Controller
from app.classes.web.base_handler import BaseHandler
from app.classes.shared.websocket_manager import WebSocketManager
logger = logging.getLogger(__name__)
@tornado.web.stream_request_body
class UploadHandler(BaseHandler):
# noinspection PyAttributeOutsideInit
def initialize(
self,
helper: Helpers = None,
controller: Controller = None,
tasks_manager=None,
translator=None,
file_helper=None,
):
self.helper = helper
self.controller = controller
self.tasks_manager = tasks_manager
self.translator = translator
self.file_helper = file_helper
def prepare(self):
# Class & Function Defination
api_key, _token_data, exec_user = self.current_user
self.upload_type = str(self.request.headers.get("X-Content-Upload-Type"))
if self.upload_type == "server_import":
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.full_access
user_id = exec_user["user_id"]
stream_size_value = self.helper.get_setting("stream_size_GB")
max_streamed_size = (1024 * 1024 * 1024) * stream_size_value
self.content_len = int(self.request.headers.get("Content-Length"))
if self.content_len > max_streamed_size:
logger.error(
f"User with ID {user_id} attempted to upload a file that"
f" exceeded the max body size."
)
return self.finish_json(
413,
{
"status": "error",
"error": "TOO LARGE",
"info": self.helper.translation.translate(
"error",
"fileTooLarge",
self.controller.users.get_user_lang_by_id(user_id),
),
},
)
self.do_upload = True
if superuser:
exec_user_server_permissions = (
self.controller.server_perms.list_defined_permissions()
)
elif api_key is not None:
exec_user_server_permissions = (
self.controller.crafty_perms.get_api_key_permissions_list(api_key)
)
else:
exec_user_server_permissions = (
self.controller.crafty_perms.get_crafty_permissions_list(
exec_user["user_id"]
)
)
if user_id is None:
logger.warning("User ID not found in upload handler call")
Console.warning("User ID not found in upload handler call")
self.do_upload = False
if (
EnumPermissionsCrafty.SERVER_CREATION
not in exec_user_server_permissions
and not exec_user["superuser"]
):
logger.warning(
f"User {user_id} tried to upload a server" " without permissions!"
)
Console.warning(
f"User {user_id} tried to upload a server" " without permissions!"
)
self.do_upload = False
path = os.path.join(self.controller.project_root, "import", "upload")
self.helper.ensure_dir_exists(path)
# Delete existing files
if len(os.listdir(path)) > 0:
for item in os.listdir():
try:
os.remove(os.path.join(path, item))
except:
logger.debug("Could not delete file on user server upload")
self.helper.ensure_dir_exists(path)
filename = urllib.parse.unquote(
self.request.headers.get("X-FileName", None)
)
if not str(filename).endswith(".zip"):
WebSocketManager().broadcast("close_upload_box", "error")
self.finish("error")
full_path = os.path.join(path, filename)
if self.do_upload:
try:
self.f = open(full_path, "wb")
except Exception as e:
logger.error(f"Upload failed with error: {e}")
self.do_upload = False
# If max_body_size is not set, you cannot upload files > 100MB
self.request.connection.set_max_body_size(max_streamed_size)
elif self.upload_type == "background":
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.full_access
user_id = exec_user["user_id"]
stream_size_value = self.helper.get_setting("stream_size_GB")
max_streamed_size = (1024 * 1024 * 1024) * stream_size_value
self.content_len = int(self.request.headers.get("Content-Length"))
if self.content_len > max_streamed_size:
logger.error(
f"User with ID {user_id} attempted to upload a file that"
f" exceeded the max body size."
)
return self.finish_json(
413,
{
"status": "error",
"error": "TOO LARGE",
"info": self.helper.translation.translate(
"error",
"fileTooLarge",
self.controller.users.get_user_lang_by_id(user_id),
),
},
)
self.do_upload = True
if not superuser:
return self.finish_json(
401,
{
"status": "error",
"error": "UNAUTHORIZED ACCESS",
"info": self.helper.translation.translate(
"error",
"superError",
self.controller.users.get_user_lang_by_id(user_id),
),
},
)
if not self.request.headers.get("X-Content-Type", None).startswith(
"image/"
):
return self.finish_json(
415,
{
"status": "error",
"error": "TYPE ERROR",
"info": self.helper.translation.translate(
"error",
"fileError",
self.controller.users.get_user_lang_by_id(user_id),
),
},
)
if user_id is None:
logger.warning("User ID not found in upload handler call")
Console.warning("User ID not found in upload handler call")
self.do_upload = False
path = os.path.join(
self.controller.project_root,
"app/frontend/static/assets/images/auth/custom",
)
filename = self.request.headers.get("X-FileName", None)
full_path = os.path.join(path, filename)
if self.do_upload:
try:
self.f = open(full_path, "wb")
except Exception as e:
logger.error(f"Upload failed with error: {e}")
self.do_upload = False
# If max_body_size is not set, you cannot upload files > 100MB
self.request.connection.set_max_body_size(max_streamed_size)
else:
server_id = self.get_argument("server_id", None)
superuser = exec_user["superuser"]
if api_key is not None:
superuser = superuser and api_key.full_access
user_id = exec_user["user_id"]
stream_size_value = self.helper.get_setting("stream_size_GB")
max_streamed_size = (1024 * 1024 * 1024) * stream_size_value
self.content_len = int(self.request.headers.get("Content-Length"))
if self.content_len > max_streamed_size:
logger.error(
f"User with ID {user_id} attempted to upload a file that"
f" exceeded the max body size."
)
return self.finish_json(
413,
{
"status": "error",
"error": "TOO LARGE",
"info": self.helper.translation.translate(
"error",
"fileTooLarge",
self.controller.users.get_user_lang_by_id(user_id),
),
},
)
self.do_upload = True
if superuser:
exec_user_server_permissions = (
self.controller.server_perms.list_defined_permissions()
)
elif api_key is not None:
exec_user_server_permissions = (
self.controller.server_perms.get_api_key_permissions_list(
api_key, server_id
)
)
else:
exec_user_server_permissions = (
self.controller.server_perms.get_user_id_permissions_list(
exec_user["user_id"], server_id
)
)
server_id = self.request.headers.get("X-ServerId", None)
if server_id is None:
logger.warning("Server ID not found in upload handler call")
Console.warning("Server ID not found in upload handler call")
self.do_upload = False
if user_id is None:
logger.warning("User ID not found in upload handler call")
Console.warning("User ID not found in upload handler call")
self.do_upload = False
if EnumPermissionsServer.FILES not in exec_user_server_permissions:
logger.warning(
f"User {user_id} tried to upload a file to "
f"{server_id} without permissions!"
)
Console.warning(
f"User {user_id} tried to upload a file to "
f"{server_id} without permissions!"
)
self.do_upload = False
path = self.request.headers.get("X-Path", None)
filename = self.request.headers.get("X-FileName", None)
full_path = os.path.join(path, filename)
if not self.helper.is_subdir(
full_path,
Helpers.get_os_understandable_path(
self.controller.servers.get_server_data_by_id(server_id)["path"]
),
):
logger.warning(
f"User {user_id} tried to upload a file to {server_id} "
f"but the path is not inside of the server!"
)
Console.warning(
f"User {user_id} tried to upload a file to {server_id} "
f"but the path is not inside of the server!"
)
self.do_upload = False
if self.do_upload:
try:
self.f = open(full_path, "wb")
except Exception as e:
logger.error(f"Upload failed with error: {e}")
self.do_upload = False
# If max_body_size is not set, you cannot upload files > 100MB
self.request.connection.set_max_body_size(max_streamed_size)
def post(self):
logger.info("Upload completed")
if self.upload_type == "server_files":
files_left = int(self.request.headers.get("X-Files-Left", None))
else:
files_left = 0
if self.do_upload:
time.sleep(5)
if files_left == 0:
WebSocketManager().broadcast("close_upload_box", "success")
self.finish("success") # Nope, I'm sending "success"
self.f.close()
else:
time.sleep(5)
if files_left == 0:
WebSocketManager().broadcast("close_upload_box", "error")
self.finish("error")
def data_received(self, chunk):
if self.do_upload:
self.f.write(chunk)

View File

@ -0,0 +1,208 @@
async function uploadFile(type, file = null, path = null, file_num = 0, _onProgress = null) {
if (file == null) {
try {
file = $("#file")[0].files[0];
} catch {
bootbox.alert("Please select a file first.")
return;
}
}
const fileId = uuidv4();
const token = getCookie("_xsrf");
if (type !== "server_upload") {
document.getElementById("upload_input").innerHTML = '<div class="progress" style="width: 100%;"><div id="upload-progress-bar" class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i class="fa-solid fa-spinner"></i></div></div>';
}
let url = ``
if (type === "server_upload") {
url = `/api/v2/servers/${serverId}/files/upload/`;
} else if (type === "background") {
url = `/api/v2/crafty/admin/upload/`
} else if (type === "import") {
url = `/api/v2/servers/import/upload/`
}
console.log(url)
const chunkSize = 1024 * 1024 * 10; // 10MB
const totalChunks = Math.ceil(file.size / chunkSize);
const uploadPromises = [];
let errors = []; // Array to store errors
try {
let res = await fetch(url, {
method: 'POST',
headers: {
'X-XSRFToken': token,
'chunked': true,
'fileSize': file.size,
'type': type,
'totalChunks': totalChunks,
'fileName': file.name,
'location': path,
'fileId': fileId,
},
body: null,
});
if (!res.ok) {
let errorResponse = await res.json();
throw new Error(JSON.stringify(errorResponse));
}
let responseData = await res.json();
if (responseData.status !== "ok") {
throw new Error(JSON.stringify(responseData));
}
for (let i = 0; i < totalChunks; i++) {
const start = i * chunkSize;
const end = Math.min(start + chunkSize, file.size);
const chunk = file.slice(start, end);
const chunk_hash = await calculateFileHash(chunk);
const uploadPromise = fetch(url, {
method: 'POST',
body: chunk,
headers: {
'Content-Range': `bytes ${start}-${end - 1}/${file.size}`,
'Content-Length': chunk.size,
'fileSize': file.size,
'chunkHash': chunk_hash,
'chunked': true,
'type': type,
'totalChunks': totalChunks,
'fileName': file.name,
'location': path,
'fileId': fileId,
'chunkId': i,
},
})
.then(async response => {
if (!response.ok) {
const errorData = await response.json();
throw new Error(JSON.stringify(errorData) || 'Unknown error occurred');
}
return response.json(); // Return the JSON data
})
.then(data => {
if (data.status !== "completed" && data.status !== "partial") {
throw new Error(data.message || 'Unknown error occurred');
}
// Update progress bar
const progress = (i + 1) / totalChunks * 100;
updateProgressBar(Math.round(progress), type, file_num);
})
.catch(error => {
errors.push(error); // Store the error
});
uploadPromises.push(uploadPromise);
}
await Promise.all(uploadPromises);
} catch (error) {
errors.push(error); // Store the error
}
if (errors.length > 0) {
const errorMessage = errors.map(error => JSON.parse(error.message).data.message || 'Unknown error occurred').join('<br>');
console.log(errorMessage)
bootbox.alert({
title: 'Error',
message: errorMessage,
callback: function () {
window.location.reload();
},
});
} else if (type !== "server_upload") {
// All promises resolved successfully
$("#upload_input").html(`<div class="card-header header-sm d-flex justify-content-between align-items-center" style="width: 100%;"><input value="${file.name}" type="text" id="file-uploaded" disabled></input> 🔒</div>`);
if (type === "import") {
document.getElementById("lower_half").style.visibility = "visible";
document.getElementById("lower_half").hidden = false;
} else if (type === "background") {
setTimeout(function () {
location.href = `/panel/custom_login`;
}, 2000);
}
} else {
let caught = false;
let expanded = false;
try {
expanded = document.getElementById(path).classList.contains("clicked");
} catch { }
let par_el;
let items;
try {
par_el = document.getElementById(path + "ul");
items = par_el.children;
} catch (err) {
console.log(err);
caught = true;
par_el = document.getElementById("files-tree");
items = par_el.children;
}
let name = file.name;
let full_path = path + '/' + name;
let flag = false;
for (let item of items) {
if ($(item).attr("data-name") === name) {
flag = true;
}
}
if (!flag) {
if (caught && !expanded) {
$(par_el).append(`<li id="${full_path}li" class="d-block tree-ctx-item tree-file tree-item" data-path="${full_path}" data-name="${name}" onclick="clickOnFile(event)"><span style="margin-right: 6px;"><i class="far fa-file"></i></span>${name}</li>`);
} else if (expanded) {
$(par_el).append(`<li id="${full_path}li" class="tree-ctx-item tree-file tree-item" data-path="${full_path}" data-name="${name}" onclick="clickOnFile(event)"><span style="margin-right: 6px;"><i class="far fa-file"></i></span>${name}</li>`);
}
setTreeViewContext();
}
$(`#upload-progress-bar-${file_num + 1}`).removeClass("progress-bar-striped");
$(`#upload-progress-bar-${file_num + 1}`).addClass("bg-success");
$(`#upload-progress-bar-${file_num + 1}`).html('<i style="color: black;" class="fas fa-box-check"></i>');
}
}
async function calculateFileHash(file) {
const arrayBuffer = await file.arrayBuffer();
const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
}
function updateProgressBar(progress, type, i) {
if (type !== "server_upload") {
if (progress === 100) {
$(`#upload-progress-bar`).removeClass("progress-bar-striped")
$(`#upload-progress-bar`).removeClass("progress-bar-animated")
}
$(`#upload-progress-bar`).css('width', progress + '%');
$(`#upload-progress-bar`).html(progress + '%');
} else {
if (progress === 100) {
$(`#upload-progress-bar-${i + 1}`).removeClass("progress-bar-striped")
$(`#upload-progress-bar-${i + 1}`).removeClass("progress-bar-animated")
}
$(`#upload-progress-bar-${i + 1}`).css('width', progress + '%');
$(`#upload-progress-bar-${i + 1}`).html(progress + '%');
}
}
function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
const r = Math.random() * 16 | 0,
v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}

View File

@ -69,7 +69,7 @@
</div>
<div class="input-group-append">
<button type="button" class="btn btn-info upload-button" id="upload-button"
onclick="sendFile()" disabled>UPLOAD</button>
onclick="uploadFile('background')" disabled>UPLOAD</button>
</div>
</div>
</div>
@ -381,61 +381,6 @@
}
img.src = src_path;
}
var file;
function sendFile() {
file = $("#file")[0].files[0]
document.getElementById("upload_input").innerHTML = '<div class="progress" style="width: 100%"><div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i class="fa-solid fa-spinner"></i></div></div>';
let xmlHttpRequest = new XMLHttpRequest();
let token = getCookie("_xsrf")
let fileName = file.name
let target = '/upload'
let mimeType = file.type
let size = file.size
let type = 'background'
xmlHttpRequest.upload.addEventListener('progress', function (e) {
if (e.loaded <= size) {
var percent = Math.round(e.loaded / size * 100);
$(`#upload-progress-bar`).css('width', percent + '%');
$(`#upload-progress-bar`).html(percent + '%');
}
});
xmlHttpRequest.open('POST', target, true);
xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType);
xmlHttpRequest.setRequestHeader('X-XSRFToken', token);
xmlHttpRequest.setRequestHeader('X-Content-Length', size);
xmlHttpRequest.setRequestHeader('X-Content-Disposition', 'attachment; filename="' + fileName + '"');
xmlHttpRequest.setRequestHeader('X-Content-Upload-Type', type);
xmlHttpRequest.setRequestHeader('X-FileName', fileName);
xmlHttpRequest.addEventListener('load', (event) => {
if (event.target.responseText == 'success') {
console.log('Upload for file', file.name, 'was successful!')
document.getElementById("upload_input").innerHTML = '<div class="card-header header-sm d-flex justify-content-between align-items-center" style="width: 100%"><span id="file-uploaded" style="color: gray;">' + fileName + '</span> 🔒</div>';
setTimeout(function () {
window.location.reload();
}, 2000);
}
else {
let response_text = JSON.parse(event.target.responseText);
var x = document.querySelector('.bootbox');
console.log(JSON.parse(event.target.responseText).info)
bootbox.alert({
message: JSON.parse(event.target.responseText).info,
callback: function () {
window.location.reload();
}
});
doUpload = false;
}
}, false);
xmlHttpRequest.addEventListener('error', (e) => {
console.error('Error while uploading file', file.name + '.', 'Event:', e)
}, false);
xmlHttpRequest.send(file);
}
</script>
<script src="../../static/assets/js/shared/upload.js"></script>
{% end %}

View File

@ -67,7 +67,8 @@
translate('serverFiles', 'download', data['lang']) }}</a>
<a onclick="deleteFileE(event)" href="javascript:void(0)" id="deleteFile" href="#"
style="color: red">{{ translate('serverFiles', 'delete', data['lang']) }}</a>
<a onclick="deleteFileE(event)" href="javascript:void(0)" id="deleteDir" href="#" style="color: red">{{
<a onclick="deleteFileE(event)" href="javascript:void(0)" id="deleteDir" href="#"
style="color: red">{{
translate('serverFiles', 'delete', data['lang']) }}</a>
<a href="javascript:void(0)" class="closebtn" style="color: var(--info);"
onclick="document.getElementById('files-tree-nav').style.display = 'none';">{{
@ -156,7 +157,8 @@
right: 35px;
}
}
.tree-file:hover{
.tree-file:hover {
cursor: pointer;
}
</style>
@ -721,105 +723,7 @@
}
}
async function sendFile(file, path, serverId, left, i, onProgress) {
let xmlHttpRequest = new XMLHttpRequest();
let token = getCookie("_xsrf")
let fileName = file.name
let target = '/upload?server_id=' + serverId
let mimeType = file.type
let size = file.size
xmlHttpRequest.upload.addEventListener('progress', function (e) {
if (e.loaded <= size) {
var percent = Math.round(e.loaded / size * 100);
$(`#upload-progress-bar-${i + 1}`).css('width', percent + '%');
$(`#upload-progress-bar-${i + 1}`).html(percent + '%');
}
});
xmlHttpRequest.open('POST', target, true);
xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType);
xmlHttpRequest.setRequestHeader('X-XSRFToken', token);
xmlHttpRequest.setRequestHeader('X-Content-Length', size);
xmlHttpRequest.setRequestHeader('X-Content-Disposition', 'attachment; filename="' + fileName + '"');
xmlHttpRequest.setRequestHeader('X-Path', path);
xmlHttpRequest.setRequestHeader('X-Content-Upload-Type', 'server_files')
xmlHttpRequest.setRequestHeader('X-Files-Left', left);
xmlHttpRequest.setRequestHeader('X-FileName', fileName);
xmlHttpRequest.setRequestHeader('X-ServerId', serverId);
xmlHttpRequest.upload.addEventListener('progress', (event) =>
onProgress(Math.floor(event.loaded / event.total * 100)), false);
xmlHttpRequest.addEventListener('load', (event) => {
if (event.target.responseText == 'success') {
console.log('Upload for file', file.name, 'was successful!');
let caught = false;
try {
if (document.getElementById(path).classList.contains("clicked")) {
var expanded = true;
}
} catch {
var expanded = false;
}
try {
var par_el = document.getElementById(path + "ul");
var items = par_el.children;
} catch (err) {
console.log(err)
caught = true;
var par_el = document.getElementById("files-tree");
var items = par_el.children;
}
let name = file.name;
console.log(par_el)
let full_path = path + '/' + name
let flag = false;
for (var k = 0; k < items.length; ++k) {
if ($(items[k]).attr("data-name") == name) {
flag = true;
}
}
if (!flag) {
if (caught && expanded == false) {
$(par_el).append('<li id=' + '"' + full_path.toString() + 'li' + '"' + 'class="d-block tree-ctx-item tree-file tree-item" data-path=' + '"' + full_path.toString() + '"' + ' data-name=' + '"' + name.toString() + '"' + ' onclick="clickOnFile(event)" ><span style="margin-right: 6px;"><i class="far fa-file"></i></span>' + name + '</li>');
} else if (expanded == true) {
$(par_el).append('<li id=' + '"' + full_path.toString() + 'li' + '"' + 'class="tree-ctx-item tree-file tree-item" data-path=' + '"' + full_path.toString() + '"' + ' data-name=' + '"' + name.toString() + '"' + ' onclick="clickOnFile(event)" ><span style="margin-right: 6px;"><i class="far fa-file"></i></span>' + name + '</li>');
}
setTreeViewContext();
}
$(`#upload-progress-bar-${i + 1}`).removeClass("progress-bar-striped");
$(`#upload-progress-bar-${i + 1}`).addClass("bg-success");
$(`#upload-progress-bar-${i + 1}`).html('<i style="color: black;" class="fas fa-box-check"></i>')
}
else {
let response_text = JSON.parse(event.target.responseText);
var x = document.querySelector('.bootbox');
if (x) {
x.remove()
}
var x = document.querySelector('.modal-content');
if (x) {
x.remove()
}
console.log(JSON.parse(event.target.responseText).info)
bootbox.alert({
message: JSON.parse(event.target.responseText).info,
callback: function () {
window.location.reload();
}
});
doUpload = false;
}
}, false);
xmlHttpRequest.addEventListener('error', (e) => {
console.error('Error while uploading file', file.name + '.', 'Event:', e)
}, false);
xmlHttpRequest.send(file);
}
let uploadWaitDialog;
let doUpload = true;
async function uploadFilesE(event) {
path = event.target.parentElement.getAttribute('data-path');
@ -842,6 +746,9 @@
label: "{{ translate('serverFiles', 'upload', data['lang']) }}",
className: "btn-default",
callback: async function () {
if ($("#files").get(0).files.length === 0) {
return hideUploadBox();
}
var height = files.files.length * 50;
var waitMessage = '<p class="text-center mb-0">' +
@ -858,54 +765,56 @@
});
let nFiles = files.files.length;
for (i = 0; i < nFiles; i++) {
if (!doUpload) {
doUpload = true;
hideUploadBox();
break;
}
const uploadPromises = [];
for (let i = 0; i < nFiles; i++) {
const file = files.files[i];
const progressHtml = `
<div style="width: 100%; min-width: 100%;">
${files.files[i].name}:
<br><div
id="upload-progress-bar-${i + 1}"
class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar"
style="width: 100%; height: 10px;"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="100"
></div>
</div><br>
`;
<div style="width: 100%; min-width: 100%;">
${file.name}:
<br><div
id="upload-progress-bar-${i + 1}"
class="progress-bar progress-bar-striped progress-bar-animated"
role="progressbar"
style="width: 100%; height: 10px;"
aria-valuenow="0"
aria-valuemin="0"
aria-valuemax="100"
></div>
</div><br>
`;
$('#upload-progress-bar-parent').append(progressHtml);
await sendFile(files.files[i], path, serverId, nFiles - i - 1, i, (progress) => {
const uploadPromise = uploadFile("server_upload", file, path, i, (progress) => {
$(`#upload-progress-bar-${i + 1}`).attr('aria-valuenow', progress)
$(`#upload-progress-bar-${i + 1}`).css('width', progress + '%');
});
uploadPromises.push(uploadPromise);
}
hideUploadBox();
//$('#upload_file').submit(); //.trigger('submit');
await Promise.all(uploadPromises);
setTimeout(() => {
hideUploadBox();
}, 2000);
}
}
}
});
var fileList = document.getElementById("files");
fileList.addEventListener("change", function (e) {
var list = "";
let files = Array.from(this.files)
files.forEach(file => {
list += "<li class='col-xs-12 file-list'>" + file.name + "</li>"
})
document.getElementById("fileList").innerHTML = list;
}, false);
});
}
async function calculateFileHash(file) {
const arrayBuffer = await file.arrayBuffer();
const hashBuffer = await crypto.subtle.digest('SHA-256', arrayBuffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
return hashHex;
}
function getDirView(event) {
let path = event.target.parentElement.getAttribute("data-path");
if (document.getElementById(path).classList.contains('clicked')) {
@ -1211,5 +1120,5 @@
</script>
<script src="../../static/assets/js/shared/upload.js"></script>
{% end %}

View File

@ -313,8 +313,8 @@
'labelZipFile', data['lang']) }}</label>
</div>
<div class="input-group-append">
<button type="button" class="btn btn-info upload-button" id="upload-button" onclick="sendFile()"
disabled>{{ translate('serverWizard',
<button type="button" class="btn btn-info upload-button" id="upload-button"
onclick="uploadFile('import')" disabled>{{ translate('serverWizard',
'uploadButton', data['lang']) }}</button>
</div>
</div>
@ -513,61 +513,8 @@
{% end %}
{% block js%}
<script src="../../static/assets/js/shared/upload.js"></script>
<script>
var upload;
var file;
function sendFile() {
file = $("#file")[0].files[0]
document.getElementById("upload_input").innerHTML = '<div class="progress" style="width: 100%;"><div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i class="fa-solid fa-spinner"></i></div></div>'
let xmlHttpRequest = new XMLHttpRequest();
let token = getCookie("_xsrf")
let fileName = encodeURIComponent(file.name)
let target = '/upload'
let mimeType = file.type
let size = file.size
let type = 'server_import'
xmlHttpRequest.upload.addEventListener('progress', function (e) {
if (e.loaded <= size) {
var percent = Math.round(e.loaded / size * 100);
$(`#upload-progress-bar`).css('width', percent + '%');
$(`#upload-progress-bar`).html(percent + '%');
}
});
xmlHttpRequest.open('POST', target, true);
xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType);
xmlHttpRequest.setRequestHeader('X-XSRFToken', token);
xmlHttpRequest.setRequestHeader('X-Content-Length', size);
xmlHttpRequest.setRequestHeader('X-Content-Disposition', 'attachment; filename="' + fileName + '"');
xmlHttpRequest.setRequestHeader('X-Content-Upload-Type', type);
xmlHttpRequest.setRequestHeader('X-FileName', fileName);
xmlHttpRequest.addEventListener('load', (event) => {
if (event.target.responseText == 'success') {
console.log('Upload for file', file.name, 'was successful!')
$("#upload_input").html(`<div class="card-header header-sm d-flex justify-content-between align-items-center" style="width: 100%;"><input value="${decodeURIComponent(fileName)}" type="text" id="file-uploaded" disabled></input> 🔒</div>`);
document.getElementById("lower_half").style.visibility = "visible";
}
else {
let response_text = JSON.parse(event.target.responseText);
var x = document.querySelector('.bootbox');
console.log(JSON.parse(event.target.responseText).info)
bootbox.alert({
message: JSON.parse(event.target.responseText).info,
callback: function () {
window.location.reload();
}
});
doUpload = false;
}
}, false);
xmlHttpRequest.addEventListener('error', (e) => {
console.error('Error while uploading file', file.name + '.', 'Event:', e)
}, false);
xmlHttpRequest.send(file);
}
document.getElementById("root_upload_button").addEventListener("click", function (event) {
if (file) {
upload = true;

View File

@ -509,8 +509,8 @@
'labelZipFile', data['lang']) }}</label>
</div>
<div class="input-group-append">
<button type="button" class="btn btn-info upload-button" id="upload-button" onclick="sendFile()"
disabled>{{ translate('serverWizard',
<button type="button" class="btn btn-info upload-button" id="upload-button"
onclick="uploadFile('import')" disabled>{{ translate('serverWizard',
'uploadButton', data['lang']) }}</button>
</div>
</div>
@ -805,6 +805,7 @@
{% end %}
{% block js %}
<script src="../../static/assets/js/shared/upload.js"></script>
<script>
document.getElementById("root_files_button").addEventListener("click", function (event) {
if (document.forms["zip"]["server_path"].value != "") {
@ -842,55 +843,7 @@
});
var upload = false;
var file;
function sendFile() {
file = $("#file")[0].files[0]
document.getElementById("upload_input").innerHTML = '<div class="progress" style="width: 100%;"><div class="progress-bar progress-bar-striped progress-bar-animated" role="progressbar" aria-valuenow="100" aria-valuemin="0" aria-valuemax="100" style="width: 100%">&nbsp;<i class="fa-solid fa-spinner"></i></div></div>'
let xmlHttpRequest = new XMLHttpRequest();
let token = getCookie("_xsrf")
let fileName = file.name
let target = '/upload'
let mimeType = file.type
let size = file.size
let type = 'server_import'
xmlHttpRequest.upload.addEventListener('progress', function (e) {
if (e.loaded <= size) {
var percent = Math.round(e.loaded / size * 100);
$(`#upload-progress-bar`).css('width', percent + '%');
$(`#upload-progress-bar`).html(percent + '%');
}
});
xmlHttpRequest.open('POST', target, true);
xmlHttpRequest.setRequestHeader('X-Content-Type', mimeType);
xmlHttpRequest.setRequestHeader('X-XSRFToken', token);
xmlHttpRequest.setRequestHeader('X-Content-Length', size);
xmlHttpRequest.setRequestHeader('X-Content-Disposition', 'attachment; filename="' + fileName + '"');
xmlHttpRequest.setRequestHeader('X-Content-Upload-Type', type);
xmlHttpRequest.setRequestHeader('X-FileName', fileName);
xmlHttpRequest.addEventListener('load', (event) => {
if (event.target.responseText == 'success') {
console.log('Upload for file', file.name, 'was successful!')
$("#upload_input").html(`<div class="card-header header-sm d-flex justify-content-between align-items-center" style="width: 100%;"><input value="${fileName}" type="text" id="file-uploaded" disabled></input> 🔒</div>`);
document.getElementById("lower_half").style.visibility = "visible";
document.getElementById("lower_half").hidden = false;
}
else {
console.log(JSON.parse(event.target.responseText).info)
bootbox.alert({
message: JSON.parse(event.target.responseText).info,
callback: function () {
window.location.reload();
}
});
}
}, false);
xmlHttpRequest.addEventListener('error', (e) => {
console.error('Error while uploading file', file.name + '.', 'Event:', e)
}, false);
xmlHttpRequest.send(file);
}
</script>
<script type="text/javascript" src="../../static/assets/js/shared/root-dir.js"></script>