11package tree
22
33import (
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
7671func (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
136257func (t * TreeService ) SaveTree () error {
137258 t .mu .Lock ()
0 commit comments