diff --git a/bincio/serve/server.py b/bincio/serve/server.py index 3e6e202..983a07d 100644 --- a/bincio/serve/server.py +++ b/bincio/serve/server.py @@ -465,43 +465,57 @@ _SUPPORTED_SUFFIXES = {".fit", ".gpx", ".tcx", ".fit.gz", ".gpx.gz", ".tcx.gz"} @app.post("/api/upload") async def upload_activity( - file: UploadFile = File(...), + files: list[UploadFile] = File(...), bincio_session: Optional[str] = Cookie(default=None), ) -> JSONResponse: + from bincio.extract.ingest import ingest_parsed + from bincio.extract.parsers.factory import parse_file + from bincio.extract.writer import make_activity_id + from bincio.render.merge import merge_all + user = _require_user(bincio_session) dd = _get_data_dir() / user.handle - name = Path(file.filename or "upload.fit").name - p = Path(name.lower()) - suffix = (p.stem.rsplit(".", 1)[-1].join([".", ".gz"]) if "." in p.stem else ".gz") if p.suffix == ".gz" else p.suffix - if suffix not in _SUPPORTED_SUFFIXES: - raise HTTPException(400, f"Unsupported file type '{suffix}'") - contents = await file.read() - if len(contents) > 50 * 1024 * 1024: - raise HTTPException(413, "File too large (max 50 MB)") staging = dd / "_uploads" staging.mkdir(exist_ok=True) - staged = staging / name - staged.write_bytes(contents) - try: - from bincio.extract.ingest import ingest_parsed - from bincio.extract.parsers.factory import parse_file - activity = parse_file(staged) - activity_id_check = dd / "activities" / f"{activity.source_file}.json" - from bincio.extract.writer import make_activity_id - activity_id = make_activity_id(activity) - if (dd / "activities" / f"{activity_id}.json").exists(): - raise HTTPException(409, f"Activity already exists: {activity_id}") - ingest_parsed(activity, dd, privacy="public") - from bincio.render.merge import merge_all + + results = [] + any_added = False + + for file in files: + name = Path(file.filename or "upload.fit").name + p = Path(name.lower()) + suffix = (p.stem.rsplit(".", 1)[-1].join([".", ".gz"]) if "." in p.stem else ".gz") if p.suffix == ".gz" else p.suffix + if suffix not in _SUPPORTED_SUFFIXES: + results.append({"name": name, "ok": False, "error": f"Unsupported file type '{suffix}'"}) + continue + + contents = await file.read() + if len(contents) > 50 * 1024 * 1024: + results.append({"name": name, "ok": False, "error": "File too large (max 50 MB)"}) + continue + + staged = staging / name + staged.write_bytes(contents) + try: + activity = parse_file(staged) + activity_id = make_activity_id(activity) + if (dd / "activities" / f"{activity_id}.json").exists(): + results.append({"name": name, "ok": False, "error": "duplicate"}) + continue + ingest_parsed(activity, dd, privacy="public") + results.append({"name": name, "ok": True, "id": activity_id}) + any_added = True + except Exception as exc: + results.append({"name": name, "ok": False, "error": f"{type(exc).__name__}: {exc}"}) + finally: + staged.unlink(missing_ok=True) + + if any_added: merge_all(dd) _trigger_rebuild(user.handle) - except HTTPException: - raise - except Exception as exc: - raise HTTPException(422, f"Failed to process activity: {type(exc).__name__}: {exc}") - finally: - staged.unlink(missing_ok=True) - return JSONResponse({"ok": True, "id": activity_id}) + + added = [r for r in results if r["ok"]] + return JSONResponse({"ok": True, "added": len(added), "results": results}) @app.post("/api/strava/sync") diff --git a/site/src/layouts/Base.astro b/site/src/layouts/Base.astro index 5411cff..679daf8 100644 --- a/site/src/layouts/Base.astro +++ b/site/src/layouts/Base.astro @@ -256,8 +256,8 @@ try { id="upload-drop" class="border-2 border-dashed border-zinc-700 rounded-lg p-8 text-center text-zinc-500 text-sm cursor-pointer hover:border-zinc-500 hover:text-zinc-300 transition-colors" > -