import os import shutil import zipfile from pathlib import Path from fastapi import APIRouter, Depends, UploadFile, File, HTTPException, BackgroundTasks from sqlalchemy.ext.asyncio import AsyncSession from app.core.database import get_db from app.core.security import get_current_user from app.core.config import settings from app.models.user import User from app.workers.tasks import process_activity_file, process_garmin_health_zip router = APIRouter() ALLOWED_EXTENSIONS = {".fit", ".gpx", ".zip"} MAX_FILE_SIZE = 500 * 1024 * 1024 # 500 MB def save_upload(upload: UploadFile, dest_dir: Path) -> Path: dest_dir.mkdir(parents=True, exist_ok=True) dest = dest_dir / upload.filename with open(dest, "wb") as f: shutil.copyfileobj(upload.file, f) return dest @router.post("/activity") async def upload_activity( file: UploadFile = File(...), background_tasks: BackgroundTasks = None, db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Upload a single .fit or .gpx activity file.""" suffix = Path(file.filename).suffix.lower() if suffix not in {".fit", ".gpx"}: raise HTTPException(status_code=400, detail="Only .fit and .gpx files are supported") dest_dir = Path(settings.file_store_path) / str(current_user.id) / "activities" dest = save_upload(file, dest_dir) # Queue processing task = process_activity_file.delay(str(dest), current_user.id, suffix[1:]) return {"task_id": task.id, "status": "queued", "filename": file.filename} @router.post("/garmin-export") async def upload_garmin_export( file: UploadFile = File(...), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """ Upload a full Garmin Connect data export ZIP. Processes all FIT files for activities + wellness data. """ if not file.filename.endswith(".zip"): raise HTTPException(status_code=400, detail="Please upload a .zip Garmin export") dest_dir = Path(settings.file_store_path) / str(current_user.id) / "exports" dest = save_upload(file, dest_dir) # Extract and queue all FIT files extract_dir = dest_dir / f"garmin_{dest.stem}" extract_dir.mkdir(exist_ok=True) task_ids = [] with zipfile.ZipFile(dest) as zf: zf.extractall(extract_dir) for name in zf.namelist(): lower = name.lower() if lower.endswith(".fit"): fit_path = extract_dir / name task = process_activity_file.delay(str(fit_path), current_user.id, "fit") task_ids.append(task.id) # Queue health/wellness data extraction health_task = process_garmin_health_zip.delay(str(dest), current_user.id) return { "status": "queued", "activity_tasks": len(task_ids), "task_id": health_task.id, } @router.post("/strava-export") async def upload_strava_export( file: UploadFile = File(...), db: AsyncSession = Depends(get_db), current_user: User = Depends(get_current_user), ): """Upload a Strava bulk export ZIP (contains activities/ folder with GPX/FIT files).""" if not file.filename.endswith(".zip"): raise HTTPException(status_code=400, detail="Please upload a .zip Strava export") dest_dir = Path(settings.file_store_path) / str(current_user.id) / "exports" dest = save_upload(file, dest_dir) extract_dir = dest_dir / f"strava_{dest.stem}" extract_dir.mkdir(exist_ok=True) task_ids = [] with zipfile.ZipFile(dest) as zf: zf.extractall(extract_dir) for name in zf.namelist(): lower = name.lower() if lower.endswith(".fit") or lower.endswith(".gpx"): file_path = extract_dir / name ext = Path(name).suffix[1:] task = process_activity_file.delay(str(file_path), current_user.id, ext) task_ids.append(task.id) return { "status": "queued", "activity_tasks": len(task_ids), "task_id": task_ids[-1] if task_ids else None, } @router.get("/task/{task_id}") async def check_task_status( task_id: str, current_user: User = Depends(get_current_user), ): """Check the status of an upload processing task.""" from app.workers.celery_app import celery_app result = celery_app.AsyncResult(task_id) return { "task_id": task_id, "status": result.status, "result": result.result if result.ready() else None, }