The Android app now edits/deletes posts, and the web upload is bulk; reflect the Android manage capability in the Features and Apps sections.
208 lines
9.5 KiB
Markdown
208 lines
9.5 KiB
Markdown
# 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](https://git.planet-express.in/fox)** — 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
|
||
|
||
```toml
|
||
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
|
||
|
||
```nix
|
||
inputs = {
|
||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||
photogallery.url = "github:youruser/photogallery";
|
||
};
|
||
```
|
||
|
||
### 2. Import the module
|
||
|
||
```nix
|
||
nixosConfigurations.myhost = nixpkgs.lib.nixosSystem {
|
||
modules = [
|
||
inputs.photogallery.nixosModules.default
|
||
./configuration.nix
|
||
];
|
||
};
|
||
```
|
||
|
||
### 3. Configure
|
||
|
||
```nix
|
||
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):
|
||
|
||
```bash
|
||
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`.
|
||
|
||
```bash
|
||
# 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
|
||
|
||
```bash
|
||
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/`.
|