Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab0bd5b65a | ||
|
|
52ff47217a | ||
|
|
4b85dba8e1 |
@@ -19,13 +19,13 @@ The binary does three things at once:
|
||||
- Serves a multipart upload API at `POST /upload`
|
||||
- In dev mode (`--serve`), also serves `public/` as static files
|
||||
|
||||
Routes: `GET /upload` (HTML upload form), `POST /upload` (multipart), `GET /health`, and with `-manage`: `GET /manage` (list of photos), `GET`/`POST /manage/edit/{slug}` (edit caption and date), `GET`/`POST /manage/delete/{slug}` (confirm, then delete).
|
||||
Routes: `GET /upload` (HTML upload form), `POST /upload` (multipart), `GET /manage` (list of photos), `GET`/`POST /manage/edit/{slug}` (edit caption and date), `GET`/`POST /manage/delete/{slug}` (confirm, then delete), `GET /health`.
|
||||
|
||||
In production nginx sits in front: serves `public/` directly, proxies `/upload` and `/manage` to localhost, htpasswd on both. Note `/health` is **not** proxied — nginx only forwards `location /upload` and `location /manage`, so anything outside the box can only reach those two.
|
||||
|
||||
### Managing photos
|
||||
|
||||
`manage.go` holds the `/manage` handlers, kept apart from the upload code in `server.go`. They are only registered when the binary runs with `-manage` (NixOS `services.photogallery.manage.enable`, which also adds the nginx location); without it the paths 404 and there is no edit or delete code path at all. The pages are rendered on request from the sidecars (not generated into `public/`), and nothing on the public site links to them. Constraints worth knowing before touching it:
|
||||
`manage.go` holds the `/manage` handlers, kept apart from the upload code in `server.go`. The pages are rendered on request from the sidecars (not generated into `public/`), and nothing on the public site links to them. Constraints worth knowing before touching it:
|
||||
|
||||
- An edit only rewrites the `.toml` sidecar. Any key other than `caption` and `date` in a hand-written sidecar is dropped, same as on upload.
|
||||
- Slugs from the URL are matched against the photos actually in `content/`, never joined into a path.
|
||||
|
||||
@@ -69,9 +69,6 @@ services.photogallery = {
|
||||
serifFamily = "EB Garamond";
|
||||
mlFamily = "Manjari";
|
||||
|
||||
# Optional: edit and delete published photos at /manage (off by default)
|
||||
manage.enable = true;
|
||||
|
||||
# Optional: built-in nginx vhost
|
||||
nginx = {
|
||||
enable = true;
|
||||
@@ -106,7 +103,6 @@ nix-shell -p apacheHttpd --run \
|
||||
| `mlFamily` | `Manjari` | CSS font-family for the alt-script bio |
|
||||
| `pageSize` | `60` | Photos per page on the timeline and grid views |
|
||||
| `defaultView` | `timeline` | View at the site root, `timeline` or `grid`; the other is served under `/timeline/` or `/grid/` |
|
||||
| `manage.enable` | `false` | Serve the `/manage` pages (edit, delete) and, with `nginx.enable`, protect them with the same htpasswd |
|
||||
| `nginx.enable` | `false` | Configure an nginx vhost |
|
||||
| `nginx.domain` | — | Domain for the vhost |
|
||||
| `nginx.htpasswdFile` | — | htpasswd file for `/upload` and `/manage` |
|
||||
@@ -147,9 +143,7 @@ Or open `https://photos.yourdomain.tld/upload` in a browser.
|
||||
|
||||
## Editing and deleting photos
|
||||
|
||||
The pages are off by default: start the binary with `-manage` (NixOS:
|
||||
`services.photogallery.manage.enable = true`). Then open
|
||||
`https://photos.yourdomain.tld/manage` in a browser. It lists every
|
||||
Open `https://photos.yourdomain.tld/manage` in a browser. It lists every
|
||||
photo; *Edit* opens a form for the caption and date, and saving rewrites the
|
||||
photo's `.toml` sidecar, which triggers a rebuild.
|
||||
|
||||
|
||||
@@ -37,13 +37,6 @@ func buildSite(t *testing.T, output, defaultView string) {
|
||||
}
|
||||
}
|
||||
|
||||
func writeFile(t *testing.T, path string, data []byte) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func readPage(t *testing.T, output, rel string) string {
|
||||
t.Helper()
|
||||
data, err := os.ReadFile(filepath.Join(output, filepath.FromSlash(rel), "index.html"))
|
||||
|
||||
@@ -20,7 +20,6 @@ func main() {
|
||||
serifFamily := flag.String("serif-family", "Crimson Pro", "CSS font-family for the primary serif text")
|
||||
mlFamily := flag.String("ml-family", "Manjari", "CSS font-family for the alt-script (Malayalam) bio line")
|
||||
pageSize := flag.Int("page-size", 60, "photos per page on the timeline and grid views (timeline packs whole days, so pages vary slightly around this)")
|
||||
manage := flag.Bool("manage", false, "serve the /manage pages for editing and deleting published photos (off by default; put them behind auth in nginx)")
|
||||
defaultView := flag.String("default-view", viewTimeline, "view published at the site root: \"timeline\" or \"grid\"; the other one is published under /<name>/")
|
||||
flag.Parse()
|
||||
|
||||
@@ -47,6 +46,6 @@ func main() {
|
||||
defer w.Close()
|
||||
go w.Watch()
|
||||
|
||||
s := NewServer(*port, *contentDir, *outputDir, *serve, *manage, g)
|
||||
s := NewServer(*port, *contentDir, *outputDir, *serve, g)
|
||||
log.Fatal(s.Listen())
|
||||
}
|
||||
|
||||
+8
-38
@@ -47,7 +47,14 @@ func newTestServer(t *testing.T) *Server {
|
||||
|
||||
g := NewGenerator(content, output, "https://photos.example.com",
|
||||
"Ada", "", "", "", "", "Crimson Pro", "Manjari", 60, viewTimeline)
|
||||
return NewServer("0", content, output, false, true, g)
|
||||
return NewServer("0", content, output, false, g)
|
||||
}
|
||||
|
||||
func writeFile(t *testing.T, path string, data []byte) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(path, data, 0644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
func readMeta(t *testing.T, s *Server, slug string) PhotoMeta {
|
||||
@@ -316,40 +323,3 @@ func TestDeleteRejectsBadInput(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestManageRoutesNeedTheFlag(t *testing.T) {
|
||||
for _, manage := range []bool{false, true} {
|
||||
s := newTestServer(t)
|
||||
s.manage = manage
|
||||
mux := s.routes()
|
||||
|
||||
want := http.StatusNotFound
|
||||
if manage {
|
||||
want = http.StatusOK
|
||||
}
|
||||
for _, path := range []string{"/manage", "/manage/edit/sunrise", "/manage/delete/sunrise"} {
|
||||
w := httptest.NewRecorder()
|
||||
mux.ServeHTTP(w, httptest.NewRequest("GET", path, nil))
|
||||
if w.Code != want {
|
||||
t.Errorf("manage=%v: GET %s = %d, want %d", manage, path, w.Code, want)
|
||||
}
|
||||
}
|
||||
// The delete POST only does something with the flag: without it the
|
||||
// photo must survive, with it (no Origin header, as from curl) it goes.
|
||||
w := httptest.NewRecorder()
|
||||
mux.ServeHTTP(w, httptest.NewRequest("POST", "/manage/delete/sunrise", nil))
|
||||
_, err := os.Stat(filepath.Join(s.contentDir, "sunrise.jpg"))
|
||||
present := err == nil
|
||||
if present == manage {
|
||||
t.Errorf("manage=%v: POST delete = %d, photo present afterwards = %v", manage, w.Code, present)
|
||||
}
|
||||
// /upload and /health are there regardless.
|
||||
for _, path := range []string{"/upload", "/health"} {
|
||||
w := httptest.NewRecorder()
|
||||
mux.ServeHTTP(w, httptest.NewRequest("GET", path, nil))
|
||||
if w.Code != http.StatusOK {
|
||||
t.Errorf("manage=%v: GET %s = %d, want 200", manage, path, w.Code)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
-5
@@ -88,8 +88,6 @@ in {
|
||||
description = "View published at the site root. The other one is published under /timeline/ or /grid/.";
|
||||
};
|
||||
|
||||
manage.enable = lib.mkEnableOption "the /manage pages for editing and deleting published photos (served behind the same htpasswd as /upload)";
|
||||
|
||||
nginx = {
|
||||
enable = lib.mkEnableOption "nginx virtual host for photogallery";
|
||||
|
||||
@@ -142,7 +140,7 @@ in {
|
||||
User = "photogallery";
|
||||
Group = "photogallery";
|
||||
|
||||
ExecStart = lib.escapeShellArgs ([
|
||||
ExecStart = lib.escapeShellArgs [
|
||||
"${pkg}/bin/photogallery"
|
||||
"--content" cfg.contentDir
|
||||
"--output" cfg.outputDir
|
||||
@@ -157,7 +155,7 @@ in {
|
||||
"--ml-family" cfg.mlFamily
|
||||
"--page-size" (toString cfg.pageSize)
|
||||
"--default-view" cfg.defaultView
|
||||
] ++ lib.optional cfg.manage.enable "--manage");
|
||||
];
|
||||
|
||||
Restart = "on-failure";
|
||||
RestartSec = "5s";
|
||||
@@ -194,7 +192,7 @@ in {
|
||||
extraConfig = "client_max_body_size 20M;";
|
||||
};
|
||||
|
||||
locations."/manage" = lib.mkIf cfg.manage.enable {
|
||||
locations."/manage" = {
|
||||
proxyPass = "http://127.0.0.1:${toString cfg.port}";
|
||||
basicAuth = "gallery";
|
||||
basicAuthFile = cfg.nginx.htpasswdFile;
|
||||
|
||||
@@ -18,43 +18,31 @@ type Server struct {
|
||||
contentDir string
|
||||
outputDir string
|
||||
serve bool
|
||||
manage bool
|
||||
generator *Generator
|
||||
}
|
||||
|
||||
func NewServer(port, contentDir, outputDir string, serve, manage bool, g *Generator) *Server {
|
||||
func NewServer(port, contentDir, outputDir string, serve bool, g *Generator) *Server {
|
||||
return &Server{
|
||||
port: port,
|
||||
contentDir: contentDir,
|
||||
outputDir: outputDir,
|
||||
serve: serve,
|
||||
manage: manage,
|
||||
generator: g,
|
||||
}
|
||||
}
|
||||
|
||||
// routes builds the handler. The /manage pages are only registered with
|
||||
// -manage, so a service without the flag has no edit or delete code path at
|
||||
// all and answers 404 there.
|
||||
func (s *Server) routes() *http.ServeMux {
|
||||
func (s *Server) Listen() error {
|
||||
mux := http.NewServeMux()
|
||||
mux.HandleFunc("GET /upload", s.handleUploadPage)
|
||||
mux.HandleFunc("POST /upload", s.handleUpload)
|
||||
if s.manage {
|
||||
mux.HandleFunc("GET /manage", s.handleManagePage)
|
||||
mux.HandleFunc("GET /manage/edit/{slug}", s.handleEditPage)
|
||||
mux.HandleFunc("POST /manage/edit/{slug}", s.handleEdit)
|
||||
mux.HandleFunc("GET /manage/delete/{slug}", s.handleDeletePage)
|
||||
mux.HandleFunc("POST /manage/delete/{slug}", s.handleDelete)
|
||||
}
|
||||
mux.HandleFunc("GET /manage", s.handleManagePage)
|
||||
mux.HandleFunc("GET /manage/edit/{slug}", s.handleEditPage)
|
||||
mux.HandleFunc("POST /manage/edit/{slug}", s.handleEdit)
|
||||
mux.HandleFunc("GET /manage/delete/{slug}", s.handleDeletePage)
|
||||
mux.HandleFunc("POST /manage/delete/{slug}", s.handleDelete)
|
||||
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, "ok")
|
||||
})
|
||||
return mux
|
||||
}
|
||||
|
||||
func (s *Server) Listen() error {
|
||||
mux := s.routes()
|
||||
|
||||
if s.serve {
|
||||
// Serve the generated site on the same port — for local development only.
|
||||
|
||||
Reference in New Issue
Block a user