GetContent streamed the whole file with a plain 200/io.Copy and no
Accept-Ranges, so the browser couldn't seek or scrub audio/video opened
from the viewer. It now serves seekable bodies (the disk store returns an
*os.File) via http.ServeContent, which advertises Accept-Ranges and
answers Range requests with 206 Partial Content; non-seekable bodies
still fall back to a plain stream. Adds an integration test asserting a
ranged request returns 206 with the right Content-Range and bytes.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The file viewer's preview is now a real link (target=_blank) to the original,
instead of fetching it into a blob. A navigation can't send the auth header, so
the access token rides in the query — the auth middleware accepts ?access_token=
as a fallback, but only for GET, so a crafted link can't drive a mutation.
GetContent gains an ?inline=1 toggle (Content-Disposition: inline) so the tab
views the original instead of downloading it; download stays the default.
Documented in openapi.yaml; TestMediaQueryTokenAuth covers GET-with-query-token
(200), missing token (401) and query-token rejected on a non-GET (401).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The activity.file_views table existed but nothing ever wrote to it. Add a
POST /files/{id}/views endpoint: FileRepo.RecordView inserts a history row,
FileService.RecordView enforces view ACL first. The file viewer fires it
(fire-and-forget) when a file is opened, including while paging prev/next.
Documented in openapi.yaml; covered by TestRecordFileView (204 on view,
repeatable, 404 for unknown file).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Run gofmt -w across the backend, normalising the manually-aligned := blocks
to the gofmt standard. No code behaviour changes.
Add Prettier (+ prettier-plugin-svelte) to the frontend with the SvelteKit
default config (tabs, single quotes) so formatting is reproducible, then run
it over the whole tree. Add format / format:check npm scripts and a
.prettierignore (build output, generated schema.ts, static assets).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
These endpoints had no Cache-Control, so the browser re-downloaded every
thumbnail on each grid mount (the client fetches them with an auth header,
which also bypasses default image caching). Returning to the grid after
viewing a file re-fetched the whole visible page of thumbnails. Add
Cache-Control: private, max-age=3600. Content is immutable per file id from
the client's perspective (there is no replace-content UI); a future replace
flow should cache-bust via a versioned URL.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Upload and Replace buffered the entire request body into memory with no
size limit, so a few large uploads could OOM the server. The file
handler now wraps the request body in http.MaxBytesReader and rejects any
file larger than MAX_UPLOAD_BYTES (default 500 MiB) before it is buffered.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two broken-access-control holes:
- PUT/DELETE /files/:id/tags(/:tag_id) and GET /files/:id/tags went
straight to TagService with no ACL check, letting any authenticated
user read or rewrite tags on anyone's private files. The handlers now
require view (list) or edit (mutate) on the target file via new
FileService.AuthorizeView/AuthorizeEdit helpers.
- POST /files/import accepted an arbitrary host path from any user,
turning it into an arbitrary server-side file read. It is now
admin-only and the supplied path is confined to IMPORT_PATH.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>