/* The sync package synchronizes the underlying filesystem with the 9P virtual filesystem by loading the denote metadata from the underlying filesystem, and refreshing the in-memory metadata. Files with denote front matter use the front matter, and all other files use the metadata encoded in the file name. */ package disk import ( p9client "denote/internal/p9/client" "denote/pkg/config" "denote/pkg/metadata" "fmt" "log" "os" "path/filepath" "regexp" "strings" "9fans.net/go/acme" "9fans.net/go/plan9/client" ) // denoteFilePattern matches denote filenames: YYYYMMDDTHHMMSS--title__tags.ext var denoteFilePattern = regexp.MustCompile(`^(\d{8}T\d{6})--`) // WatchAcmeLog monitors Acme's log for Put events and syncs denote file // front matter changes to the 9P filesystem metadata. func WatchAcmeLog() { for { alog, err := acme.Log() if err != nil { log.Printf("sync: failed to open acme log: %v", err) return } for { ev, err := alog.Read() if err != nil { log.Printf("sync: error reading acme log: %v", err) alog.Close() break } // Only handle Put events if ev.Op != "put" { continue } // Check if this is a denote file if !isDenoteFile(ev.Name) { continue } // Extract identifier from filename identifier := extractIdentifier(ev.Name) if identifier == "" { continue } // Check if note exists in 9P exists := noteExists(identifier) if exists { // Existing note: sync front matter if supported (triggers 'u' event) if SupportsFrontMatter(ev.Name) { if err := syncFrontMatter(ev.Name, identifier); err != nil { log.Printf("sync: failed to sync %s: %v", ev.Name, err) } } } else { // New note: register with 9P (triggers 'n' event) if err := registerNewNote(ev.Name); err != nil { log.Printf("sync: failed to register %s: %v", ev.Name, err) } } } } } // isDenoteFile checks if the file is in the denote directory func isDenoteFile(path string) bool { // Try to read current dir from 9p, fall back to default denoteDir := config.DefaultDenoteDir err := p9client.With9P(func(f *client.Fsys) error { dir, readErr := p9client.ReadFile(f, "dir") if readErr == nil && dir != "" { denoteDir = strings.TrimSpace(dir) } return nil // Don't propagate error, just use fallback }) _ = err // Ignore connection errors, use default absPath, err := filepath.Abs(path) if err != nil { return false } return strings.HasPrefix(absPath, denoteDir) } // extractIdentifier extracts the identifier from a denote filename func extractIdentifier(path string) string { filename := filepath.Base(path) matches := denoteFilePattern.FindStringSubmatch(filename) if len(matches) < 2 { return "" } return matches[1] } // noteExists checks if a note exists in 9P func noteExists(identifier string) bool { err := p9client.With9P(func(f *client.Fsys) error { titlePath := "n/" + identifier + "/title" fid, err := f.Open(titlePath, 0) if err != nil { return err } fid.Close() return nil }) return err == nil } // registerNewNote writes to /new to register a new note (triggers 'n' event) func registerNewNote(path string) error { meta, err := ExtractMetadata(path) if err != nil { return err } // Format as 'title' tag1,tag2 var newInput string if len(meta.Tags) > 0 { newInput = fmt.Sprintf("'%s' %s", meta.Title, strings.Join(meta.Tags, ",")) } else { newInput = fmt.Sprintf("'%s'", meta.Title) } return p9client.With9P(func(f *client.Fsys) error { return p9client.WriteFile(f, "new", newInput) }) } // syncFrontMatter reads the file's front matter and writes it to 9P metadata func syncFrontMatter(path, identifier string) error { // Extract front matter from file fm, _, err := ExtractFrontMatter(path) if err != nil { return err } // Write to 9P filesystem return p9client.With9P(func(f *client.Fsys) error { // Write title (triggers 'u' event) titlePath := "n/" + identifier + "/title" if err := p9client.WriteFile(f, titlePath, fm.Title); err != nil { return err } // Write keywords (triggers 'u' event) keywords := strings.Join(fm.Tags, ",") keywordsPath := "n/" + identifier + "/keywords" if err := p9client.WriteFile(f, keywordsPath, keywords); err != nil { return err } // Trigger rename event ctlPath := "n/" + identifier + "/ctl" if err := p9client.WriteFile(f, ctlPath, "r"); err != nil { return err } return nil }) } // UpdateMetadataFunc is a function that updates metadata for a note type UpdateMetadataFunc func(identifier, title, keywords, signature string) error // GetAll reloads all notes from disk to 9P (discards uncommitted 9P changes) func GetAll(updateFunc UpdateMetadataFunc) error { return p9client.With9P(func(f *client.Fsys) error { // Read index to get all identifiers indexFid, err := f.Open("index", 0) if err != nil { return err } defer indexFid.Close() var content []byte buf := make([]byte, 8192) for { n, err := indexFid.Read(buf) if err != nil && err.Error() != "EOF" { return err } if n == 0 { break } content = append(content, buf[:n]...) } // Parse identifiers from index lines := strings.Split(string(content), "\n") for _, line := range lines { line = strings.TrimSpace(line) if line == "" { continue } // Parse identifier from line (format: "denote:ID | title | tags") parts := strings.SplitN(line, "|", 2) if len(parts) < 1 { continue } identifier := strings.TrimSpace(strings.TrimPrefix(parts[0], "denote:")) if identifier == "" { continue } // Read path pathFid, err := f.Open("n/"+identifier+"/path", 0) if err != nil { log.Printf("sync: failed to read path for %s: %v", identifier, err) continue } pathBuf := make([]byte, 8192) n, _ := pathFid.Read(pathBuf) pathFid.Close() path := strings.TrimSpace(string(pathBuf[:n])) // Check if file exists on filesystem if _, err := os.Stat(path); os.IsNotExist(err) { // Path in metadata doesn't exist - search for actual file on disk // Use the directory from the metadata path (handles subdirs like journal/) dir := filepath.Dir(path) pattern := filepath.Join(dir, identifier+"*") matches, err := filepath.Glob(pattern) if err != nil { log.Printf("sync: failed to glob for %s: %v", identifier, err) continue } if len(matches) > 0 { // File exists (maybe encrypted or renamed), update metadata path actualPath := matches[0] if err := p9client.WriteFile(f, "n/"+identifier+"/path", actualPath); err != nil { log.Printf("sync: failed to update path for %s: %v", identifier, err) continue } // Continue to sync the file content path = actualPath } else { // No file on disk - delete metadata (filesystem is source of truth) if err := p9client.WriteFile(f, "n/"+identifier+"/ctl", "d"); err != nil { log.Printf("sync: failed to delete metadata for %s: %v", identifier, err) } continue } } // OPTIMIZATION: Circumvent 9P write operation overhead by directly manipulating in-memory data. var title, keywords, signature string if SupportsFrontMatter(path) { fm, _, err := ExtractFrontMatter(path) if err != nil { log.Printf("get: failed to extract front matter for %s: %v", identifier, err) continue } title = fm.Title keywords = strings.Join(fm.Tags, ",") signature = fm.Signature } else { meta := metadata.ParseFilename(path) title = meta.Title keywords = strings.Join(meta.Tags, ",") signature = meta.Signature } // Update in-memory metadata directly if err := updateFunc(identifier, title, keywords, signature); err != nil { log.Printf("get: failed to update metadata for %s: %v", identifier, err) } } return nil }) } // getDenoteFile reloads a text file with front matter from disk to 9P func getDenoteFile(f *client.Fsys, path, identifier string) error { // Extract front matter from file fm, _, err := ExtractFrontMatter(path) if err != nil { return err } // Write title (triggers 'u' event) titlePath := "n/" + identifier + "/title" if err := p9client.WriteFile(f, titlePath, fm.Title); err != nil { return err } // Write keywords (triggers 'u' event) keywords := strings.Join(fm.Tags, ",") keywordsPath := "n/" + identifier + "/keywords" if err := p9client.WriteFile(f, keywordsPath, keywords); err != nil { return err } // Write signature (triggers 'u' event) signaturePath := "n/" + identifier + "/signature" if err := p9client.WriteFile(f, signaturePath, fm.Signature); err != nil { return err } return nil } // getNonDenoteFile reloads a binary file from disk to 9P (metadata from filename) func getNonDenoteFile(f *client.Fsys, path, identifier string) error { // Extract metadata from filename meta := metadata.ParseFilename(path) // Write title (triggers 'u' event) titlePath := "n/" + identifier + "/title" if err := p9client.WriteFile(f, titlePath, meta.Title); err != nil { return err } // Write keywords (triggers 'u' event) keywords := strings.Join(meta.Tags, ",") keywordsPath := "n/" + identifier + "/keywords" if err := p9client.WriteFile(f, keywordsPath, keywords); err != nil { return err } // Write signature (triggers 'u' event) signaturePath := "n/" + identifier + "/signature" if err := p9client.WriteFile(f, signaturePath, meta.Signature); err != nil { return err } return nil }