mirror of
https://gitlab.com/crafty-controller/crafty-4.git
synced 2024-08-30 18:23:09 +00:00
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:
commit
b71b7cb1c4
@ -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))
|
||||
|
@ -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
|
||||
|
@ -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,
|
||||
|
@ -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):
|
||||
|
@ -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,
|
||||
|
308
app/classes/web/routes/api/crafty/upload/index.py
Normal file
308
app/classes/web/routes/api/crafty/upload/index.py
Normal 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"},
|
||||
},
|
||||
)
|
@ -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),
|
||||
|
@ -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)
|
208
app/frontend/static/assets/js/shared/upload.js
Normal file
208
app/frontend/static/assets/js/shared/upload.js
Normal 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%"> <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);
|
||||
});
|
||||
}
|
@ -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%"> <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 %}
|
@ -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,16 +765,13 @@
|
||||
});
|
||||
|
||||
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}:
|
||||
${file.name}:
|
||||
<br><div
|
||||
id="upload-progress-bar-${i + 1}"
|
||||
class="progress-bar progress-bar-striped progress-bar-animated"
|
||||
@ -879,33 +783,38 @@
|
||||
></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);
|
||||
}
|
||||
|
||||
|
||||
await Promise.all(uploadPromises);
|
||||
setTimeout(() => {
|
||||
hideUploadBox();
|
||||
//$('#upload_file').submit(); //.trigger('submit');
|
||||
}, 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 %}
|
@ -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%"> <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;
|
||||
|
@ -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%"> <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>
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user