From e2f4a51838db638ca0d652614136c462b02f9d9f Mon Sep 17 00:00:00 2001 From: Andrew-AAAA Date: Tue, 29 Sep 2026 08:46:28 +0700 Subject: [PATCH] feat: archive upload and date-folder endpoints (REQ-171) --- backend/api/projects.py | 27 +++++++++++++++++++++++++++ backend/library.py | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 66 insertions(+) diff --git a/backend/api/projects.py b/backend/api/projects.py index 14dbbb0..7280596 100644 --- a/backend/api/projects.py +++ b/backend/api/projects.py @@ -151,6 +151,33 @@ def list_library_date(project_id: int, date: str) -> dict: raise HTTPException(400, str(exc)) +@router.post("/{project_id}/library/dates") +def create_library_date(project_id: int, date: str) -> dict: + project = project_or_404(project_id) + try: + library.create_date(project["video_root"], date) + except library.LibraryConflict as exc: + raise HTTPException(409, str(exc)) + except library.LibraryError as exc: + raise HTTPException(400, str(exc)) + return {"date": date} + + +@router.post("/{project_id}/library/upload") +def upload_library_video(project_id: int, date: str, + file: UploadFile = File(...)) -> dict: + project = project_or_404(project_id) + try: + rel = library.upload_video( + project["video_root"], date, file.filename or "", + iter(lambda: file.file.read(1 << 20), b"")) + except library.LibraryConflict as exc: + raise HTTPException(409, str(exc)) + except library.LibraryError as exc: + raise HTTPException(400, str(exc)) + return {"rel": rel} + + @router.get("/{project_id}/video/info") def video_info(project_id: int, rel: str) -> dict: project = project_or_404(project_id) diff --git a/backend/library.py b/backend/library.py index ba3c127..ebc1bac 100644 --- a/backend/library.py +++ b/backend/library.py @@ -99,6 +99,45 @@ def list_videos(video_root: str, date: str, project_id: Optional[int] = None) -> return videos +class LibraryConflict(LibraryError): + pass + + +def create_date(video_root: str, date: str) -> str: + """Make a YYYY-MM-DD folder; never touch an existing one (REQ-171).""" + if not re.fullmatch(r"\d{4}-\d{2}-\d{2}", date): + raise LibraryError("Folder name must be YYYY-MM-DD") + folder = _safe_join(_effective_root(video_root), date) + if os.path.exists(folder): + raise LibraryConflict(f"Folder already exists: {date}") + os.makedirs(folder) + return folder + + +def upload_video(video_root: str, date: str, filename: str, chunks) -> str: + """Stream `chunks` into /; atomic via .part + rename.""" + name = os.path.basename(filename or "") + if not name or name != filename or not name.lower().endswith(video.VIDEO_EXTS): + raise LibraryError(f"Unsupported file: {filename}") + root = _effective_root(video_root) + if not os.path.isdir(_safe_join(root, date)): + raise LibraryError(f"No such date in the archive: {date}") + dest = _safe_join(root, date, name) + if os.path.exists(dest): + raise LibraryConflict(f"Already exists: {date}/{name}") + part = dest + ".part" + try: + with open(part, "wb") as handle: + for chunk in chunks: + handle.write(chunk) + os.replace(part, dest) + except BaseException: + if os.path.exists(part): + os.unlink(part) + raise + return f"{date}/{name}" + + import threading _conversion_queue = set()