Bug fixed — temp ZIPs now go to /tmp/ (system temp) and are always deleted in a finally block, so they can't leak. A startup hook also auto-cleans any leftovers on
next server restart. Admin page now shows: - Overall disk bar (used/free/%) - Per-user table: total, activities (with file count), originals (with Strava breakdown), merged, images - A mini bar per user showing relative size - Red ⚠ warning if orphaned temp ZIPs are still present for a user - Delete activities button (reloads sizes after)
This commit is contained in:
+66
-2
@@ -106,6 +106,19 @@ def _get_data_dir() -> Path:
|
||||
|
||||
app = FastAPI(title="BincioActivity Serve", docs_url=None, redoc_url=None)
|
||||
|
||||
|
||||
@app.on_event("startup")
|
||||
async def _cleanup_orphaned_tmp_zips() -> None:
|
||||
"""Remove tmp*.zip files left in user data dirs by the pre-fix upload handler."""
|
||||
import glob as _glob
|
||||
data_dir = _get_data_dir()
|
||||
for p in _glob.glob(str(data_dir / "*" / "tmp*.zip")):
|
||||
try:
|
||||
Path(p).unlink()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
app.add_middleware(GZipMiddleware, minimum_size=1024)
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
@@ -437,6 +450,56 @@ async def admin_jobs(bincio_session: Optional[str] = Cookie(default=None)) -> JS
|
||||
return JSONResponse(jobs)
|
||||
|
||||
|
||||
@app.get("/api/admin/disk")
|
||||
async def admin_disk(bincio_session: Optional[str] = Cookie(default=None)) -> JSONResponse:
|
||||
"""Per-user disk usage breakdown. Admin only."""
|
||||
_require_admin(bincio_session)
|
||||
import shutil
|
||||
|
||||
data_dir = _get_data_dir()
|
||||
|
||||
def _mb(path: Path) -> float:
|
||||
if not path.exists():
|
||||
return 0.0
|
||||
total = sum(f.stat().st_size for f in path.rglob("*") if f.is_file())
|
||||
return round(total / 1_048_576, 1)
|
||||
|
||||
def _count(path: Path, pattern: str = "*") -> int:
|
||||
if not path.exists():
|
||||
return 0
|
||||
return sum(1 for f in path.glob(pattern) if f.is_file())
|
||||
|
||||
users = []
|
||||
for user_dir in sorted(data_dir.iterdir()):
|
||||
if not user_dir.is_dir() or user_dir.name.startswith("_"):
|
||||
continue
|
||||
# leaked tmp zips
|
||||
leaked = [f for f in user_dir.glob("tmp*.zip") if f.is_file()]
|
||||
users.append({
|
||||
"handle": user_dir.name,
|
||||
"total_mb": _mb(user_dir),
|
||||
"activities_mb": _mb(user_dir / "activities"),
|
||||
"activities_count": _count(user_dir / "activities", "*.json"),
|
||||
"merged_mb": _mb(user_dir / "_merged"),
|
||||
"originals_mb": _mb(user_dir / "originals"),
|
||||
"originals_strava_mb": _mb(user_dir / "originals" / "strava"),
|
||||
"images_mb": _mb(user_dir / "edits" / "images"),
|
||||
"leaked_zips_mb": round(sum(f.stat().st_size for f in leaked) / 1_048_576, 1),
|
||||
"leaked_zips_count": len(leaked),
|
||||
})
|
||||
|
||||
disk = shutil.disk_usage("/")
|
||||
return JSONResponse({
|
||||
"disk": {
|
||||
"total_gb": round(disk.total / 1_073_741_824, 1),
|
||||
"used_gb": round(disk.used / 1_073_741_824, 1),
|
||||
"free_gb": round(disk.free / 1_073_741_824, 1),
|
||||
"percent": round(disk.used / disk.total * 100, 1),
|
||||
},
|
||||
"users": users,
|
||||
})
|
||||
|
||||
|
||||
@app.delete("/api/admin/users/{handle}/activities")
|
||||
async def admin_delete_activities(
|
||||
handle: str,
|
||||
@@ -803,7 +866,7 @@ async def upload_strava_zip(
|
||||
|
||||
dd = _get_data_dir() / user.handle
|
||||
import tempfile
|
||||
tmp = tempfile.NamedTemporaryFile(suffix=".zip", delete=False, dir=dd)
|
||||
tmp = tempfile.NamedTemporaryFile(suffix=".zip", delete=False)
|
||||
zip_path = Path(tmp.name)
|
||||
try:
|
||||
while chunk := await file.read(1024 * 1024): # 1 MB chunks
|
||||
@@ -826,8 +889,9 @@ async def upload_strava_zip(
|
||||
merge_all(dd)
|
||||
_trigger_rebuild(user.handle)
|
||||
except Exception as exc:
|
||||
zip_path.unlink(missing_ok=True)
|
||||
yield f"data: {json.dumps({'type': 'error', 'message': str(exc)})}\n\n"
|
||||
finally:
|
||||
zip_path.unlink(missing_ok=True)
|
||||
|
||||
return StreamingResponse(
|
||||
event_stream(),
|
||||
|
||||
Reference in New Issue
Block a user