puttaalu 3614cfe1f8 Merge trunk into trunk-ng
# Conflicts:
#	CLAUDE.md
#	README.md
#	generator.go
#	main.go
#	manage.go
#	manage_test.go
#	module.nix
#	server.go
#	templates/grid.html
#	templates/manage.html
#	templates/timeline.html
2026-10-19 17:04:58 +02:00
2026-09-26 10:16:17 +02:00
2026-07-02 00:53:17 +02:00
2026-07-02 00:53:17 +02:00

photogallery

A single-user static photo gallery: a Go binary watches a directory of images + .toml sidecars and regenerates a static site, with an Atom feed, albums, an authenticated upload/manage API, and Android + desktop apps. No database, no framework.

Contributors

  • Santhosh Raju — originated the ideas for the edit/delete ("manage") capability and the configurable default view (-default-view, timeline or grid as the site root). Both landed here as independent implementations building on that work.

Features

Gallery

  • Two views of the same photos — a day-grouped timeline and a flat grid — either one as the site root (-default-view); the other lives under /timeline/ or /grid/. 3-column square grid (2 on mobile) with infinite-scroll pagination.
  • Single-day pages (/day/<date>/) linked from each timeline date heading.
  • Albums: tag photos with free-text, lowercase tags; each tag is an album at /album/<tag>/, indexed at /albums/, plus a machine-readable /tags.json.
  • Single photo pages with newer/older navigation (arrow keys + touch swipe) and a link to the original.
  • Open Graph / Twitter-card link previews so shared photo links unfurl.
  • Atom feed at /feed.xml. Gruvbox-dark theme; configurable fonts and identity.

Content & management (no database)

  • .toml sidecars are the source of truth; the binary watches the content dir and rebuilds on any change. Four renditions per photo (original, 600² thumbnail, 1600px medium, 1200px OG), each skipped when already up to date.
  • Web upload (/upload): pick one or many photos, each with its own EXIF-prefilled date, caption and tag-autocompleting tags field; posted with per-photo status and retry.
  • Manage UI (/manage): edit caption/date/tags, bulk tag add/remove, and delete — delete is reversible (moves to content/.trash/, with restore / empty).
  • Tag autocomplete across the upload and manage forms, fed by /tags.json.

Auth & security

  • App-side HTTP Basic auth (bcrypt htpasswd, -htpasswd) on every write endpoint; nginx just terminates TLS and proxies. Failed attempts are rate-limited; state-changing requests get a same-origin (CSRF) check.
  • A signed-in indicator + management toolbar (Upload / Manage / Edit this) appear on every page when authenticated, via a silent /api/session probe (no login dialog for anonymous visitors).

Clients

  • The web upload + manage UI.
  • Android uploader/manager (Kotlin/Compose, android/): pick or share photos, multi-photo bulk upload with captions/dates/tags, plus a Manage screen to edit or delete published posts.
  • Qt desktop uploader (desktop/): bulk upload with configurable concurrency, HEIC support, credentials in the system keychain.

Deployment

  • One Go binary. NixOS flake + module (systemd service, tmpfiles, optional nginx vhost with ACME).

How it works

  1. Drop photo.jpg + photo.toml into the content directory (or upload via the web UI at /upload, the manage UI, or an app).
  2. The binary watches the directory and rebuilds the static site on any change (and on every upload/edit/delete).
  3. nginx terminates TLS, serves public/, and proxies the dynamic endpoints (/upload, /manage, /api/) to the local API. Auth is the app's job now (bcrypt htpasswd via -htpasswd), not nginx's.

For each photo the generator produces a 600×600 square thumbnail for the grid and an aspect-preserved medium JPEG (longest side 1600) for the detail page. The original file stays reachable from a download link on the detail page. Regeneration is skipped when the destination is at least as new as the source, so editing one sidecar doesn't reprocess every photo.

The upload page reads DateTimeOriginal from a JPEG's EXIF (or falls back to the file's modification time) and prefills the date field, which stays editable before submitting.

Sidecar format

caption = "Tempelhof at sunrise"
date    = "2026-06-14"
tags    = ["berlin", "sunrise"]

If no .toml is present the filename is used as the caption and today's date as the date. tags is optional, free-text and lowercased; each tag becomes an album. A tag may contain spaces ("berlin trip"); its URL slug is derived automatically.


Using as a NixOS flake module

1. Add to your flake inputs

