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`
|
- Serves a multipart upload API at `POST /upload`
|
||||||
- In dev mode (`--serve`), also serves `public/` as static files
|
- 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.
|
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
|
### 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.
|
- 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.
|
- 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";
|
serifFamily = "EB Garamond";
|
||||||
mlFamily = "Manjari";
|
mlFamily = "Manjari";
|
||||||
|
|
||||||
# Optional: edit and delete published photos at /manage (off by default)
|
|
||||||
manage.enable = true;
|
|
||||||
|
|
||||||
# Optional: built-in nginx vhost
|
# Optional: built-in nginx vhost
|
||||||
nginx = {
|
nginx = {
|
||||||
enable = true;
|
enable = true;
|
||||||
@@ -106,7 +103,6 @@ nix-shell -p apacheHttpd --run \
|
|||||||
| `mlFamily` | `Manjari` | CSS font-family for the alt-script bio |
|
| `mlFamily` | `Manjari` | CSS font-family for the alt-script bio |
|
||||||
| `pageSize` | `60` | Photos per page on the timeline and grid views |
|
| `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/` |
|
| `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.enable` | `false` | Configure an nginx vhost |
|
||||||
| `nginx.domain` | — | Domain for the vhost |
|
| `nginx.domain` | — | Domain for the vhost |
|
||||||
| `nginx.htpasswdFile` | — | htpasswd file for `/upload` and `/manage` |
|
| `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
|
## Editing and deleting photos
|
||||||
|
|
||||||
The pages are off by default: start the binary with `-manage` (NixOS:
|
Open `https://photos.yourdomain.tld/manage` in a browser. It lists every
|
||||||
`services.photogallery.manage.enable = true`). Then 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; *Edit* opens a form for the caption and date, and saving rewrites the
|
||||||
photo's `.toml` sidecar, which triggers a rebuild.
|
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 {
|
func readPage(t *testing.T, output, rel string) string {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
data, err := os.ReadFile(filepath.Join(output, filepath.FromSlash(rel), "index.html"))
|
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")
|
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")
|
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)")
|
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>/")
|
defaultView := flag.String("default-view", viewTimeline, "view published at the site root: \"timeline\" or \"grid\"; the other one is published under /<name>/")
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
@@ -47,6 +46,6 @@ func main() {
|
|||||||
defer w.Close()
|
defer w.Close()
|
||||||
go w.Watch()
|
go w.Watch()
|
||||||
|
|
||||||
s := NewServer(*port, *contentDir, *outputDir, *serve, *manage, g)
|
s := NewServer(*port, *contentDir, *outputDir, *serve, g)
|
||||||
log.Fatal(s.Listen())
|
log.Fatal(s.Listen())
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-38
@@ -47,7 +47,14 @@ func newTestServer(t *testing.T) *Server {
|
|||||||
|
|
||||||
g := NewGenerator(content, output, "https://photos.example.com",
|
g := NewGenerator(content, output, "https://photos.example.com",
|
||||||
"Ada", "", "", "", "", "Crimson Pro", "Manjari", 60, viewTimeline)
|
"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 {
|
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/.";
|
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 = {
|
nginx = {
|
||||||
enable = lib.mkEnableOption "nginx virtual host for photogallery";
|
enable = lib.mkEnableOption "nginx virtual host for photogallery";
|
||||||
|
|
||||||
@@ -142,7 +140,7 @@ in {
|
|||||||
User = "photogallery";
|
User = "photogallery";
|
||||||
Group = "photogallery";
|
Group = "photogallery";
|
||||||
|
|
||||||
ExecStart = lib.escapeShellArgs ([
|
ExecStart = lib.escapeShellArgs [
|
||||||
"${pkg}/bin/photogallery"
|
"${pkg}/bin/photogallery"
|
||||||
"--content" cfg.contentDir
|
"--content" cfg.contentDir
|
||||||
"--output" cfg.outputDir
|
"--output" cfg.outputDir
|
||||||
@@ -157,7 +155,7 @@ in {
|
|||||||
"--ml-family" cfg.mlFamily
|
"--ml-family" cfg.mlFamily
|
||||||
"--page-size" (toString cfg.pageSize)
|
"--page-size" (toString cfg.pageSize)
|
||||||
"--default-view" cfg.defaultView
|
"--default-view" cfg.defaultView
|
||||||
] ++ lib.optional cfg.manage.enable "--manage");
|
];
|
||||||
|
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
RestartSec = "5s";
|
RestartSec = "5s";
|
||||||
@@ -194,7 +192,7 @@ in {
|
|||||||
extraConfig = "client_max_body_size 20M;";
|
extraConfig = "client_max_body_size 20M;";
|
||||||
};
|
};
|
||||||
|
|
||||||
locations."/manage" = lib.mkIf cfg.manage.enable {
|
locations."/manage" = {
|
||||||
proxyPass = "http://127.0.0.1:${toString cfg.port}";
|
proxyPass = "http://127.0.0.1:${toString cfg.port}";
|
||||||
basicAuth = "gallery";
|
basicAuth = "gallery";
|
||||||
basicAuthFile = cfg.nginx.htpasswdFile;
|
basicAuthFile = cfg.nginx.htpasswdFile;
|
||||||
|
|||||||
@@ -18,43 +18,31 @@ type Server struct {
|
|||||||
contentDir string
|
contentDir string
|
||||||
outputDir string
|
outputDir string
|
||||||
serve bool
|
serve bool
|
||||||
manage bool
|
|
||||||
generator *Generator
|
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{
|
return &Server{
|
||||||
port: port,
|
port: port,
|
||||||
contentDir: contentDir,
|
contentDir: contentDir,
|
||||||
outputDir: outputDir,
|
outputDir: outputDir,
|
||||||
serve: serve,
|
serve: serve,
|
||||||
manage: manage,
|
|
||||||
generator: g,
|
generator: g,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// routes builds the handler. The /manage pages are only registered with
|
func (s *Server) Listen() error {
|
||||||
// -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 {
|
|
||||||
mux := http.NewServeMux()
|
mux := http.NewServeMux()
|
||||||
mux.HandleFunc("GET /upload", s.handleUploadPage)
|
mux.HandleFunc("GET /upload", s.handleUploadPage)
|
||||||
mux.HandleFunc("POST /upload", s.handleUpload)
|
mux.HandleFunc("POST /upload", s.handleUpload)
|
||||||
if s.manage {
|
mux.HandleFunc("GET /manage", s.handleManagePage)
|
||||||
mux.HandleFunc("GET /manage", s.handleManagePage)
|
mux.HandleFunc("GET /manage/edit/{slug}", s.handleEditPage)
|
||||||
mux.HandleFunc("GET /manage/edit/{slug}", s.handleEditPage)
|
mux.HandleFunc("POST /manage/edit/{slug}", s.handleEdit)
|
||||||
mux.HandleFunc("POST /manage/edit/{slug}", s.handleEdit)
|
mux.HandleFunc("GET /manage/delete/{slug}", s.handleDeletePage)
|
||||||
mux.HandleFunc("GET /manage/delete/{slug}", s.handleDeletePage)
|
mux.HandleFunc("POST /manage/delete/{slug}", s.handleDelete)
|
||||||
mux.HandleFunc("POST /manage/delete/{slug}", s.handleDelete)
|
|
||||||
}
|
|
||||||
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
|
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
|
||||||
fmt.Fprintln(w, "ok")
|
fmt.Fprintln(w, "ok")
|
||||||
})
|
})
|
||||||
return mux
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Server) Listen() error {
|
|
||||||
mux := s.routes()
|
|
||||||
|
|
||||||
if s.serve {
|
if s.serve {
|
||||||
// Serve the generated site on the same port — for local development only.
|
// Serve the generated site on the same port — for local development only.
|
||||||
|
|||||||
Reference in New Issue
Block a user