Writers that created or rewrote brain files by path followed symlinks: - promote/reject: os.MkdirAll + os.WriteFile of the destination, then os.Remove of the raw source. Now PutRegular + brainfs.Remove; the source is kept when the write is refused. - StampProvenanceFile (brain_edit's re-stamp): provenance.ApplyFile read and rewrote by path. Now ReadRegular + WriteRegular; ApplyFile, now unused, is removed. - BuildWingIndex (_index.md) and wiki.RebuildIndex (wiki/index.md): now PutRegular. - wiki.AppendLog (log.md) and AutoTunnel's fuzzy-candidate log (raw/tunnel-candidates-*.md): now AppendRegular; tunnel's fileExists, now unused, is removed. - New brainfs.Remove: clean path, real parent, removes a final symlink itself, never its target. brain_edit's own atomic rename was already refused behind a symlinked directory by #187's read check; a guard test pins it. Pipeline writes, BackfillRefs, the watcher's raw/ markers and the session archive in cmd/server follow in a separate PR (#158). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
77 lines
2.2 KiB
Go
77 lines
2.2 KiB
Go
package api
|
|
|
|
import (
|
|
"fmt"
|
|
"path/filepath"
|
|
|
|
"github.com/mathiasbq/hyperguild/ingestion/internal/brainfs"
|
|
"github.com/mathiasbq/hyperguild/ingestion/internal/provenance"
|
|
)
|
|
|
|
// StampProvenance applies server-owned provenance (hyperguild#145) to note
|
|
// content. See package provenance for the rule; an error means the stamped
|
|
// note would not read back as stamped, and the write must be refused.
|
|
func StampProvenance(content, origin, writtenBy string) (string, error) {
|
|
return provenance.Apply(content, provenance.Stamp{Origin: origin, WrittenBy: writtenBy})
|
|
}
|
|
|
|
// StampProvenanceFile applies StampProvenance to a brain-relative note path,
|
|
// read and rewritten through the brain root (hyperguild#158).
|
|
func StampProvenanceFile(brainDir, relPath, origin, writtenBy string) error {
|
|
rel, _, err := resolveWithin(brainDir, relPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bfs, err := brainfs.Open(brainDir)
|
|
if err != nil {
|
|
return fmt.Errorf("open brain: %w", err)
|
|
}
|
|
defer func() { _ = bfs.Close() }()
|
|
b, err := bfs.ReadRegular(rel)
|
|
if err != nil {
|
|
return fmt.Errorf("read for provenance: %w", err)
|
|
}
|
|
out, err := provenance.Apply(string(b), provenance.Stamp{Origin: origin, WrittenBy: writtenBy})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if out == string(b) {
|
|
return nil
|
|
}
|
|
if err := bfs.WriteRegular(rel, []byte(out)); err != nil {
|
|
return fmt.Errorf("write provenance: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// putBrainFile creates or replaces the file at abs (inside brainDir) through
|
|
// the brain root, never through a symlinked directory or file (#188).
|
|
func putBrainFile(brainDir, abs string, data []byte) (string, error) {
|
|
rel, err := filepath.Rel(brainDir, abs)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
rel = filepath.ToSlash(rel)
|
|
bfs, err := brainfs.Open(brainDir)
|
|
if err != nil {
|
|
return "", fmt.Errorf("open brain: %w", err)
|
|
}
|
|
defer func() { _ = bfs.Close() }()
|
|
return rel, bfs.PutRegular(rel, data)
|
|
}
|
|
|
|
// removeBrainFile removes the file at abs (inside brainDir) through the
|
|
// brain root.
|
|
func removeBrainFile(brainDir, abs string) error {
|
|
rel, err := filepath.Rel(brainDir, abs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
bfs, err := brainfs.Open(brainDir)
|
|
if err != nil {
|
|
return fmt.Errorf("open brain: %w", err)
|
|
}
|
|
defer func() { _ = bfs.Close() }()
|
|
return bfs.Remove(filepath.ToSlash(rel))
|
|
}
|