taobin_image/main.py

320 lines
9.6 KiB
Python
Raw Normal View History

2026-05-08 10:41:29 +07:00
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
2026-05-08 10:41:29 +07:00
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"}
2026-05-08 10:41:29 +07:00
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}")
2026-05-08 10:41:29 +07:00
# ─────────────────────────────────────────
# UPLOAD
# ─────────────────────────────────────────
@app.post("/image/{folder}/upload/{uid}/{displayname}/{email}")
2026-05-08 10:41:29 +07:00
async def upload_images(
folder: str,
uid: str,
displayname: str,
email: str,
2026-05-08 10:41:29 +07:00
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}"
)
2026-05-08 10:41:29 +07:00
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
}
2026-05-08 10:41:29 +07:00
@app.post("/inter/{country}/image/{folder}/upload/{uid}/{displayname}/{email}")
2026-05-08 10:41:29 +07:00
async def upload_inter_images(
country: str,
folder: str,
uid: str,
displayname: str,
email: str,
2026-05-08 10:41:29 +07:00
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}"
)
2026-05-08 10:41:29 +07:00
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
}