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>
Full directories in the real world are a lot longer than I realised. I will come up with a better way to show them. For now, filenames only, just so the page doesn't break
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>
Runs on pull requests to main: frontend type-check and build, then
gofmt, go mod tidy verification, go vet, go build, and go test.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This Gitea instance reports as GHES and rejects the @actions/artifact v2
protocol, so upload-artifact@v4 / download-artifact@v4 fail. Roll the
workflow, Dockerfile and .dockerignore back to the pre-split state
(dd6bb1a): one build-and-push job, no artifact handoff.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Rename the CI binary and its artifact to zampler-amd64, and make the
Dockerfile arch-agnostic via ARG TARGETARCH / COPY zampler-$TARGETARCH.
Keeps the pipeline single-arch for now but makes adding an arm64 leg a
smaller change later.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
build-binary compiles the frontend and Go binary (with the module and
build caches) and uploads it as an artifact; build-image downloads the
artifact and builds/pushes the Docker image from it.
The executable bit is restored after download since upload-artifact
does not preserve it.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The Go compile is dominated by modernc.org/libc (~400k lines of
generated Go from modernc.org/sqlite). Inside `docker build` on an
ephemeral runner it recompiled cold every push; on a 1 CPU / 1 GB
runner that took ~10 minutes.
Move the frontend and Go builds into the workflow, where actions/cache
persists ~/.cache/go-build across runs so libc is compiled once. The
Dockerfile is now just an alpine wrapper around the prebuilt
CGO_ENABLED=0 static binary.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Copy dependency manifests before source so npm ci / go mod download
layers survive source-only changes, and add BuildKit cache mounts for
the npm, Go module, and Go build caches. Build a static binary with
CGO_ENABLED=0 -ldflags="-s -w".
Tighten .dockerignore so the 177M frontend/node_modules, .git, docs and
editor dirs stay out of the build context.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>