349 lines
9.3 KiB
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
|
|
}
|