from fastapi import FastAPI, UploadFile, File, HTTPException, Query from fastapi.responses import FileResponse from pathlib import Path from typing import Optional import shutil import uuid import os import httpx app = FastAPI() BASE_DIR = Path("/usr/src/app/taobin_project") # BASE_DIR = Path("/taobin_project") SERVICE_NAME = os.getenv("SERVICE_NAME") GIT_REPO_SERVER_URL = os.getenv("GIT_REPO_SERVER_URL") FRONTEND_NOTIFY_URL = os.getenv("FRONTEND_NOTIFY_URL") ALLOWED_FOLDERS = {"page_drink", "page_drink_disable", "page_drink_disable_n", "page_drink_disable_n2", "page_drink_n", "page_drink_picture2_n", "page_drink_press", "page_drink_press_n", "page_drink_select"} ALLOWED_EXTENSIONS = {".jpg", ".jpeg", ".png", ".gif", ".webp"} def validate_folder(folder: str): if folder not in ALLOWED_FOLDERS: raise HTTPException(400, f"folder must be {ALLOWED_FOLDERS}") def validate_ext(filename: str): ext = Path(filename).suffix.lower() if ext not in ALLOWED_EXTENSIONS: raise HTTPException(400, f"file not allow {ext}") return ext def get_image_dir(folder: str, country: Optional[str] = None) -> Path: """ no country → /taobin_project/image/{folder}/ has country → /taobin_project/inter/{country}/image/{folder}/ """ if country: path = BASE_DIR / "inter" / country / "image" / folder else: path = BASE_DIR / "image" / folder path.mkdir(parents=True, exist_ok=True) return path async def commit_files_to_git( files: list[UploadFile], folder: str, display_name: str, email: str, country: str, git_server_url: str = GIT_REPO_SERVER_URL ) -> dict: """ Commit file(s) to Git repository server via multipart form POST. Args: files: List of UploadFile objects (file pointers will be reset) folder: Folder name used in commit message display_name: Git signature username email: Git signature email git_server_url: Target Git repo server endpoint Returns: Response JSON from Git server """ commit_data = { "signature_username": display_name, "signature_email": email, "message": f"commit {folder} {country}" } multipart_files = {} if len(files) == 1: file_obj = files[0] file_obj.file.seek(0) commit_data["path"] = Path(file_obj.filename).name multipart_files["file"] = ( Path(file_obj.filename).name, file_obj.file, "application/octet-stream" ) else: for idx, file_obj in enumerate(files, start=1): file_obj.file.seek(0) commit_data[f"path{idx}"] = Path(file_obj.filename).name multipart_files[f"file{idx}"] = ( Path(file_obj.filename).name, file_obj.file, "application/octet-stream" ) # return { # "git_url": git_server_url, # "data": commit_data, # "file": multipart_files # } async with httpx.AsyncClient() as client: response = await client.post( git_server_url + "/commit", data=commit_data, files=multipart_files, timeout=30.0 ) response.raise_for_status() return response.json() def backup_existing_file(dest: Path) -> Optional[Path]: if dest.exists(): backup_path = dest.with_suffix(dest.suffix + f".bak.{uuid.uuid4().hex[:8]}") shutil.copy2(dest, backup_path) print(f"[BACKUP] {dest.name} → {backup_path.name}") return backup_path return None def rollback_files(saved_files: list[dict], backups: dict[str, Optional[Path]], folder: str, country: Optional[str] = None): for item in saved_files: filename = item["filename"] dest = get_image_dir(folder, country) / filename backup = backups.get(filename) try: if backup and backup.exists(): if dest.exists(): dest.unlink() shutil.move(str(backup), str(dest)) print(f"[ROLLBACK] Restored: {filename}") else: if dest.exists(): dest.unlink() print(f"[ROLLBACK] Deleted new file: {filename}") except Exception as e: print(f"[ROLLBACK ERROR] {filename}: {e}") def cleanup_backups(backups: dict[str, Optional[Path]]): for filename, backup in backups.items(): if backup and backup.exists(): try: backup.unlink() except Exception as e: print(f"[CLEANUP ERROR] {backup}: {e}") async def notify_frontend(uid: str, msg: str): if not FRONTEND_NOTIFY_URL: print(f"[NOTIFY SKIP] FRONTEND_NOTIFY_URL not set") return payload = { "type": "notify", "payload": { "from": SERVICE_NAME, "level": "error", "to": uid, "msg": msg } } try: async with httpx.AsyncClient() as client: await client.post(FRONTEND_NOTIFY_URL, json=payload, timeout=10.0) print(f"[NOTIFY SEND SUCCESS] {payload}") except Exception as e: print(f"[NOTIFY ERROR] Failed to notify frontend: {e}") # ───────────────────────────────────────── # UPLOAD # ───────────────────────────────────────── @app.post("/image/{folder}/upload/{uid}/{displayname}/{email}") async def upload_images( folder: str, uid: str, displayname: str, email: str, files: list[UploadFile] = File(...) ): required_user_fields = { "uid": uid, "displayname": displayname, "email": email, "country": "tha" } if not (required_user_fields.get("country")): raise HTTPException(status_code=400, detail="Invalid country") for field, value in required_user_fields.items(): if not str(value).strip(): raise HTTPException( status_code=400, detail=f"Missing or empty user_info.{field}" ) validate_folder(folder) saved = [] backups: dict[str, Optional[Path]] = {} try: for file in files: ext = validate_ext(file.filename) filename = Path(file.filename).name dest = get_image_dir(folder) / filename backups[filename] = backup_existing_file(dest) with open(dest, "wb") as f: shutil.copyfileobj(file.file, f) saved.append({ "filename": filename, "url": f"/image/{folder}/{filename}" }) except Exception as e: rollback_files(saved, backups, folder) error_msg = f"Save image failed: {str(e)}" await notify_frontend(uid=uid, msg=error_msg) raise HTTPException(status_code=500, detail=error_msg) try: git_response = await commit_files_to_git( files=files, folder=folder, display_name=displayname, email=email, country=required_user_fields.get("country") ) except Exception as e: rollback_files(saved, backups, folder) error_msg = f"Git commit failed: {str(e)}" await notify_frontend(uid=uid, msg=error_msg) raise HTTPException(status_code=502, detail=f"{error_msg}, files rolled back") cleanup_backups(backups) return { "uploaded": saved, "git_commit": git_response } @app.post("/inter/{country}/image/{folder}/upload/{uid}/{displayname}/{email}") async def upload_inter_images( country: str, folder: str, uid: str, displayname: str, email: str, files: list[UploadFile] = File(...) ): required_user_fields = { "uid": uid, "displayname": displayname, "email": email, "country": country } if not (country): raise HTTPException(status_code=400, detail="Invalid country") for field, value in required_user_fields.items(): if not str(value).strip(): raise HTTPException( status_code=400, detail=f"Missing or empty user_info.{field}" ) validate_folder(folder) saved = [] backups: dict[str, Optional[Path]] = {} try: for file in files: ext = validate_ext(file.filename) filename = Path(file.filename).name dest = get_image_dir(folder, country) / filename backups[filename] = backup_existing_file(dest) with open(dest, "wb") as f: shutil.copyfileobj(file.file, f) saved.append({ "filename": filename, "url": f"/inter/{country}/image/{folder}/{filename}" }) except Exception as e: rollback_files(saved, backups, folder, country) error_msg = f"Save image failed: {str(e)}" await notify_frontend(uid=uid, msg=error_msg) raise HTTPException(status_code=500, detail=error_msg) try: git_response = await commit_files_to_git( files=files, folder=folder, display_name=displayname, email=email, country=country ) except Exception as e: rollback_files(saved, backups, folder, country) error_msg = f"Git commit failed: {str(e)}" await notify_frontend(uid=uid, msg=error_msg) raise HTTPException(status_code=502, detail=f"{error_msg}, files rolled back") cleanup_backups(backups) return { "uploaded": saved, "git_commit": git_response }