Rework wikilinks feature
This commit is contained in:
@@ -1,19 +1,14 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// handleMove moves the folder at srcFsPath (wiki URL srcURL) to dstURL. When
|
||||
// updateLinks is true it also rewrites every [[...]] wiki link across the
|
||||
// tree that targets the old path or any descendant; rewritten files are held
|
||||
// in memory for rollback.
|
||||
// handleMove moves the folder at srcFsPath (wiki URL srcURL) to dstURL.
|
||||
//
|
||||
// If the destination folder already exists it is normally a hard conflict.
|
||||
// The one exception is a merge: when the source carries a page (index.md) and
|
||||
@@ -21,7 +16,7 @@ import (
|
||||
// container without clobbering anything. That merge only proceeds when merge
|
||||
// is true; otherwise the client is told a merge is available so it can ask the
|
||||
// user to confirm.
|
||||
func (h *handler) handleMove(w http.ResponseWriter, r *http.Request, srcURL, srcFsPath, dstURL string, updateLinks, merge bool) {
|
||||
func (h *handler) handleMove(w http.ResponseWriter, r *http.Request, srcURL, srcFsPath, dstURL string, merge bool) {
|
||||
oldPath := normalizeMovePath(srcURL)
|
||||
if oldPath == "/" {
|
||||
http.Error(w, "cannot move wiki root", http.StatusBadRequest)
|
||||
@@ -74,69 +69,32 @@ func (h *handler) handleMove(w http.ResponseWriter, r *http.Request, srcURL, src
|
||||
merging = true
|
||||
}
|
||||
|
||||
// Phase 1: optionally walk the tree and rewrite every index.md that
|
||||
// references the moved path. Keep the pre-rewrite bytes in memory so we
|
||||
// can revert on failure. The walker only reads directory listings and
|
||||
// files literally named index.md; hidden directories are pruned. A cheap
|
||||
// substring check skips parsing files that cannot contain a relevant
|
||||
// link.
|
||||
rewritten := map[string][]byte{}
|
||||
if updateLinks {
|
||||
needle := []byte("[[" + oldPath)
|
||||
walkErr := walkIndexFiles(h.root, func(fsPath string) error {
|
||||
orig, err := os.ReadFile(fsPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !bytes.Contains(orig, needle) {
|
||||
return nil
|
||||
}
|
||||
updated, changed := rewriteWikiLinks(orig, oldPath, newPath)
|
||||
if !changed {
|
||||
return nil
|
||||
}
|
||||
if err := writeFileAtomic(fsPath, updated, 0644); err != nil {
|
||||
return fmt.Errorf("write %s: %w", fsPath, err)
|
||||
}
|
||||
rewritten[fsPath] = orig
|
||||
return nil
|
||||
})
|
||||
if walkErr != nil {
|
||||
rollbackRewrites(rewritten)
|
||||
http.Error(w, "rewrite failed: "+walkErr.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 2: create intermediate parent folders for the destination.
|
||||
// Phase 1: create intermediate parent folders for the destination.
|
||||
if parent := filepath.Dir(dstFsPath); parent != "" {
|
||||
if err := os.MkdirAll(parent, 0755); err != nil {
|
||||
rollbackRewrites(rewritten)
|
||||
http.Error(w, "mkdir failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 3: move the source into place. A plain move renames the whole
|
||||
// Phase 2: move the source into place. A plain move renames the whole
|
||||
// folder; a merge moves the source's entries into the existing
|
||||
// destination and drops the emptied source.
|
||||
if merging {
|
||||
if err := mergeFolder(srcFsPath, dstFsPath); err != nil {
|
||||
rollbackRewrites(rewritten)
|
||||
http.Error(w, "merge failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
folderIndexMergeSubtree(strings.TrimPrefix(oldPath, "/"), strings.TrimPrefix(newPath, "/"))
|
||||
} else {
|
||||
if err := os.Rename(srcFsPath, dstFsPath); err != nil {
|
||||
rollbackRewrites(rewritten)
|
||||
http.Error(w, "rename failed: "+err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
folderIndexRenameSubtree(strings.TrimPrefix(oldPath, "/"), strings.TrimPrefix(newPath, "/"))
|
||||
}
|
||||
|
||||
http.Redirect(w, r, wikiTargetHref(newPath), http.StatusSeeOther)
|
||||
http.Redirect(w, r, fileURL(strings.TrimPrefix(newPath, "/"))+"/", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
// canMergeMove reports whether moving onto an existing destination should
|
||||
@@ -248,77 +206,6 @@ func validateAndNormalizeNewPath(raw string) (string, error) {
|
||||
return "/" + trimmed, nil
|
||||
}
|
||||
|
||||
// rewriteWikiLinks returns (newContent, changed). Any [[target]] or
|
||||
// [[target::display]] whose target equals oldPath or begins with oldPath+"/"
|
||||
// has its target rewritten to the corresponding position under newPath.
|
||||
func rewriteWikiLinks(content []byte, oldPath, newPath string) ([]byte, bool) {
|
||||
changed := false
|
||||
out := wikiLinkPattern.ReplaceAllFunc(content, func(match []byte) []byte {
|
||||
parts := wikiLinkPattern.FindSubmatch(match)
|
||||
if parts == nil {
|
||||
return match
|
||||
}
|
||||
target := strings.TrimSpace(string(parts[1]))
|
||||
normTarget := normalizeMovePath(target)
|
||||
var newTarget string
|
||||
switch {
|
||||
case normTarget == oldPath:
|
||||
newTarget = newPath
|
||||
case strings.HasPrefix(normTarget, oldPath+"/"):
|
||||
newTarget = newPath + strings.TrimPrefix(normTarget, oldPath)
|
||||
default:
|
||||
return match
|
||||
}
|
||||
changed = true
|
||||
suffix := ""
|
||||
if len(parts[2]) > 0 {
|
||||
suffix = "::" + string(parts[2])
|
||||
}
|
||||
return []byte("[[" + newTarget + suffix + "]]")
|
||||
})
|
||||
return out, changed
|
||||
}
|
||||
|
||||
// rollbackRewrites restores the given files to their pre-rewrite contents.
|
||||
// Errors are logged; best-effort since we're already in a failure path.
|
||||
func rollbackRewrites(rewritten map[string][]byte) {
|
||||
for path, orig := range rewritten {
|
||||
if err := writeFileAtomic(path, orig, 0644); err != nil {
|
||||
log.Printf("rollback %s: %v", path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// walkIndexFiles visits every `index.md` under root, skipping hidden
|
||||
// directories (names beginning with `.`). Unlike filepath.WalkDir this does
|
||||
// not stat each regular file — on spinning disks that saves the bulk of the
|
||||
// traversal cost when folders contain many non-page files (photos, archives).
|
||||
func walkIndexFiles(root string, visit func(fsPath string) error) error {
|
||||
entries, err := os.ReadDir(root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, e := range entries {
|
||||
name := e.Name()
|
||||
if strings.HasPrefix(name, ".") {
|
||||
continue
|
||||
}
|
||||
full := filepath.Join(root, name)
|
||||
if e.IsDir() {
|
||||
if err := walkIndexFiles(full, visit); err != nil {
|
||||
return err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if name == "index.md" {
|
||||
if err := visit(full); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeFileAtomic writes data to a temp file in the same directory as path
|
||||
// and renames it into place so readers never observe a partial file.
|
||||
func writeFileAtomic(path string, data []byte, perm os.FileMode) error {
|
||||
|
||||
Reference in New Issue
Block a user