Update README for current features

Bring the README in line with the gallery as it stands: add a Features section
(views and default-view, albums/tags, day pages, link previews, manage/trash,
tag autocomplete, app-side auth, the signed-in indicator, and the Android and
Qt desktop apps). Correct the auth story (bcrypt htpasswd in the app, nginx
only terminates TLS and proxies), move htpasswdFile to the top-level module
option, document defaultView/pageSize/tags, and add an Apps section.
This commit is contained in:
puttaalu committed 2026-10-18 23:16:26 +02:00
1 parent 94859fb0d6
commit e67d25b191
1 file changed
+69 -12
+69 -12
View File
@@ -1,6 +1,9 @@
# photogallery # photogallery
A minimal static photo gallery with an Atom feed and an upload API. 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 uploader apps. No
database, no framework.
## Contributors ## Contributors
@@ -9,11 +12,39 @@ A minimal static photo gallery with an Atom feed and an upload API.
grid as the site root). Both landed here as independent implementations grid as the site root). Both landed here as independent implementations
building on that work. 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`) with EXIF-date prefill and tag autocomplete.
- **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 (Kotlin/Compose, `android/`): pick or share photos, multi-photo bulk upload, captions/dates/tags.
- **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 ## How it works
1. Drop `photo.jpg` + `photo.toml` into the content directory (or upload via the web UI at `/upload`). 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. 2. The binary watches the directory and rebuilds the static site on any change (and on every upload/edit/delete).
3. nginx serves `public/` and proxies `/upload` to the local API, secured with htpasswd. 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 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. and an aspect-preserved medium JPEG (longest side 1600) for the detail page.
@@ -30,9 +61,13 @@ editable before submitting.
```toml ```toml
caption = "Tempelhof at sunrise" caption = "Tempelhof at sunrise"
date = "2026-06-14" 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. 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.
--- ---
@@ -65,6 +100,14 @@ services.photogallery = {
enable = true; enable = true;
baseURL = "https://photos.yourdomain.tld"; 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 # Optional: identity shown on the header and copyright footer
author = "Ada Lovelace"; author = "Ada Lovelace";
handle = "@ada@example.com"; handle = "@ada@example.com";
@@ -76,20 +119,19 @@ services.photogallery = {
serifFamily = "EB Garamond"; serifFamily = "EB Garamond";
mlFamily = "Manjari"; mlFamily = "Manjari";
# Optional: built-in nginx vhost # Optional: built-in nginx vhost (TLS + proxy only; auth is app-side above)
nginx = { nginx = {
enable = true; enable = true;
domain = "photos.yourdomain.tld"; domain = "photos.yourdomain.tld";
htpasswdFile = "/etc/nginx/.htpasswd-gallery";
}; };
}; };
``` ```
Create the htpasswd file: Create the htpasswd file (**bcrypt** — the app only accepts `-B` entries):
```bash ```bash
nix-shell -p apacheHttpd --run \ nix-shell -p apacheHttpd --run \
"htpasswd -c /etc/nginx/.htpasswd-gallery youruser" "htpasswd -B -c /run/secrets/photogallery-htpasswd youruser"
``` ```
### Available options ### Available options
@@ -101,6 +143,9 @@ nix-shell -p apacheHttpd --run \
| `contentDir` | `/var/lib/photogallery/content` | Images and .toml sidecars | | `contentDir` | `/var/lib/photogallery/content` | Images and .toml sidecars |
| `outputDir` | `/var/lib/photogallery/public` | Generated site output | | `outputDir` | `/var/lib/photogallery/public` | Generated site output |
| `port` | `8080` | Upload API port (localhost only) | | `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 | | `author` | `Ashik Salahudeen` | Name shown on the header, photo pages and copyright footer |
| `handle` | `@puttaalu@inflo.ws` | Handle line under the author name | | `handle` | `@puttaalu@inflo.ws` | Handle line under the author name |
| `bio` | `Personal photo journal.` | Primary bio line | | `bio` | `Personal photo journal.` | Primary bio line |
@@ -108,9 +153,8 @@ nix-shell -p apacheHttpd --run \
| `fontsURL` | Crimson Pro + Manjari | Stylesheet URL loading both web fonts | | `fontsURL` | Crimson Pro + Manjari | Stylesheet URL loading both web fonts |
| `serifFamily` | `Crimson Pro` | CSS font-family for the primary serif | | `serifFamily` | `Crimson Pro` | CSS font-family for the primary serif |
| `mlFamily` | `Manjari` | CSS font-family for the alt-script bio | | `mlFamily` | `Manjari` | CSS font-family for the alt-script bio |
| `nginx.enable` | `false` | Configure an nginx vhost | | `nginx.enable` | `false` | Configure an nginx vhost (TLS + proxy) |
| `nginx.domain` | — | Domain for the vhost | | `nginx.domain` | — | Domain for the vhost |
| `nginx.htpasswdFile` | — | htpasswd file for `/upload` |
| `nginx.enableACME` | `true` | Enable Let's Encrypt | | `nginx.enableACME` | `true` | Enable Let's Encrypt |
--- ---
@@ -141,7 +185,20 @@ curl -u user:password \
-F "image=@photo.jpg" \ -F "image=@photo.jpg" \
-F "caption=Tempelhof at sunrise" \ -F "caption=Tempelhof at sunrise" \
-F "date=2026-06-14" \ -F "date=2026-06-14" \
-F "tags=berlin, sunrise" \
https://photos.yourdomain.tld/upload https://photos.yourdomain.tld/upload
``` ```
Or open `https://photos.yourdomain.tld/upload` in a browser. Or open `https://photos.yourdomain.tld/upload` in a browser, manage existing
posts at `/manage`, or use one of the apps below.
## Apps
Both post to the same `/upload` 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. 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/`.