Files
MileVault/backend/app/api/upload.py
T
2026-06-06 13:23:33 +01:00

135 lines
4.4 KiB
Python

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),
"health_task": 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),
}
@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,
}