inputs = {
  nixpkgs.url        = "github:NixOS/nixpkgs/nixos-unstable";
  photogallery.url   = "github:youruser/photogallery";
};

2. Import the module

nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
  modules = [
    inputs.photogallery.nixosModules.default
    ./configuration.nix
  ];
};

3. Configure

services.photogallery = {
  enable   = true;
  baseURL  = "https://photos.yourdomain.tld";

  # Auth for the upload/manage endpoints: a bcrypt htpasswd file. Handed to the
  # service via systemd LoadCredential, so a root-owned 0400 secret is fine.
  # Omit to leave /upload and /manage open.
  htpasswdFile = "/run/secrets/photogallery-htpasswd";

  # Optional: which view is the site root ("timeline" or "grid")
  defaultView = "timeline";

  # Optional: identity shown on the header and copyright footer
  author  = "Ada Lovelace";
  handle  = "@ada@example.com";
  bio     = "Photos from the field.";
  bioAlt  = "";  # optional secondary line in the alt-script font

  # Optional: swap fonts
  fontsURL    = "https://fonts.googleapis.com/css2?family=EB+Garamond:wght@400;600;700&family=Manjari:wght@400;700&display=swap";
  serifFamily = "EB Garamond";
  mlFamily    = "Manjari";

  # Optional: built-in nginx vhost (TLS + proxy only; auth is app-side above)
  nginx = {
    enable = true;
    domain = "photos.yourdomain.tld";
  };
};

Create the htpasswd file (bcrypt — the app only accepts -B entries):

nix-shell -p apacheHttpd --run \
  "htpasswd -B -c /run/secrets/photogallery-htpasswd youruser"

Available options

Option Default Description
enable false Enable the service
baseURL — Public URL, no trailing slash
contentDir /var/lib/photogallery/content Images and .toml sidecars
outputDir /var/lib/photogallery/public Generated site output
port 8080 Upload API port (localhost only)
htpasswdFile null bcrypt htpasswd guarding write endpoints (null = open)
defaultView timeline Site-root view: timeline or grid
pageSize 60 Photos per page (timeline/grid)
author Ashik Salahudeen Name shown on the header, photo pages and copyright footer
handle @puttaalu@inflo.ws Handle line under the author name
bio Personal photo journal. Primary bio line
bioAlt Malayalam default Secondary bio line in the alt-script font ("" to omit)
fontsURL Crimson Pro + Manjari Stylesheet URL loading both web fonts
serifFamily Crimson Pro CSS font-family for the primary serif
mlFamily Manjari CSS font-family for the alt-script bio
nginx.enable false Configure an nginx vhost (TLS + proxy)
nginx.domain — Domain for the vhost
nginx.enableACME true Enable Let's Encrypt

Bootstrap (first build)

buildGoModule needs a go.sum and the correct vendorHash.

# 1. Generate go.sum
go mod tidy

# 2. Commit go.sum
git add go.sum && git commit -m "add go.sum"

# 3. Get the vendorHash — this will fail and print the correct hash
nix build
# error: hash mismatch ... got: sha256-XXXXX...

# 4. Paste the hash into flake.nix, then build again
nix build

Uploading via curl

curl -u user:password \
     -F "image=@photo.jpg" \
     -F "caption=Tempelhof at sunrise" \
     -F "date=2026-06-14" \
     -F "tags=berlin, sunrise" \
     https://photos.yourdomain.tld/upload

Or open https://photos.yourdomain.tld/upload in a browser, manage existing posts at /manage, or use one of the apps below.

Apps

Both use the same API (HTTP Basic auth) and need no server change.

  • Android (android/) — Kotlin/Compose. Pick or share one or many photos, set caption/date/tags, uploads run in a durable background queue. A Manage screen lists published posts (with thumbnails) and edits their caption/date/tags or deletes them to the server's trash, over the same /api/* endpoints the web /manage uses. Has its own flake; cd android && nix develop && ./gradlew assembleRelease.
  • Qt desktop (desktop/) — Qt6/C++. Bulk upload with configurable concurrency, HEIC support, credentials stored in the system keychain. Builds via its own flake in desktop/.
S
Description
No description provided
Readme Unlicense
2 MiB
0 Stars 1 Watchers 0 Forks
Languages
Kotlin 28.5%
Go 25.7%
C++ 19.9%
HTML 14.2%
Nix 4.1%
Other 7.6%