a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
116 lines
3.1 KiB
Go
116 lines
3.1 KiB
Go
package wiki
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"maps"
|
|
"os"
|
|
"path/filepath"
|
|
"sort"
|
|
)
|
|
|
|
// FileResult describes one file the writer materialised on disk.
|
|
// It is returned to the caller in stable (path-sorted) order so
|
|
// tests and MCP responses are deterministic.
|
|
type FileResult struct {
|
|
Path string `json:"path"` // absolute path
|
|
Bytes int `json:"bytes"` // content length in bytes
|
|
SHA256 string `json:"sha256"` // hex digest, lower-case
|
|
}
|
|
|
|
// Writer collects in-memory file payloads, then materialises them on
|
|
// disk via Flush. Idempotent: it only writes when the new content
|
|
// differs from what's already on disk so re-running the wiki keeps
|
|
// mtimes stable for unchanged pages.
|
|
type Writer struct {
|
|
root string
|
|
files map[string][]byte // relative path → contents
|
|
}
|
|
|
|
// NewWriter creates a writer rooted at outputDir. The root directory
|
|
// is created on Flush if it doesn't already exist.
|
|
func NewWriter(outputDir string) *Writer {
|
|
return &Writer{
|
|
root: outputDir,
|
|
files: make(map[string][]byte),
|
|
}
|
|
}
|
|
|
|
// Write queues a file. Path is interpreted relative to the writer's
|
|
// root. Calling Write twice with the same path replaces the prior
|
|
// content — the wiki generator does this when a later page re-renders
|
|
// the same target.
|
|
func (w *Writer) Write(relPath string, content []byte) {
|
|
w.files[filepath.ToSlash(relPath)] = content
|
|
}
|
|
|
|
// Flush materialises every queued file under root, creating
|
|
// intermediate directories as needed. Returns the manifest in
|
|
// path-sorted order.
|
|
func (w *Writer) Flush() ([]FileResult, error) {
|
|
if err := os.MkdirAll(w.root, 0o755); err != nil {
|
|
return nil, fmt.Errorf("create wiki root %q: %w", w.root, err)
|
|
}
|
|
|
|
// Sort paths so the manifest and the on-disk write order are
|
|
// deterministic — useful for golden tests.
|
|
paths := make([]string, 0, len(w.files))
|
|
for p := range w.files {
|
|
paths = append(paths, p)
|
|
}
|
|
sort.Strings(paths)
|
|
|
|
var out []FileResult
|
|
for _, rel := range paths {
|
|
body := w.files[rel]
|
|
abs := filepath.Join(w.root, filepath.FromSlash(rel))
|
|
if err := os.MkdirAll(filepath.Dir(abs), 0o755); err != nil {
|
|
return nil, fmt.Errorf("mkdir %q: %w", filepath.Dir(abs), err)
|
|
}
|
|
// Idempotency: skip the write when on-disk bytes match.
|
|
if existing, err := os.ReadFile(abs); err == nil && bytesEqual(existing, body) {
|
|
out = append(out, FileResult{
|
|
Path: abs,
|
|
Bytes: len(body),
|
|
SHA256: hashHex(body),
|
|
})
|
|
continue
|
|
}
|
|
if err := os.WriteFile(abs, body, 0o644); err != nil {
|
|
return nil, fmt.Errorf("write %q: %w", abs, err)
|
|
}
|
|
out = append(out, FileResult{
|
|
Path: abs,
|
|
Bytes: len(body),
|
|
SHA256: hashHex(body),
|
|
})
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// Files returns the queued (rel-path → content) payload — used by
|
|
// tests that prefer to assert against memory rather than disk.
|
|
func (w *Writer) Files() map[string][]byte {
|
|
out := make(map[string][]byte, len(w.files))
|
|
maps.Copy(out, w.files)
|
|
return out
|
|
}
|
|
|
|
func hashHex(b []byte) string {
|
|
sum := sha256.Sum256(b)
|
|
return hex.EncodeToString(sum[:])
|
|
}
|
|
|
|
func bytesEqual(a, b []byte) bool {
|
|
if len(a) != len(b) {
|
|
return false
|
|
}
|
|
for i := range a {
|
|
if a[i] != b[i] {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
}
|