Listing files with a tag filter now logs each referenced tag to
activity.tag_uses, flagging it included (positive) or excluded (negated
under an odd number of NOTs); the untagged pseudo-token is skipped. The
filter AST is reused to determine polarity, so grouped negations like
!(A|B) mark both tags excluded.
Recording happens only when a filter is first applied — not on cursor
pagination or an anchored return — so one browse counts once. The write
is best-effort and never fails the listing.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The import endpoint did all the work in one request and returned only an
aggregate summary, so the UI couldn't show progress or per-file status.
Refactor FileService.Import to take an optional progress callback and emit
a "start" event (with the total entry count), one "file" event per entry as
it finishes (index, filename, status, optional reason), and a final "done"
event with the tallies. The handler streams these as newline-delimited JSON
and flushes after each, deferring the response headers until the first event
so a validation error raised before any file is touched is still returned as
a normal JSON error.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The previous goexif reader only understood EXIF in JPEG/TIFF, so videos,
PNGs and any image without an EXIF block were stored with no metadata at
all. Shell out to exiftool instead (the same tool the prior version used),
which covers images, video and audio in one pass.
Run it with `-n` so every tag comes back as a raw numeric/machine value
(FileSize in bytes, Duration in seconds, AvgBitrate as a number) rather
than human-readable strings — the metadata is the basis for analytics, not
decoration. Temp-file artifacts (SourceFile/Directory/permissions/inode
dates) are stripped and FileName is set to the original.
content_datetime now resolves from the first real capture date in the
metadata (DateTimeOriginal, then the video CreateDate atoms), still falling
back to the import mtime. When exiftool isn't on PATH the pure-Go EXIF
reader remains as a graceful fallback.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The directory import now removes each source file after it is safely
ingested, so the import folder drains and re-running doesn't create
duplicates (a removal failure is reported per-file but doesn't undo the
import). It also captures the source file's mtime and passes it as a new
ContentDatetimeFallback on Upload, used for content_datetime only when
the file has no EXIF date — so non-photo files keep a meaningful date
instead of the zero value once the source is gone. Adds an integration
test covering ingest, directory skip, source removal and the mtime
fallback.
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>
Listings returned every row regardless of ownership: GET /files, /tags,
/pools and /categories exposed other users' private items (while the
single-item GET correctly returned 403), and the pool file operations
(GET /pools/:id, /pools/:id/files, add/remove/reorder) skipped ACL
entirely, so any authenticated user could read and rewrite anyone's
private pool.
- List queries now filter to rows the caller may see (public, owned, or
granted can_view) via a shared SQL condition; admins bypass. The viewer
identity is taken from the request context by the service and passed to
the repository in the list params.
- Tag/Category/Pool single-item Get now enforce CanView (File already did).
- Pool Get/ListFiles require pool view; AddFiles/RemoveFiles/Reorder
require pool edit.
Adds regression tests for private-by-default listing (hidden / public /
granted / admin) and for pool operations rejecting a non-owner.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Replacing a file's content left the old {id}_thumb.jpg / {id}_preview.jpg
in the cache, and the cache-hit fast path kept serving the stale image
forever; permanent deletion left those files orphaned. FileStorage gains
InvalidateCache, which Replace and PermanentDelete now call.
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>