mirror of
https://github.com/aaPanel/aaPanel.git
synced 2026-08-17 21:25:47 +02:00
211 lines
8.7 KiB
Python
211 lines
8.7 KiB
Python
# coding: utf-8
|
|
|
|
import os
|
|
import sys
|
|
|
|
if "/www/server/panel" not in sys.path:
|
|
sys.path.insert(0, "/www/server/panel")
|
|
if "/www/server/panel/class" not in sys.path:
|
|
sys.path.insert(0, "/www/server/panel/class")
|
|
|
|
import public
|
|
|
|
from mod.project.project_import.core.api_utils import error_response
|
|
from mod.project.project_import.core.constants import MAX_UPLOAD_SIZE, locks_dir, new_id
|
|
from mod.project.project_import.core.exceptions import ProjectImportError
|
|
from mod.project.project_import.core.json_store import file_lock
|
|
from mod.project.project_import.core.security import ensure_child_path
|
|
from mod.project.project_import.core.session_store import SessionStore
|
|
|
|
|
|
ALLOWED_ARCHIVES = (".zip", ".tar", ".tar.gz", ".tgz")
|
|
|
|
|
|
class main:
|
|
def __init__(self):
|
|
self.sessions = SessionStore()
|
|
|
|
def upload(self, get):
|
|
"""Wrap files.upload with a session-scoped fixed destination."""
|
|
try:
|
|
from BTPanel import request
|
|
from files import files as PanelFiles
|
|
|
|
file_name = os.path.basename(str(get.get("f_name", "")).strip())
|
|
file_size = int(get.get("f_size", 0) or 0)
|
|
offset = int(get.get("f_start", 0) or 0)
|
|
if not file_name or not file_name.lower().endswith(ALLOWED_ARCHIVES):
|
|
raise ProjectImportError("Unsupported archive format", "UNSUPPORTED_ARCHIVE")
|
|
if file_size <= 0 or file_size > MAX_UPLOAD_SIZE:
|
|
raise ProjectImportError("Invalid upload file size", "UPLOAD_SIZE_INVALID")
|
|
if offset < 0 or offset > file_size:
|
|
raise ProjectImportError("Invalid upload offset", "UPLOAD_OFFSET_INVALID")
|
|
if not request.files.getlist("blob"):
|
|
raise ProjectImportError("Upload chunk is missing", "UPLOAD_CHUNK_REQUIRED")
|
|
|
|
session_id = str(get.get("session_id", "")).strip()
|
|
if session_id:
|
|
session = self.sessions.get(session_id)
|
|
if session.get("source_type") != "archive":
|
|
raise ProjectImportError("This session is not an archive session", "INVALID_SOURCE_TYPE")
|
|
else:
|
|
if offset != 0:
|
|
raise ProjectImportError(
|
|
"session_id is required after the first upload chunk",
|
|
"UPLOAD_SESSION_REQUIRED",
|
|
)
|
|
session = self.sessions.create("archive")
|
|
session_id = session["session_id"]
|
|
|
|
upload_dir = os.path.join(self.sessions.work_dir(session_id), "upload")
|
|
os.makedirs(upload_dir, mode=0o700, exist_ok=True)
|
|
temporary_path = ensure_child_path(
|
|
upload_dir,
|
|
os.path.join(upload_dir, file_name + "." + str(file_size) + ".upload.tmp"),
|
|
)
|
|
final_path = ensure_child_path(upload_dir, os.path.join(upload_dir, file_name))
|
|
lock_path = os.path.join(locks_dir(), session_id + ".upload.lock")
|
|
|
|
with file_lock(lock_path):
|
|
session = self.sessions.get(session_id)
|
|
upload = session.get("upload", {})
|
|
same_file = (
|
|
upload.get("file_name") == file_name
|
|
and int(upload.get("file_size", 0) or 0) == file_size
|
|
)
|
|
if (
|
|
same_file
|
|
and upload.get("completed")
|
|
and os.path.isfile(final_path)
|
|
and os.path.getsize(final_path) == file_size
|
|
and offset == file_size
|
|
):
|
|
return public.return_message(0, 0, self._result(session_id, upload))
|
|
|
|
if not same_file or upload.get("completed"):
|
|
if offset != 0:
|
|
expected = file_size if same_file and upload.get("completed") else 0
|
|
raise ProjectImportError(
|
|
"Unexpected upload offset; expected {}".format(expected),
|
|
"UPLOAD_OFFSET_MISMATCH",
|
|
)
|
|
self._remove_previous_upload(upload_dir, upload)
|
|
upload = {
|
|
"upload_id": new_id("piu"),
|
|
"file_name": file_name,
|
|
"file_size": file_size,
|
|
"uploaded_size": 0,
|
|
"completed": False,
|
|
"part_path": temporary_path,
|
|
"path": final_path,
|
|
}
|
|
|
|
def initialize(data):
|
|
data["upload"] = dict(upload)
|
|
data["status"] = "uploading"
|
|
return data
|
|
|
|
self.sessions.update(session_id, initialize)
|
|
|
|
current_size = os.path.getsize(temporary_path) if os.path.isfile(temporary_path) else 0
|
|
if current_size != offset:
|
|
raise ProjectImportError(
|
|
"Unexpected upload offset; expected {}".format(current_size),
|
|
"UPLOAD_OFFSET_MISMATCH",
|
|
)
|
|
|
|
args = public.to_dict_obj({
|
|
"f_path": upload_dir,
|
|
"f_name": file_name,
|
|
"f_size": file_size,
|
|
"f_start": offset,
|
|
})
|
|
try:
|
|
upstream = PanelFiles().upload(args)
|
|
except Exception:
|
|
self._rollback_partial(temporary_path, current_size)
|
|
raise
|
|
|
|
completed = os.path.isfile(final_path) and os.path.getsize(final_path) == file_size
|
|
if completed:
|
|
uploaded_size = file_size
|
|
else:
|
|
uploaded_size = os.path.getsize(temporary_path) if os.path.isfile(temporary_path) else 0
|
|
if uploaded_size > file_size:
|
|
self._rollback_partial(temporary_path, current_size)
|
|
raise ProjectImportError(
|
|
"Uploaded data exceeds declared size",
|
|
"UPLOAD_SIZE_EXCEEDED",
|
|
)
|
|
if type(upstream) is not int:
|
|
self._rollback_partial(temporary_path, current_size)
|
|
raise ProjectImportError(
|
|
self._upstream_error(upstream),
|
|
"FILE_UPLOAD_FAILED",
|
|
)
|
|
|
|
def save_progress(data):
|
|
data["upload"] = dict(upload)
|
|
data["upload"]["uploaded_size"] = uploaded_size
|
|
data["upload"]["completed"] = completed
|
|
if completed:
|
|
data["upload"].pop("part_path", None)
|
|
data["status"] = "created"
|
|
else:
|
|
data["status"] = "uploading"
|
|
return data
|
|
|
|
saved = self.sessions.update(session_id, save_progress)
|
|
return public.return_message(0, 0, self._result(session_id, saved["upload"]))
|
|
except Exception as exc:
|
|
return error_response(exc)
|
|
|
|
@staticmethod
|
|
def _result(session_id, upload):
|
|
uploaded_size = int(upload.get("uploaded_size", 0) or 0)
|
|
return {
|
|
"session_id": session_id,
|
|
"upload_id": upload.get("upload_id", ""),
|
|
"file_name": upload.get("file_name", ""),
|
|
"file_size": int(upload.get("file_size", 0) or 0),
|
|
"uploaded_size": uploaded_size,
|
|
"next_start": uploaded_size,
|
|
"completed": bool(upload.get("completed")),
|
|
}
|
|
|
|
@staticmethod
|
|
def _rollback_partial(path, size):
|
|
if not os.path.isfile(path):
|
|
return
|
|
try:
|
|
with open(path, "r+b") as handle:
|
|
handle.truncate(max(0, int(size)))
|
|
except OSError:
|
|
pass
|
|
|
|
@staticmethod
|
|
def _upstream_error(result):
|
|
if isinstance(result, dict):
|
|
message = result.get("message", result.get("msg", result.get("result", "")))
|
|
if isinstance(message, dict):
|
|
message = message.get("result", message.get("error", ""))
|
|
if message:
|
|
return str(message)
|
|
return "File upload failed"
|
|
|
|
@staticmethod
|
|
def _remove_previous_upload(upload_dir, upload):
|
|
for field in ("part_path", "path"):
|
|
path = str(upload.get(field, ""))
|
|
if not path:
|
|
continue
|
|
try:
|
|
path = ensure_child_path(upload_dir, path)
|
|
except ProjectImportError:
|
|
continue
|
|
if os.path.isfile(path):
|
|
try:
|
|
os.remove(path)
|
|
except OSError:
|
|
pass
|