No description
  • Scheme 76%
  • Shell 6.1%
  • CSS 5.1%
  • Python 4.7%
  • C 4.1%
  • Other 4%
Find a file
2026-07-30 15:46:46 -06:00
.builds ci: set umask 022 in package/verify tasks too (jerbuild creates group-writable cache dirs during pkg operations) 2026-07-22 13:10:56 -06:00
.jerboa Resolve security audit findings 2026-07-11 17:22:32 -06:00
deploy Resolve security audit findings 2026-07-11 17:22:32 -06:00
docs Resolve security audit findings 2026-07-11 17:22:32 -06:00
public Resolve security audit findings 2026-07-11 17:22:32 -06:00
scripts Resolve security audit findings 2026-07-11 17:22:32 -06:00
src perf(db): batch tag inserts with prepared statements in set-media-tags! (P2) 2026-07-22 15:52:59 -06:00
support Resolve security audit findings 2026-07-11 17:22:32 -06:00
tests perf(db): batch tag inserts with prepared statements in set-media-tags! (P2) 2026-07-22 15:52:59 -06:00
tools Resolve security audit findings 2026-07-11 17:22:32 -06:00
.gitignore jpkg: complete binary package setup 2026-07-30 15:19:46 -06:00
.jerbuild Resolve security audit findings 2026-07-11 17:22:32 -06:00
AGENTS.md docs: remove jerboa-emacs restriction 2026-07-30 15:46:46 -06:00
jpkg.lock jpkg: complete binary package setup 2026-07-30 15:19:46 -06:00
jpkg.policy.sexp jpkg: complete binary package setup 2026-07-30 15:19:46 -06:00
jpkg.sexp jpkg: complete binary package setup 2026-07-30 15:19:46 -06:00
LICENSE Switch to MIT license 2026-07-21 13:42:17 -06:00
Makefile build: add lint target 2026-07-21 11:56:17 -06:00
README.md Resolve security audit findings 2026-07-11 17:22:32 -06:00
SECURITY.md Resolve security audit findings 2026-07-11 17:22:32 -06:00

jerboa-imagesite

jerboa-imagesite is a small Piwigo replacement for the Wendy Fournier Sanborn photo archive and similar filesystem-backed galleries.

The design keeps the durable archive simple:

  • originals and videos stay on disk under a media root;
  • generated thumbnails/previews are optional files under a derivative root;
  • Jerboa's (std db sqlite) API, backed by vendored jsqlite, stores albums, media rows, tags, captions, visibility, and sync history;
  • Jerboa-Sinatra serves the catalog UI, search, JSON endpoints, family access gate, and admin sync hooks;
  • the app can terminate TLS directly and serves gated media with byte-range support for video playback.

This is intentionally not a PHP plugin system. It takes the useful Piwigo ideas that mattered in the existing deployment, physical albums, tags, search, family-gated access, video support, sidecar metadata, and sync-from-disk, while keeping the runtime native Scheme plus SQLite.

Build

make build

The Makefile builds sibling dependencies from ../jerboa-sinatra, uses the configured Jerboa tree's vendored vendor/jsqlite/src, then transpiles this repository into build/lib.

For the FreeBSD jail deployment, build a single target binary with the cross-build variables used by the Ansible playbook:

make static-binary \
  STATIC_TARGET_MACHINE=ta6fb \
  STATIC_RUST_TARGET=x86_64-unknown-freebsd \
  STATIC_BINARY_OUTPUT=dist/jerboa-imagesite-freebsd-amd64

Test

make test
make web-security-smoke

The hostile smoke starts the standalone server on loopback and proves that credentials are POST-only, cross-origin and missing-CSRF requests fail, GET mutations are inert, opaque sessions rotate at login, and a media symlink swap cannot escape the configured root.

Run Locally

export JERBOA_IMAGESITE_SESSION_SECRET="$(openssl rand -hex 32)"
export JERBOA_IMAGESITE_ACCESS_ANSWER="<family-answer>"
export JERBOA_IMAGESITE_MEDIA_ROOT="/srv/wendyfourniersanborn/photos/published"
export JERBOA_IMAGESITE_DB="/var/db/jerboa-imagesite/imagesite.db"
make run

Then visit http://127.0.0.1:4580/.

For direct TLS, set cert and key paths:

export JERBOA_IMAGESITE_PORT=8443
export JERBOA_IMAGESITE_TLS_CERT=/path/fullchain.cer
export JERBOA_IMAGESITE_TLS_KEY=/path/privkey.key
make run

For a first local smoke test without production media:

mkdir -p var/media/family var/db
cp some-photo.jpg var/media/family/
JERBOA_IMAGESITE_SESSION_SECRET=dev-secret \
JERBOA_IMAGESITE_SESSION_SECURE=0 \
JERBOA_IMAGESITE_ACCESS_ANSWER=dev-answer \
JERBOA_IMAGESITE_MEDIA_ROOT="$PWD/var/media" \
JERBOA_IMAGESITE_DB="$PWD/var/db/imagesite.db" \
make run

In another shell:

curl -X POST -H "X-Imagesite-Admin-Token: dev-admin" \
  http://127.0.0.1:4580/admin/sync

Set JERBOA_IMAGESITE_ADMIN_TOKEN=dev-admin before starting the server if you want the admin sync endpoint enabled.

Browser gate/admin credentials are accepted only in form-encoded POST bodies. They are never accepted from query parameters. Browser mutations require a random, session-bound CSRF value and same-origin validation. The cookie holds only an opaque random session identifier; it is HttpOnly, SameSite=Strict, and Secure by default. The administrator grant expires server-side after one hour and login failures are rate-limited and audited without logging answers.

Filesystem Layout

The Ansible deployment stages media like this:

/srv/wendyfourniersanborn/photos/inbox
/srv/wendyfourniersanborn/photos/metadata
/srv/wendyfourniersanborn/photos/classified
/srv/wendyfourniersanborn/photos/published

jerboa-imagesite indexes only the configured published media root. Directory names become album paths. A file at:

published/reunion/IMG_0001.jpg

becomes album reunion and media path reunion/IMG_0001.jpg.

Rows are marked missing during sync when a previously indexed file is absent. They are not deleted automatically, so a bad mount or partial rsync does not destroy catalog work.

The media root is opened as a no-follow directory descriptor. Walks use descriptor-relative openat/fstatat, reject links and inode swaps, stay on the root filesystem, remember visited device/inode pairs, and enforce depth (32), entry (100,000), per-file (8 GiB), and aggregate (256 GiB) limits. Direct media serving is development-only and disabled by default; production deployments should use an internal nginx X-Accel-Redirect location with symlinks disabled.

Metadata Sidecars

Sidecar import is deliberately simple and append-only friendly. For a media file family/IMG_0001.jpg, the syncer looks for either:

metadata/family/IMG_0001.jpg.json
metadata/family/IMG_0001.json

Supported keys:

{
  "title": "Wendy at the lake",
  "caption": "Summer trip",
  "taken_at": "1998-07-04",
  "tags": ["family", "lake"],
  "favorite": true,
  "width": 1600,
  "height": 1200
}

Production

See docs/deployment.md and deploy/nginx.conf.example.