Send our own Cache-Control headers instead of relying entirely on
front-proxy configuration, so browsers cache appropriately even when
Zampler is reached directly. --static-max-age, --file-max-age, and
--api-max-age (all cobra Duration flags, 0 disables caching) cover the
three response categories independently; --no-immutable turns off the
immutable directive on served files, which is on by default since a
file's content never changes for a given ID. The HTML shell always
gets no-cache regardless of --static-max-age, since it isn't
content-hashed like the rest of the Vite build and must keep
revalidating so a new deploy's asset references are picked up.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Previously each scanned file was its own autocommit transaction, so a
large library meant tens of thousands of individually journaled (and,
outside WAL mode, fsynced) commits back to back. Grouping them into
batches cuts that by ~2-3 orders of magnitude and shrinks the window
during which the scanner holds the write lock, part of the fix for #17.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Address review feedback on !18: appending "?..." unconditionally broke
if the --sqlite value already carried its own query string, since the
driver treats the first "?" as the path/query boundary. Join with "&"
when one is already present.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes SQLITE_BUSY errors seen with a large library: the connection
previously used SQLite's default journal mode (which blocks readers
during a write) and a zero busy_timeout (any lock contention failed
immediately instead of retrying), while an uncapped connection pool
let multiple connections contend for the single writer lock.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Closes#8, #9, #10; partially addresses #11.
middlewares (#8):
- Log: log once from the Middleware closure after the handler returns
instead of spawning a goroutine per Write (data race on the shared
LogWriter, multiple log lines per request). Bodiless responses
(204/304/redirects) are now logged too, and the status-class switch has
a default branch so a stray sub-100 status can't nil-deref the event.
- logWriter forwards http.Flusher / http.Hijacker / io.ReaderFrom so
streaming handlers and the file server keep their fast paths.
- Both middlewares use the closure idiom rather than storing next on a
shared instance, so they're safe to register on more than one chain.
- CorsOrigin now honours its argument: a comma-separated allow-list,
with "*" meaning allow-any. Wired to a new --allow-origin flag that
defaults to "*" (unchanged behaviour out of the box).
ReadFile (#9):
- Add the missing returns after GetFile / open failures (the old code
fell through to os.ReadFile(nil path) and wrote a second body).
- Hand off to http.ServeContent for Content-Type, Range and
If-Modified-Since, and set Content-Disposition: inline so the embedded
audio player streams instead of downloading (the download button
already uses the HTML download attribute).
lib.Coalesce (#10):
- Constrain to comparable and compare against a fresh zero value. The
old IsZero type switch compared an any against untyped constants, so
every narrow numeric type (int8..int64, uint*, float32) reported
non-zero and never fell back. IsZero is removed (was unused elsewhere).
tests (#11, high-value/easy set):
- lib.Coalesce table across every numeric width, string, bool, pointer.
- database.Migrate against a temp sqlite: schema created, recorded once,
second call is a no-op.
- types.NewFile: id = sha256(path), hash = sha256(contents), scan fields
copied, and the missing-file path still yields an id.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Everything under code.aneur.in/go/* except validate was archived. Swap
each for its replacement; validate stays.
- rest -> internal/rest: the JSON response/error helpers zampler actually
uses (~110 lines), same {"message","data"} wire shape and status codes.
- sqan -> internal/scan: a filepath.WalkDir walk. zampler only filtered
by extension; sqan's path semantics (absolute root, rooted RelativePath,
"/" RelativeDir for root-level files) are kept.
- sqimple + sqaffold query building -> github.com/doug-martin/goqu/v9,
which also does the row scanning that record + sqaffold provided. A
fileRow struct with db tags maps to types.File (millis <-> time.Time).
- sqaffold.Migrate + version.Migration -> a ~40-line migrator in
internal/database/migration.go tracking applied names in a
schema_migrations table. goose was disproportionate for one migration.
- version -> gone (only its Migration helper was used).
Net dependency change: -6 code.aneur.in/go modules, +1 (goqu).
go build / go vet / go test ./... green; manual run verified migration,
scan+insert, list/get/search/sort/paging, 404 and 400 shapes, and
rescan idempotency (on conflict replace).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds a --allow-indexing / -i flag (default false). The server now handles
GET /robots.txt itself instead of relying on a static file:
- default: block every crawler (User-agent: * / Disallow: /)
- --allow-indexing: permissive (Disallow: empty)
Generating it in the handler means the policy is set per host at run time.
Search indexing is opt-in, as requested on the issue.
Closes#5
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
RunE looked up flags.GetString("host"), but the flag is registered as
"address" (-a), so --address was silently ignored and the server always
bound to all interfaces. Read "address" instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>