Skip to content

Storage: disk usage and free space in Settings, a header warning, and refusing writes when full - #35

Merged
davior merged 1 commit into
mainfrom
claude/funny-goodall-iitfyh
Oct 12, 2026
Merged

davior merged 1 commit into
mainfrom
claude/funny-goodall-iitfyh

Conversation

@davior

@davior davior commented Oct 12, 2026

Copy link
Copy Markdown
Owner

What this adds

GAM couldn't show how much disk the library uses or how much is left, and nothing stopped writes before the disk filled. A full disk fails an import halfway, leaves .partial files behind, and can fail a SQLite commit.

Settings → Storage (new panel, after "What enrichment has cost")

  • Everyone: their own library's total, a row per type (Video · 34 files · 120 GB), previews, and their 10 largest files. Each of those links to its asset page.
  • Admins (ADMIN_USERS) also see the server:
    • One bar per disk, split into GAM media, GAM database, everything else, the filesystem's reserve, and free space. The bar uses the first two categorical slots of the validated palette, checked against both light and dark surfaces. Inside Docker these are the host's numbers, through the data/ bind mount.
    • Stray files: files under data/media that no storage_key/thumb_key refers to and that are more than 24 h old. The age limit exists because every write path puts the file on disk before it saves the row. Remove them asks first, then deletes through storage.delete.

Header. A drive icon appears only when free space is low: amber below DISK_WARN_FREE_GB (20), red below DISK_RESERVE_GB (5). It links to /settings#storage. Non-admins get the level but not the numbers.

Refusing writes. Below the reserve, uploads, URL imports, generations, sub-video extractions and promotions return 507 disk_full.

  • They are checked when queued and again when the job starts, since a job can wait in the queue while the disk fills.
  • A generation is checked before fal.submit, so nothing is billed for an output that can't be saved. A request already submitted still saves.
  • Uploads are checked per file, matching the existing partial-success design. If every file is refused, the response is a 507 with the disk-full message.
  • The check fails open if statvfs can't be read.

The container's temp directory is shown when it's on a separate disk, but doesn't count toward the level. Nothing persistent lives there, and on a dev machine it's often a small tmpfs that would otherwise block every upload.

Files

  • backend/app/storage/capacity.py: volumes (merged by device), levels, has_room / require_room.
  • backend/app/services/storage_usage.py, routers/storage.py, schemas_storage.py: GET /api/storage, GET /api/storage/status, POST /api/storage/cleanup (admin only).
  • LocalStorage.files(): walks exactly <owner>/<name> and doesn't follow symlinks.
  • Guards added in routers/{assets,imports,generate,clips}.py, enrichment/{import_url,extract_subvideo}.py, generation/run.py.
  • Frontend: api/storage.ts, StoragePanel.tsx, StorageWarning.tsx.
  • Config: DISK_WARN_FREE_GB and DISK_RESERVE_GB in config.py, docker-compose.yml and .env.example. There's a new "Disk space" section in docs/deployment.md.

Testing

  • pytest -q: 1247 passed (43 new). The new tests are test_storage_usage.py, listing tests in test_storage.py, and job-level disk-full tests in the generation, import and clip suites. The generation test asserts fal is never called.
  • A new autouse disk fixture fakes statvfs, so the suite no longer depends on how full the runner is.
  • I disabled the guard and the stray age check by hand to confirm the tests fail without them: 7 fail.
  • npm run lint, format:check, npm test (393 passed, 16 new) and npm run build are all clean.
  • Live run (uvicorn on 8001 + Vite on 5174, dev auth as an admin, scratch DB):
    • Panel figures match df -B1 and du -sb byte for byte.
    • Raising the thresholds above current free space turned the header amber, then red.
    • Upload, import and sub-video returned 507.
    • Cleanup removed a 2-day-old orphan, and left a fresh orphan, every asset file and every preview in place.

🤖 Generated with Claude Code

https://claude.ai/code/session_017Ev6jNsXYxKEPPN6EpqpTE


Generated by Claude Code

Settings gains a Storage panel, and the header a drive icon that appears only
when free space is low.

Everyone sees their own library: totals by type, previews, and their ten
largest files as links, which is the quickest way to find something to delete.
Admins (ADMIN_USERS) also see the server:
- one bar per disk, split into GAM media, GAM database, everything else, the
  filesystem's reserve and free space; inside Docker these are the host's
  numbers, through the data/ bind mount
- stray files: files under data/media that no asset refers to, older than a
  day, with a button that removes them through storage.delete

Below DISK_RESERVE_GB (default 5) uploads, URL imports, generations and
sub-video extractions are refused with 507 disk_full. They are checked when
queued and again when the job starts. A generation is checked before it is
sent to fal, so nothing is billed for an output that cannot be saved, and a
request already submitted still saves. An upload stores the files that fit
and rejects the rest. DISK_WARN_FREE_GB (default 20) sets the header warning.
Both are fixed sizes because single large files are what fill the disk.

The container's temp directory is shown when it is a separate disk, but does
not count toward the level: nothing persistent lives there, and on a dev
machine it is often a small tmpfs that would otherwise block every upload.

Tests run against a fake disk (conftest's autouse `disk`), so the suite no
longer depends on how full the machine running it is.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017Ev6jNsXYxKEPPN6EpqpTE
@davior
davior marked this pull request as ready for review October 12, 2026 12:45
@davior
davior merged commit 4acc57d into main Oct 12, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants