feat: notify document updates over websocket
This commit is contained in:
@@ -10,6 +10,7 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/tim/md-hub-secure/apps/server/internal/markdown"
|
||||
"github.com/tim/md-hub-secure/apps/server/internal/store"
|
||||
@@ -21,6 +22,15 @@ type Service struct {
|
||||
renderer *markdown.Renderer
|
||||
repo *Repository
|
||||
logger *slog.Logger
|
||||
mu sync.Mutex
|
||||
onChange func(DocumentChange)
|
||||
}
|
||||
|
||||
type DocumentChange struct {
|
||||
Path string `json:"path"`
|
||||
Title string `json:"title"`
|
||||
Hash string `json:"hash"`
|
||||
PreviousHash string `json:"previousHash,omitempty"`
|
||||
}
|
||||
|
||||
type Page struct {
|
||||
@@ -41,27 +51,56 @@ func NewService(sourceDir string, store *store.ContentStore, renderer *markdown.
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) SyncSourceDir(ctx context.Context) error {
|
||||
return filepath.WalkDir(s.sourceDir, func(path string, entry fs.DirEntry, err error) error {
|
||||
func (s *Service) OnChange(callback func(DocumentChange)) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.onChange = callback
|
||||
}
|
||||
|
||||
func (s *Service) SyncSourceDir(ctx context.Context) ([]DocumentChange, error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
var changes []DocumentChange
|
||||
err := filepath.WalkDir(s.sourceDir, func(path string, entry fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if entry.IsDir() || filepath.Ext(path) != ".md" {
|
||||
return nil
|
||||
}
|
||||
return s.syncFile(ctx, path)
|
||||
|
||||
change, err := s.syncFile(ctx, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if change != nil {
|
||||
changes = append(changes, *change)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if s.onChange != nil {
|
||||
for _, change := range changes {
|
||||
s.onChange(change)
|
||||
}
|
||||
}
|
||||
|
||||
return changes, nil
|
||||
}
|
||||
|
||||
func (s *Service) ListDocuments(ctx context.Context) ([]DocumentRecord, error) {
|
||||
if err := s.SyncSourceDir(ctx); err != nil {
|
||||
if _, err := s.SyncSourceDir(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s.repo.ListDocuments(ctx)
|
||||
}
|
||||
|
||||
func (s *Service) LoadPage(ctx context.Context, requestPath string) (*Page, error) {
|
||||
if err := s.SyncSourceDir(ctx); err != nil {
|
||||
if _, err := s.SyncSourceDir(ctx); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -101,31 +140,31 @@ func normalizeRequestPath(path string) string {
|
||||
return path
|
||||
}
|
||||
|
||||
func (s *Service) syncFile(ctx context.Context, path string) error {
|
||||
func (s *Service) syncFile(ctx context.Context, path string) (*DocumentChange, error) {
|
||||
content, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read content file %s: %w", path, err)
|
||||
return nil, fmt.Errorf("read content file %s: %w", path, err)
|
||||
}
|
||||
|
||||
rendered, err := s.renderer.Render(content)
|
||||
if err != nil {
|
||||
return fmt.Errorf("render content file %s: %w", path, err)
|
||||
return nil, fmt.Errorf("render content file %s: %w", path, err)
|
||||
}
|
||||
|
||||
record, err := s.store.PutBytes(content)
|
||||
if err != nil {
|
||||
return fmt.Errorf("store content file %s: %w", path, err)
|
||||
return nil, fmt.Errorf("store content file %s: %w", path, err)
|
||||
}
|
||||
|
||||
relative, err := filepath.Rel(s.sourceDir, path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("compute relative path %s: %w", path, err)
|
||||
return nil, fmt.Errorf("compute relative path %s: %w", path, err)
|
||||
}
|
||||
relative = filepath.ToSlash(relative)
|
||||
|
||||
existing, err := s.repo.GetDocumentByPath(ctx, relative)
|
||||
if err != nil && !errors.Is(err, sql.ErrNoRows) {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
previousHash := ""
|
||||
@@ -134,7 +173,7 @@ func (s *Service) syncFile(ctx context.Context, path string) error {
|
||||
previousHash = existing.CurrentHash
|
||||
documentID = existing.ID
|
||||
if existing.CurrentHash == record.Hash {
|
||||
return nil
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -146,9 +185,14 @@ func (s *Service) syncFile(ctx context.Context, path string) error {
|
||||
PreviousHash: previousHash,
|
||||
Tags: rendered.Tags,
|
||||
}); err != nil {
|
||||
return err
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Debug("synced document", "path", relative, "hash", record.Hash)
|
||||
return nil
|
||||
return &DocumentChange{
|
||||
Path: relative,
|
||||
Title: rendered.Title,
|
||||
Hash: record.Hash,
|
||||
PreviousHash: previousHash,
|
||||
}, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user