- Scheme 76%
- Shell 6.1%
- CSS 5.1%
- Python 4.7%
- C 4.1%
- Other 4%
| .builds | ||
| .jerboa | ||
| deploy | ||
| docs | ||
| public | ||
| scripts | ||
| src | ||
| support | ||
| tests | ||
| tools | ||
| .gitignore | ||
| .jerbuild | ||
| AGENTS.md | ||
| jpkg.lock | ||
| jpkg.policy.sexp | ||
| jpkg.sexp | ||
| LICENSE | ||
| Makefile | ||
| README.md | ||
| SECURITY.md | ||
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
}