135 lines
4.4 KiB
Python
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,
|
|
}
|