From ddfd29ea0e9b2fd4db955169354ce4609ab7ba11 Mon Sep 17 00:00:00 2001 From: luxick Date: Tue, 21 Jul 2026 17:26:31 +0200 Subject: [PATCH] Prevent page deletion on save --- main.go | 53 ++++++++++++++++++++++++++++++----------------------- 1 file changed, 30 insertions(+), 23 deletions(-) diff --git a/main.go b/main.go index 6f8d96f..afb8c0d 100644 --- a/main.go +++ b/main.go @@ -426,9 +426,14 @@ func (h *handler) handlePost(w http.ResponseWriter, r *http.Request, urlPath, fs } rawMD, _ := os.ReadFile(indexPath) sections := splitSections(rawMD) - if sectionIndex < len(sections) { - sections[sectionIndex] = []byte(content) + // Out of range means the file changed under the editor (or the index + // never matched it). Writing back the untouched file would swallow the + // edit silently, so refuse and keep the editor's content in the browser. + if sectionIndex >= len(sections) { + http.Error(w, "section no longer exists — the page changed since you opened the editor", http.StatusConflict) + return } + sections[sectionIndex] = []byte(content) content = string(joinSections(sections)) // Section index ≥ 1 is a heading-anchored section. Redirect to its // anchor so the user lands on the section they just saved, even if @@ -441,28 +446,30 @@ func (h *handler) handlePost(w http.ResponseWriter, r *http.Request, urlPath, fs } } + // A save must never remove a page. An empty POST — a truncated mobile + // request, a lost `section` field, an editor that came up blank — is + // indistinguishable from "clear this page", and deleting index.md on that + // signal loses the whole file even though the user only edited one section. + // Removing a page is the explicit ?delete action's job (moves.go). if strings.TrimSpace(content) == "" { - if err := os.Remove(indexPath); err != nil && !os.IsNotExist(err) { - http.Error(w, "delete failed: "+err.Error(), http.StatusInternalServerError) - return - } - } else { - // Stat first so we know whether MkdirAll actually created the folder - // — if it did, the search index needs a new entry. - _, statErr := os.Stat(fsPath) - newlyCreated := os.IsNotExist(statErr) - if err := os.MkdirAll(fsPath, 0755); err != nil { - http.Error(w, "mkdir failed: "+err.Error(), http.StatusInternalServerError) - return - } - if err := os.WriteFile(indexPath, []byte(content), 0644); err != nil { - http.Error(w, "write failed: "+err.Error(), http.StatusInternalServerError) - return - } - if newlyCreated { - if rel, err := filepath.Rel(h.root, fsPath); err == nil { - folderIndexAdd(filepath.ToSlash(rel)) - } + http.Error(w, "refusing to save empty content — use DELETE to remove this page", http.StatusBadRequest) + return + } + // Stat first so we know whether MkdirAll actually created the folder + // — if it did, the search index needs a new entry. + _, statErr := os.Stat(fsPath) + newlyCreated := os.IsNotExist(statErr) + if err := os.MkdirAll(fsPath, 0755); err != nil { + http.Error(w, "mkdir failed: "+err.Error(), http.StatusInternalServerError) + return + } + if err := os.WriteFile(indexPath, []byte(content), 0644); err != nil { + http.Error(w, "write failed: "+err.Error(), http.StatusInternalServerError) + return + } + if newlyCreated { + if rel, err := filepath.Rel(h.root, fsPath); err == nil { + folderIndexAdd(filepath.ToSlash(rel)) } } // The editor saves via fetch so the save and its result share one history