feat(legacymove): also clear raw/ leftovers from before inbox zero (#209)
PlanRaw: an original that was archived but never removed (it has a .processed marker AND a byte-identical copy under raw/processed/) is deleted with its marker; a marker without a note is deleted; an unmarked note, a note without an identical archive copy, and AutoTunnel's tunnel-candidates-* review queue are never touched. The archive key flattens the whole path so raw/x.md and wiki/sources/x.md cannot overwrite each other. Live dry run: 84 entries, 16 move, 53 delete (24 junk + 29 raw leftovers), 15 left. Mutation-checked. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,11 +11,11 @@
|
||||
package legacymove
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -150,7 +150,7 @@ func Apply(brainDir string, items []Item, now time.Time) (Result, error) {
|
||||
if err != nil {
|
||||
return res, fmt.Errorf("read %s: %w", it.From, err)
|
||||
}
|
||||
if err := bfs.PutRegular(res.ArchiveDir+"/originals/"+path.Base(it.From), content); err != nil {
|
||||
if err := bfs.PutRegular(res.ArchiveDir+"/originals/"+archiveKey(it.From), content); err != nil {
|
||||
return res, fmt.Errorf("archive %s: %w", it.From, err)
|
||||
}
|
||||
switch it.Action {
|
||||
@@ -205,7 +205,7 @@ func Rollback(brainDir, archiveDir string) error {
|
||||
return fmt.Errorf("parse manifest: %w", err)
|
||||
}
|
||||
restore := func(from string) error {
|
||||
orig, err := bfs.ReadRegular(archiveDir + "/originals/" + path.Base(from))
|
||||
orig, err := bfs.ReadRegular(archiveDir + "/originals/" + archiveKey(from))
|
||||
if err != nil {
|
||||
return fmt.Errorf("read archived %s: %w", from, err)
|
||||
}
|
||||
@@ -230,6 +230,11 @@ func Rollback(brainDir, archiveDir string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// archiveKey flattens a brain path into one file name, so two pages with the
|
||||
// same basename in different places (raw/x.md and wiki/sources/x.md) never
|
||||
// overwrite each other in the archive.
|
||||
func archiveKey(p string) string { return strings.ReplaceAll(p, "/", "__") }
|
||||
|
||||
// stamp inserts wing/hall after the opening fence, leaving every other byte
|
||||
// alone. A page without frontmatter gets a block.
|
||||
func stamp(content, wing string) string {
|
||||
@@ -252,3 +257,89 @@ func frontmatterValue(content, key string) string {
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
const rawDir = "raw"
|
||||
|
||||
// PlanRaw finds what the watcher left in raw/ before inbox zero: originals that
|
||||
// were archived but never removed, with their .processed markers, and markers
|
||||
// whose note is gone. An original is deleted only when a byte-identical copy
|
||||
// exists under raw/processed/ (so nothing is lost), and only when it has a
|
||||
// marker (an unmarked note is still in the inbox). tunnel-candidates-* are
|
||||
// AutoTunnel's own review queue and are never touched.
|
||||
func PlanRaw(brainDir string) ([]Item, error) {
|
||||
bfs, err := brainfs.Open(brainDir)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open brain: %w", err)
|
||||
}
|
||||
defer func() { _ = bfs.Close() }()
|
||||
names, err := bfs.ReadDirRegular(rawDir)
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read %s: %w", rawDir, err)
|
||||
}
|
||||
present := map[string]bool{}
|
||||
for _, n := range names {
|
||||
present[n] = true
|
||||
}
|
||||
archived, err := archivedCopies(bfs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var items []Item
|
||||
for _, n := range names {
|
||||
if strings.HasPrefix(n, "tunnel-candidates-") {
|
||||
continue
|
||||
}
|
||||
switch {
|
||||
case strings.HasSuffix(n, ".processed"):
|
||||
note := strings.TrimSuffix(n, ".processed")
|
||||
if !present[note] {
|
||||
items = append(items, Item{From: rawDir + "/" + n, Slug: n, Action: Delete, Reason: "marker without a note"})
|
||||
}
|
||||
case present[n+".processed"]:
|
||||
content, rerr := bfs.ReadRegular(rawDir + "/" + n)
|
||||
if rerr != nil {
|
||||
items = append(items, Item{From: rawDir + "/" + n, Slug: n, Action: Skip, Reason: fmt.Sprintf("unreadable: %v", rerr)})
|
||||
continue
|
||||
}
|
||||
if !archived[sha256.Sum256(content)] {
|
||||
items = append(items, Item{From: rawDir + "/" + n, Slug: n, Action: Skip, Reason: "marked processed but no identical copy under raw/processed/; kept"})
|
||||
continue
|
||||
}
|
||||
items = append(items,
|
||||
Item{From: rawDir + "/" + n, Slug: n, Action: Delete, Reason: "archived copy exists under raw/processed/"},
|
||||
Item{From: rawDir + "/" + n + ".processed", Slug: n + ".processed", Action: Delete, Reason: "marker of an archived note"})
|
||||
}
|
||||
}
|
||||
sort.SliceStable(items, func(i, j int) bool { return items[i].From < items[j].From })
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// archivedCopies returns the SHA-256 of every file under raw/processed/, as a
|
||||
// set, so "is there an identical copy" is one lookup.
|
||||
func archivedCopies(bfs *brainfs.Brain) (map[[32]byte]bool, error) {
|
||||
out := map[[32]byte]bool{}
|
||||
days, err := bfs.ReadDirDirs(rawDir + "/processed")
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
return out, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("list %s/processed: %w", rawDir, err)
|
||||
}
|
||||
for _, d := range days {
|
||||
files, err := bfs.ReadDirRegular(rawDir + "/processed/" + d)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
for _, f := range files {
|
||||
b, err := bfs.ReadRegular(rawDir + "/processed/" + d + "/" + f)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
out[sha256.Sum256(b)] = true
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user