|
| 1 | +from fastapi import FastAPI, UploadFile, File, HTTPException, Header, Depends |
| 2 | +from fastapi.responses import FileResponse |
| 3 | +from fastapi.middleware.cors import CORSMiddleware |
| 4 | +from sqlmodel import SQLModel, create_engine, Session, select |
| 5 | +from urllib.parse import quote |
| 6 | +from pathlib import Path |
| 7 | +from app.config import API_KEY, DB_URL, UPLOAD_DIR, CORS_ORIGINS, DB_CONNECT_ARGS, ENABLE_CLEANER |
| 8 | +from app.models import File as FileModel |
| 9 | +from app.storage import save_file |
| 10 | +from app.cleaner import start_cleaner |
| 11 | + |
| 12 | +app = FastAPI(title="AlterBase CDN API", version="1.0") |
| 13 | + |
| 14 | +# CORS |
| 15 | +origins = [o.strip() for o in CORS_ORIGINS.split(",")] |
| 16 | +app.add_middleware( |
| 17 | + CORSMiddleware, |
| 18 | + allow_origins=origins, |
| 19 | + allow_methods=["*"], |
| 20 | + allow_headers=["*"], |
| 21 | +) |
| 22 | + |
| 23 | +engine = create_engine(DB_URL, connect_args=DB_CONNECT_ARGS) |
| 24 | +SQLModel.metadata.create_all(engine) |
| 25 | +if ENABLE_CLEANER: |
| 26 | + start_cleaner(engine) # background scheduler |
| 27 | + |
| 28 | +# --- API Key guard --- |
| 29 | +async def require_api_key(x_api_key: str | None = Header(default=None)): |
| 30 | + if API_KEY and x_api_key != API_KEY: |
| 31 | + raise HTTPException(status_code=401, detail="Invalid or missing API Key") |
| 32 | + |
| 33 | +# --- Routes --- |
| 34 | + |
| 35 | +@app.post("/upload", dependencies=[Depends(require_api_key)]) |
| 36 | +async def upload(file: UploadFile = File(...)): |
| 37 | + if not file.filename: |
| 38 | + raise HTTPException(status_code=400, detail="Missing filename") |
| 39 | + data = await file.read() |
| 40 | + stored_name, size_bytes = save_file(data, file.filename, file.content_type or "application/octet-stream") |
| 41 | + file_id = stored_name.split(".")[0] |
| 42 | + |
| 43 | + with Session(engine) as session: |
| 44 | + rec = FileModel( |
| 45 | + id=file_id, |
| 46 | + original_name=file.filename, |
| 47 | + stored_name=stored_name, |
| 48 | + content_type=file.content_type or "application/octet-stream", |
| 49 | + size_bytes=size_bytes, |
| 50 | + ) |
| 51 | + session.add(rec) |
| 52 | + session.commit() |
| 53 | + |
| 54 | + return { |
| 55 | + "id": file_id, |
| 56 | + "url": f"/{quote(stored_name)}", |
| 57 | + "size": size_bytes, |
| 58 | + "type": file.content_type, |
| 59 | + } |
| 60 | + |
| 61 | +@app.get("/list", dependencies=[Depends(require_api_key)]) |
| 62 | +def list_files(): |
| 63 | + with Session(engine) as session: |
| 64 | + files = session.exec(select(FileModel).order_by(FileModel.created_at.desc())).all() |
| 65 | + return [ |
| 66 | + { |
| 67 | + "id": f.id, |
| 68 | + "url": f"/{quote(f.stored_name)}", |
| 69 | + "name": f.original_name, |
| 70 | + "size": f.size_bytes, |
| 71 | + "created_at": f.created_at, |
| 72 | + } for f in files |
| 73 | + ] |
| 74 | + |
| 75 | +@app.get("/{filename}") |
| 76 | +def serve_file(filename: str): |
| 77 | + upload_root = Path(UPLOAD_DIR).resolve() |
| 78 | + try: |
| 79 | + path = (upload_root / filename).resolve() |
| 80 | + path.relative_to(upload_root) |
| 81 | + except (ValueError, RuntimeError): |
| 82 | + raise HTTPException(status_code=404, detail="Not found") |
| 83 | + if not path.is_file(): |
| 84 | + raise HTTPException(status_code=404, detail="Not found") |
| 85 | + return FileResponse(path) |
0 commit comments