"""FastAPI web upload endpoint for mobile file ingestion. Auth: Token-based via URL query param (?token=secret) or header (X-Upload-Token). Processing: File is saved immediately, enrichment runs in background. """ import logging import secrets import threading from pathlib import Path from fastapi import FastAPI, File, HTTPException, Query, Request, UploadFile from fastapi.responses import HTMLResponse from ingestion.config import IngestionConfig from ingestion.web.browse import router as browse_router logger = logging.getLogger(__name__) app = FastAPI(title="NoteGraph", version="0.1.0") app.include_router(browse_router) @app.get("/") async def root(request: Request): """Redirect root to browse page.""" from fastapi.responses import RedirectResponse token = request.query_params.get("token", "") return RedirectResponse(url=f"/browse?token={token}" if token else "/browse") def _get_config() -> IngestionConfig: return IngestionConfig() def _verify_token(request: Request, token: str = Query(default=None)): """Verify upload token from query param or X-Upload-Token header.""" config = _get_config() # Token is the password part of SB_USER (after the colon) sb_user = config.sb_user.strip() parts = sb_user.split(":", 1) expected_token = parts[1].strip() if len(parts) == 2 else sb_user # Check query param if token and secrets.compare_digest(token.encode(), expected_token.encode()): return True # Check header header_token = request.headers.get("X-Upload-Token", "") if header_token and secrets.compare_digest(header_token.encode(), expected_token.encode()): return True raise HTTPException(status_code=401, detail="Invalid or missing token. Use ?token=YOUR_PASSWORD") def _process_in_background(file_path: Path, config: IngestionConfig): """Run the enrichment pipeline in a background thread.""" try: from ingestion.pipeline import process_file result = process_file(file_path=file_path, config=config) if result.success: logger.info("Background processing complete: %s → %s", file_path.name, result.output_file) else: logger.warning("Background processing failed for %s: %s", file_path.name, result.error) except Exception as e: logger.error("Background processing error for %s: %s", file_path.name, e) UPLOAD_HTML = """ NoteGraph Upload

📄 NoteGraph Upload

Drop file here or tap to select

PDF, JPG, PNG, DOCX, TXT, MD
""" @app.get("/", response_class=HTMLResponse) @app.get("/upload", response_class=HTMLResponse) async def upload_form(request: Request, token: str = Query(default=None)): """Serve the upload form. Token required.""" _verify_token(request, token) return UPLOAD_HTML @app.post("/upload") async def upload_file( request: Request, file: UploadFile = File(...), token: str = Query(default=None), ): """Accept file upload, save immediately, process in background.""" _verify_token(request, token) config = _get_config() inbox = Path(config.inbox_dir) inbox.mkdir(parents=True, exist_ok=True) # Save uploaded file target = inbox / file.filename content = await file.read() target.write_bytes(content) logger.info("Uploaded file saved: %s (%d bytes)", target, len(content)) # Process in background (don't block the response) thread = threading.Thread( target=_process_in_background, args=(target, config), daemon=True, ) thread.start() return {"message": f"File received: {file.filename}. Processing in background — check SilverBullet in ~30s."}