from fastapi import FastAPI, UploadFile, File, HTTPException, Form from fastapi.responses import JSONResponse, FileResponse from fastapi.staticfiles import StaticFiles import os, json, uuid, shutil, re from datetime import datetime PDF_DIR = "/files" META = "/files/.metadata.json" app = FastAPI() def load_meta(): if os.path.exists(META): with open(META) as f: return json.load(f) return {} def save_meta(m): with open(META, "w") as f: json.dump(m, f, indent=2) def safe_name(name): return re.sub(r'[^\w\-. ]', '_', name)[:200] @app.get("/api/list") def list_pdfs(): meta = load_meta() items = [] for fid, info in meta.items(): path = os.path.join(PDF_DIR, info["filename"]) if os.path.exists(path): items.append({ "id": fid, "title": info.get("title", info["filename"]), "filename": info["filename"], "tag": info.get("tag", ""), "size": os.path.getsize(path), "uploaded": info.get("uploaded", ""), }) items.sort(key=lambda x: x["uploaded"], reverse=True) return items @app.post("/api/upload") async def upload(file: UploadFile = File(...), title: str = Form(""), tag: str = Form("")): if not file.filename.lower().endswith(".pdf"): raise HTTPException(400, "PDFs only") fid = uuid.uuid4().hex[:12] fname = safe_name(file.filename) path = os.path.join(PDF_DIR, f"{fid}_{fname}") with open(path, "wb") as f: shutil.copyfileobj(file.file, f) meta = load_meta() meta[fid] = { "filename": f"{fid}_{fname}", "title": title or fname.rsplit(".", 1)[0], "tag": tag, "uploaded": datetime.utcnow().isoformat(), } save_meta(meta) return {"id": fid} @app.delete("/api/delete/{fid}") def delete(fid: str): meta = load_meta() if fid not in meta: raise HTTPException(404) path = os.path.join(PDF_DIR, meta[fid]["filename"]) if os.path.exists(path): os.remove(path) del meta[fid] save_meta(meta) return {"ok": True} @app.get("/view/{fid}") def view(fid: str): meta = load_meta() if fid not in meta: raise HTTPException(404) path = os.path.join(PDF_DIR, meta[fid]["filename"]) return FileResponse(path, media_type="application/pdf") @app.patch("/api/update/{fid}") async def update(fid: str, tag: str = Form(None), title: str = Form(None)): meta = load_meta() if fid not in meta: raise HTTPException(404) if tag is not None: meta[fid]["tag"] = tag if title is not None and title.strip(): meta[fid]["title"] = title.strip() save_meta(meta) return {"ok": True} app.mount("/", StaticFiles(directory="/static", html=True), name="static")