Skip to content

Commit 8de4dcf

Browse files
authored
feat: add migration to add frontmatter to markdown files (#565)
1 parent 8ffdc61 commit 8de4dcf

File tree

6 files changed

+557
-20
lines changed

6 files changed

+557
-20
lines changed

cmd/leafwiki/main.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"flag"
55
"fmt"
66
"log"
7+
"log/slog"
78
"os"
89
"strings"
910
"time"
@@ -53,7 +54,22 @@ func printUsage() {
5354
`)
5455
}
5556

57+
func setupLogger() {
58+
level := slog.LevelInfo
59+
if os.Getenv("LOG_LEVEL") == "debug" {
60+
level = slog.LevelDebug
61+
}
62+
63+
handler := slog.NewJSONHandler(os.Stdout, &slog.HandlerOptions{
64+
Level: level,
65+
AddSource: true,
66+
})
67+
68+
slog.SetDefault(slog.New(handler))
69+
}
70+
5671
func main() {
72+
setupLogger()
5773

5874
// flags
5975
hostFlag := flag.String("host", "", "host/IP address to bind the server to (e.g. 127.0.0.1 or 0.0.0.0)")

internal/core/tree/errors.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ var ErrMovePageCircularReference = errors.New("circular reference detected")
1111
var ErrPageCannotBeMovedToItself = errors.New("page cannot be moved to itself")
1212
var ErrInvalidSortOrder = errors.New("invalid sort order")
1313
var ErrFrontmatterParse = errors.New("frontmatter parse error")
14+
var ErrFileNotFound = errors.New("file not found")

internal/core/tree/page_store.go

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,19 @@ func (f *PageStore) MovePage(entry *PageNode, parentEntry *PageNode) error {
308308
return nil
309309
}
310310

311+
// ReadPageRaw returns the raw content of a page including frontmatter
312+
func (f *PageStore) ReadPageRaw(entry *PageNode) (string, error) {
313+
filePath, err := f.getFilePath(entry)
314+
if err != nil {
315+
return "", err
316+
}
317+
raw, err := os.ReadFile(filePath)
318+
if err != nil {
319+
return "", err
320+
}
321+
return string(raw), nil
322+
}
323+
311324
// ReadPageContent returns the content of a page
312325
func (f *PageStore) ReadPageContent(entry *PageNode) (string, error) {
313326
if entry == nil {
@@ -362,5 +375,5 @@ func (f *PageStore) getFilePath(entry *PageNode) (string, error) {
362375
return path.Join(entryPath, "index.md"), nil
363376
}
364377

365-
return "", errors.New("file not found")
378+
return "", ErrFileNotFound
366379
}

internal/core/tree/schema.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"path/filepath"
88
)
99

10-
const CurrentSchemaVersion = 1
10+
const CurrentSchemaVersion = 2
1111

1212
type SchemaInfo struct {
1313
Version int `json:"version"`

internal/core/tree/tree_service.go

Lines changed: 139 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
package tree
22

33
import (
4+
"errors"
45
"fmt"
5-
"log"
6+
"log/slog"
67
"os"
78
"sort"
89
"strings"
@@ -19,6 +20,7 @@ type TreeService struct {
1920
treeFilename string
2021
tree *PageNode
2122
store *PageStore
23+
log *slog.Logger
2224

2325
mu sync.RWMutex
2426
}
@@ -30,6 +32,7 @@ func NewTreeService(storageDir string) *TreeService {
3032
treeFilename: "tree.json",
3133
tree: nil,
3234
store: NewPageStore(storageDir),
35+
log: slog.Default().With("component", "TreeService"),
3336
}
3437
}
3538

@@ -47,42 +50,56 @@ func (t *TreeService) LoadTree() error {
4750
}
4851

4952
// Load the schema version
50-
log.Printf("Checking schema version...")
53+
t.log.Info("Checking schema version...")
5154
schema, err := loadSchema(t.storageDir)
5255
if err != nil {
53-
log.Printf("Error loading schema: %v", err)
56+
t.log.Error("Error loading schema", "error", err)
5457
return err
5558
}
5659

5760
if schema.Version < CurrentSchemaVersion {
58-
log.Printf("Migrating schema from version %d to %d...", schema.Version, CurrentSchemaVersion)
61+
t.log.Info("Migrating schema", "fromVersion", schema.Version, "toVersion", CurrentSchemaVersion)
5962
if err := t.migrate(schema.Version); err != nil {
60-
log.Printf("Error migrating schema: %v", err)
63+
t.log.Error("Error migrating schema", "error", err)
6164
return err
6265
}
63-
64-
// migration was successful, update schema version
65-
if err := saveSchema(t.storageDir, CurrentSchemaVersion); err != nil {
66-
log.Printf("Error saving schema: %v", err)
67-
return err
68-
}
69-
70-
return t.saveTreeLocked()
7166
}
7267

7368
return err
7469
}
7570

7671
func (t *TreeService) migrate(fromVersion int) error {
77-
if fromVersion < 1 {
78-
if err := t.migrateTreeToV1Schema(); err != nil {
72+
73+
for v := fromVersion; v < CurrentSchemaVersion; v++ {
74+
switch v {
75+
case 0:
76+
if err := t.migrateToV1(); err != nil {
77+
t.log.Error("Error migrating to v1", "error", err)
78+
return err
79+
}
80+
case 1:
81+
if err := t.migrateToV2(); err != nil {
82+
t.log.Error("Error migrating to v2", "error", err)
83+
return err
84+
}
85+
}
86+
87+
// Save the tree after each migration step
88+
if err := t.saveTreeLocked(); err != nil {
89+
t.log.Error("Error saving tree after migration", "version", v+1, "error", err)
90+
return err
91+
}
92+
93+
// Update the schema version file
94+
if err := saveSchema(t.storageDir, v+1); err != nil {
95+
t.log.Error("Error saving schema", "version", v+1, "error", err)
7996
return err
8097
}
8198
}
8299
return nil
83100
}
84101

85-
func (t *TreeService) migrateTreeToV1Schema() error {
102+
func (t *TreeService) migrateToV1() error {
86103
// Backfill metadata for all pages
87104
var backfillMetadata func(node *PageNode) error
88105
backfillMetadata = func(node *PageNode) error {
@@ -98,7 +115,7 @@ func (t *TreeService) migrateTreeToV1Schema() error {
98115
// Log the error and continue
99116
// We still want to backfill metadata for other nodes
100117
// but we cannot do it for this node
101-
log.Printf("could not get file path for node %s: %v", node.ID, err)
118+
t.log.Error("Could not get file path for node", "nodeID", node.ID, "error", err)
102119
return nil
103120
}
104121

@@ -111,7 +128,7 @@ func (t *TreeService) migrateTreeToV1Schema() error {
111128
createdAt = info.ModTime().UTC()
112129
updatedAt = info.ModTime().UTC()
113130
} else if !os.IsNotExist(err) {
114-
log.Printf("could not stat file for node %s at path %s: %v", node.ID, filePath, err)
131+
t.log.Error("Could not stat file for node", "nodeID", node.ID, "filePath", filePath, "error", err)
115132
}
116133

117134
node.Metadata = PageMetadata{
@@ -129,9 +146,113 @@ func (t *TreeService) migrateTreeToV1Schema() error {
129146
return nil
130147
}
131148

149+
if t.tree == nil {
150+
return ErrTreeNotLoaded
151+
}
152+
132153
return backfillMetadata(t.tree)
133154
}
134155

156+
// migrateToV2 migrates the tree to the v2 schema
157+
// Adds frontmatter to all existing pages if missing
158+
func (t *TreeService) migrateToV2() error {
159+
// Traverse all pages and add frontmatter if missing
160+
var addFrontmatter func(node *PageNode) error
161+
addFrontmatter = func(node *PageNode) error {
162+
// Read the content of the page
163+
content, err := t.store.ReadPageRaw(node)
164+
if err != nil {
165+
if errors.Is(err, os.ErrNotExist) || errors.Is(err, ErrFileNotFound) {
166+
t.log.Warn("Page file does not exist, skipping frontmatter addition", "nodeID", node.ID)
167+
// Recurse into children
168+
for _, child := range node.Children {
169+
if err := addFrontmatter(child); err != nil {
170+
t.log.Error("Error adding frontmatter to child node", "nodeID", child.ID, "error", err)
171+
return err
172+
}
173+
}
174+
return nil
175+
}
176+
t.log.Error("Could not read page content for node", "nodeID", node.ID, "error", err)
177+
return fmt.Errorf("could not read page content for node %s: %v", node.ID, err)
178+
}
179+
180+
// Parse the frontmatter
181+
fm, body, has, err := ParseFrontmatter(content)
182+
if err != nil {
183+
t.log.Error("Could not parse frontmatter for node", "nodeID", node.ID, "error", err)
184+
return fmt.Errorf("could not parse frontmatter for node %s: %v", node.ID, err)
185+
}
186+
187+
// Decide if we need to change anything
188+
changed := false
189+
190+
// If there is no frontmatter, start with a new one
191+
if !has {
192+
fm = Frontmatter{}
193+
changed = true
194+
}
195+
196+
// Ensure required fields exist
197+
if strings.TrimSpace(fm.LeafWikiID) == "" {
198+
fm.LeafWikiID = node.ID
199+
changed = true
200+
}
201+
// Optional but nice: keep title in sync *at least once*
202+
// (you might choose to NOT overwrite existing title)
203+
if strings.TrimSpace(fm.LeafWikiTitle) == "" {
204+
fm.LeafWikiTitle = node.Title
205+
changed = true
206+
}
207+
208+
// Only write if changed
209+
if changed {
210+
newContent, err := BuildMarkdownWithFrontmatter(fm, body)
211+
if err != nil {
212+
t.log.Error("could not build markdown with frontmatter", "nodeID", node.ID, "error", err)
213+
return fmt.Errorf("could not build markdown with frontmatter for node %s: %w", node.ID, err)
214+
}
215+
216+
filePath, err := t.store.getFilePath(node)
217+
if err != nil {
218+
t.log.Error("could not get file path", "nodeID", node.ID, "error", err)
219+
return fmt.Errorf("could not get file path for node %s: %w", node.ID, err)
220+
}
221+
222+
if err := writeFileAtomic(filePath, []byte(newContent), 0o644); err != nil {
223+
t.log.Error("could not write updated page content", "nodeID", node.ID, "filePath", filePath, "error", err)
224+
return fmt.Errorf("could not write updated page content for node %s: %w", node.ID, err)
225+
}
226+
227+
t.log.Info("frontmatter backfilled", "nodeID", node.ID, "path", filePath)
228+
}
229+
230+
// Recurse into children
231+
for _, child := range node.Children {
232+
if err := addFrontmatter(child); err != nil {
233+
t.log.Error("Error adding frontmatter to child node", "nodeID", child.ID, "error", err)
234+
return err
235+
}
236+
}
237+
238+
return nil
239+
}
240+
241+
if t.tree == nil {
242+
return ErrTreeNotLoaded
243+
}
244+
245+
// start the recursion from the children of the root
246+
for _, child := range t.tree.Children {
247+
if err := addFrontmatter(child); err != nil {
248+
t.log.Error("Error adding frontmatter to child node", "nodeID", child.ID, "error", err)
249+
return err
250+
}
251+
}
252+
253+
return nil
254+
}
255+
135256
// SaveTree saves the tree to the storage directory
136257
func (t *TreeService) SaveTree() error {
137258
t.mu.Lock()

0 commit comments

Comments
 (0)