taobin_image/main.py

320 lines
No EOL
9.6 KiB
Python

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
}