denote-acme/internal/disk/sync.go

349 lines
9.3 KiB
Go

/*
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
}