Remove VM upload layer; keep file picker only
This commit is contained in:
@@ -1,15 +0,0 @@
|
||||
"""Переиспользуемый backend-слой закачки через ВМ."""
|
||||
|
||||
from .blueprint import create_upload_refs_blueprint
|
||||
from .config import PULL_RETRIES, PULL_RETRY_DELAY, VM_UPLOAD_PREFIX
|
||||
from .pull_file import pull_file
|
||||
from .safe_name import safe_name
|
||||
|
||||
__all__ = [
|
||||
"create_upload_refs_blueprint",
|
||||
"safe_name",
|
||||
"pull_file",
|
||||
"PULL_RETRIES",
|
||||
"PULL_RETRY_DELAY",
|
||||
"VM_UPLOAD_PREFIX",
|
||||
]
|
||||
@@ -1,79 +0,0 @@
|
||||
"""Blueprint слоя закачки ссылок через ВМ-буфер."""
|
||||
|
||||
import logging
|
||||
|
||||
import requests
|
||||
from flask import Blueprint, jsonify, request
|
||||
|
||||
from ..session import (MAX_FILE_BYTES, add_file, configure, create_session,
|
||||
file_count, get_files)
|
||||
from .config import PULL_RETRIES, PULL_RETRY_DELAY, VM_UPLOAD_PREFIX
|
||||
from .pull_file import pull_file
|
||||
from .safe_name import safe_name
|
||||
|
||||
|
||||
log = logging.getLogger("upload.upload_refs")
|
||||
|
||||
|
||||
def create_upload_refs_blueprint(cfg: dict) -> Blueprint:
|
||||
"""Создать Blueprint с POST ``/upload_refs``."""
|
||||
|
||||
cfg = cfg or {}
|
||||
prefix = cfg.get("apiPrefix", "/api")
|
||||
vm_prefix = cfg.get("vmUploadUrl", VM_UPLOAD_PREFIX)
|
||||
max_file_bytes = cfg.get("maxFileBytes", MAX_FILE_BYTES)
|
||||
pull_retries = cfg.get("pullRetries", PULL_RETRIES)
|
||||
pull_delay = cfg.get("pullRetryDelay", PULL_RETRY_DELAY)
|
||||
pull_timeout = cfg.get("pullTimeout", 120)
|
||||
configure(max_session_bytes=cfg.get("maxSessionBytes"))
|
||||
|
||||
blueprint = Blueprint("upload_refs", __name__, url_prefix=prefix)
|
||||
|
||||
@blueprint.route("/upload_refs", methods=["POST"])
|
||||
def upload_refs():
|
||||
data = request.get_json(silent=True) or {}
|
||||
sid = data.get("session") or create_session()
|
||||
refs = data.get("files") or []
|
||||
if not isinstance(refs, list) or not refs:
|
||||
return jsonify({"ok": False, "error": "No files"}), 400
|
||||
|
||||
added = 0
|
||||
try:
|
||||
with requests.Session() as client:
|
||||
for ref in refs:
|
||||
if not isinstance(ref, dict):
|
||||
continue
|
||||
name = safe_name(ref.get("name", ""))
|
||||
url = ref.get("url")
|
||||
if not name or not isinstance(url, str) or not url.startswith(vm_prefix):
|
||||
continue
|
||||
if (ref.get("size") or 0) > max_file_bytes:
|
||||
_delete(client, url)
|
||||
continue
|
||||
content = pull_file(
|
||||
client, url, max_file_bytes, pull_retries, pull_delay,
|
||||
pull_timeout, sid=sid, name=name,
|
||||
)
|
||||
if not add_file(sid, name, content):
|
||||
if get_files(sid) is None:
|
||||
return jsonify({"ok": False, "error": "Session not found"}), 404
|
||||
_delete(client, url)
|
||||
continue
|
||||
_delete(client, url)
|
||||
added += 1
|
||||
except Exception as error:
|
||||
log.error("upload_refs: pull error sid=%s: %r", sid, error)
|
||||
return jsonify({"ok": False, "error": "Pull failed: %s" % error}), 502
|
||||
|
||||
return jsonify({"ok": True, "session": sid, "count": file_count(sid)})
|
||||
|
||||
return blueprint
|
||||
|
||||
|
||||
def _delete(client, url: str):
|
||||
"""Удалить временный объект на ВМ, не ломая основной запрос при сбое."""
|
||||
|
||||
try:
|
||||
client.delete(url, timeout=10)
|
||||
except Exception:
|
||||
log.warning("upload_refs: could not delete VM object url=%r", url)
|
||||
@@ -1,5 +0,0 @@
|
||||
"""Значения по умолчанию для pull из ВМ-буфера."""
|
||||
|
||||
PULL_RETRIES = 3
|
||||
PULL_RETRY_DELAY = 2
|
||||
VM_UPLOAD_PREFIX = "https://example.invalid/upload/"
|
||||
@@ -1,38 +0,0 @@
|
||||
"""Исходящий pull файла из ВМ-буфера с ретраями и лимитом размера."""
|
||||
|
||||
import logging
|
||||
import time
|
||||
|
||||
from .config import PULL_RETRIES, PULL_RETRY_DELAY
|
||||
|
||||
|
||||
log = logging.getLogger("upload.upload_refs.pull")
|
||||
|
||||
|
||||
def pull_file(client, url: str, max_bytes: int,
|
||||
retries: int = PULL_RETRIES, delay: float = PULL_RETRY_DELAY,
|
||||
timeout: int = 120, sid: str = None, name: str = None) -> bytes:
|
||||
"""Забрать файл потоково, не принимая тело больше ``max_bytes``."""
|
||||
|
||||
last_error = None
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
response = client.get(url, stream=True, timeout=timeout)
|
||||
response.raise_for_status()
|
||||
chunks = []
|
||||
total = 0
|
||||
for chunk in response.iter_content(chunk_size=1024 * 1024):
|
||||
if not chunk:
|
||||
continue
|
||||
total += len(chunk)
|
||||
if total > max_bytes:
|
||||
raise ValueError("file exceeds maxFileBytes")
|
||||
chunks.append(chunk)
|
||||
return b"".join(chunks)
|
||||
except Exception as error:
|
||||
last_error = error
|
||||
log.warning("pull: attempt %d/%d failed sid=%s file=%r: %r",
|
||||
attempt + 1, retries, sid, name, error)
|
||||
if attempt + 1 < retries:
|
||||
time.sleep(delay)
|
||||
raise last_error if last_error else RuntimeError("pull failed")
|
||||
@@ -1,13 +0,0 @@
|
||||
"""Санитизация имени файла с сохранением подпапок."""
|
||||
|
||||
|
||||
def safe_name(name: str) -> str:
|
||||
"""Вернуть безопасное относительное имя или пустую строку."""
|
||||
|
||||
if not isinstance(name, str) or not name:
|
||||
return ""
|
||||
parts = [part for part in name.replace("\\", "/").split("/")
|
||||
if part and part != "."]
|
||||
if not parts or any(part == ".." for part in parts):
|
||||
return ""
|
||||
return "/".join(parts)
|
||||
Reference in New Issue
Block a user