feat(manage): delete photos from /manage, prune leftover pages
/manage/delete/{slug} asks for confirmation, then removes the image,
its sidecar and everything generated from them. Builds now drop day
pages of days without photos and pager pages past the last one, so a
delete or a date change leaves nothing behind.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
@@ -13,14 +13,14 @@ import (
|
||||
"github.com/BurntSushi/toml"
|
||||
)
|
||||
|
||||
// The /manage pages edit photos that are already published. Like /upload they
|
||||
// carry no auth of their own: in production nginx puts them behind htpasswd,
|
||||
// and nothing on the public site links to them.
|
||||
// The /manage pages edit and delete photos that are already published. Like
|
||||
// /upload they carry no auth of their own: in production nginx puts them
|
||||
// behind htpasswd, and nothing on the public site links to them.
|
||||
|
||||
// maxEditBody caps the edit form. It carries a caption and a date, nothing else.
|
||||
const maxEditBody = 64 << 10
|
||||
|
||||
// handleManagePage lists every photo with a link to its edit form.
|
||||
// handleManagePage lists every photo with links to edit and delete it.
|
||||
func (s *Server) handleManagePage(w http.ResponseWriter, r *http.Request) {
|
||||
photos, err := s.generator.loadPhotos()
|
||||
if err != nil {
|
||||
@@ -38,6 +38,7 @@ func (s *Server) handleManagePage(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
}
|
||||
data["Deleted"] = r.URL.Query().Get("deleted")
|
||||
s.renderManage(w, "manage.html", data)
|
||||
}
|
||||
|
||||
@@ -111,6 +112,51 @@ func (s *Server) handleEdit(w http.ResponseWriter, r *http.Request) {
|
||||
http.Redirect(w, r, "/manage?saved="+url.QueryEscape(photo.Slug), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// handleDeletePage asks for confirmation before a photo is deleted. Deleting
|
||||
// is a separate page rather than a button in the list so that it always takes
|
||||
// two deliberate steps.
|
||||
func (s *Server) handleDeletePage(w http.ResponseWriter, r *http.Request) {
|
||||
photo, ok := s.findPhoto(r.PathValue("slug"))
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
data := s.managePageData()
|
||||
data["Photo"] = photo
|
||||
s.renderManage(w, "delete.html", data)
|
||||
}
|
||||
|
||||
// handleDelete removes a photo for good: image, sidecar and everything
|
||||
// generated from them. There is no undo.
|
||||
func (s *Server) handleDelete(w http.ResponseWriter, r *http.Request) {
|
||||
if !s.sameOrigin(r) {
|
||||
http.Error(w, "cross-site request refused", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
photo, ok := s.findPhoto(r.PathValue("slug"))
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.generator.Delete(photo.Slug); err != nil {
|
||||
log.Printf("delete %s: %v", photo.Slug, err)
|
||||
http.Error(w, "could not delete photo", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
log.Printf("deleted %s (%q)", photo.Slug, photo.Caption)
|
||||
|
||||
// Build() is mutex-guarded so calling it here and from the watcher is safe.
|
||||
go func() {
|
||||
if err := s.generator.Build(); err != nil {
|
||||
log.Printf("rebuild after delete: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
http.Redirect(w, r, "/manage?deleted="+url.QueryEscape(photo.Caption), http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// findPhoto looks slug up among the photos in the content directory. Matching
|
||||
// against real entries, rather than building a path from the URL, is what
|
||||
// keeps a crafted slug from reaching outside the content directory.
|
||||
@@ -130,8 +176,8 @@ func (s *Server) findPhoto(slug string) (Photo, bool) {
|
||||
|
||||
// sameOrigin reports whether a form post came from the gallery's own pages.
|
||||
// Basic auth credentials are replayed by the browser on every request to the
|
||||
// host, so without this check any other website could submit the edit form
|
||||
// on a logged-in user's behalf.
|
||||
// host, so without this check any other website could submit the edit or
|
||||
// delete form on a logged-in user's behalf.
|
||||
//
|
||||
// The Origin header is compared against -base-url as well as the Host header:
|
||||
// behind nginx the Host that reaches this server is the proxy's upstream
|
||||
|
||||
Reference in New Issue
Block a user