feat: file type filter + NFS-friendly thumbnail caching

- Files page: type filter chips (All/Image/Video/Audio/Text/Document/Archive/
  Other) with live counts, persisted in localStorage, upload/delete re-apply
  the active filter; header shows filtered/total count
- Cache headers: thumbnails + inline media get public,max-age=31536000,immutable
  (uuid stored_name is immutable); attachment downloads no-store — repeated
  views stop hammering the NFS uploads volume
- Thumbnails pre-generated on upload (form view + /api/files ViewSet +
  collection uploads) so the first page view is instant
- tests: 4 new cache/pre-gen cases (152 total)
This commit is contained in:
OpenClaw Sub-agent
2026-08-03 16:53:00 +10:00
parent 9f3a673e47
commit e685aa6ed8
4 changed files with 179 additions and 16 deletions
+41
View File
@@ -252,3 +252,44 @@ class TestFilesAPI:
if os.path.exists(record.file_path):
os.remove(record.file_path)
assert client.get(f"/ui/files/{pk}/thumb/").status_code == 404
# ── Cache headers (NFS-friendly) ────────────────────────────────────
def test_thumbnail_cache_control_immutable(self, client, api_client):
"""Thumbnails are content-addressed by the immutable stored_name."""
pk = api_client.post(
f"{self.BASE}", {"files": _real_png()}, format="multipart"
).json()[0]["id"]
r = client.get(f"/ui/files/{pk}/thumb/")
assert r.status_code == 200
assert r["Cache-Control"] == "public, max-age=31536000, immutable"
def test_inline_download_cache_control(self, client, api_client):
"""Inline media (images) must be cacheable so repeated views skip NFS."""
uploaded = api_client.post(
f"{self.BASE}", {"files": _real_png()}, format="multipart"
).json()[0]
r = client.get(f"/ui/files/{uploaded['id']}-{uploaded['name']}")
assert r.status_code == 200
assert "max-age=31536000" in r["Cache-Control"]
assert "immutable" in r["Cache-Control"]
def test_attachment_download_no_store(self, client, api_client):
"""Non-media (attachment) downloads must not be cached."""
uploaded = api_client.post(
f"{self.BASE}", {"files": _pdf()}, format="multipart"
).json()[0]
r = client.get(f"/ui/files/{uploaded['id']}-{uploaded['name']}")
assert r.status_code == 200
assert (r["Content-Disposition"] or "").startswith("attachment")
assert r["Cache-Control"] == "private, no-store"
def test_upload_pre_generates_thumbnail(self, api_client):
"""Image uploads pre-generate the thumbnail so first view is instant."""
uploaded = api_client.post(
f"{self.BASE}", {"files": _real_png()}, format="multipart"
).json()[0]
from links.models import FileUpload
record = FileUpload.objects.get(pk=uploaded["id"])
assert os.path.exists(record.thumb_path)