indexer: skip dotfiles (editor lock/swap) + fault-isolate per-note indexing
A dangling Emacs lock symlink (.#foo.md -> [email protected], created whenever a note is opened in Emacs) matched the .md suffix, entered the note-file walk, and crashed the indexer with io-error when index-note! read the dead symlink — taking down the whole folio MCP server for every client, and leaving any worker that connected during the crash window permanently missing its folio tools.
Two-level fix: - find-md-files-recursive now skips any dotfile (name starting with '.'), cleanly excluding Emacs .#locks, Vim .swp swaps, .DS_Store, etc. Notes are never dotfiles. This is the primary fix (the lock never enters the list). - index-note-safe! wraps per-note indexing in a guard so a single unreadable, dangling, or malformed file logs-and-skips instead of aborting the whole index. One bad file must never be a fleet-wide outage (same class as prior malformed-tags / log crashes).
Test: store-note-files dotfile filtering (prove-fails-first). Version 0.2.6.
Note: the native test backend can't run in a sigil 0.17.16 env (folio pins 0.17.12); filter logic verified via interpreter eval + this test under the matching toolchain at deploy.
package.sgl | 2 +-
src/folio/index.sgl | 12 ++++++++++--
src/folio/store.sgl | 6 ++++++
test/test-note.sgl | 19 +++++++++++++++++++
4 files changed, 36 insertions(+), 3 deletions(-)package.sglmodified
(package name: "folio" version: "0.2.5" version: "0.2.6" sigil: "^0.17" description: "Markdown-native knowledge and project management via MCP" url: "https://codeberg.org/sigil/folio"src/folio/index.sglmodified
(prev (assoc name indexed-dict))) (when (or (not prev) (> disk-mtime (cdr prev))) (index-note! idx file-path) (index-note-safe! idx file-path) (set! reindexed (+ reindexed 1))))) file-dict) (let ((removed 0)) removed: removed total: (length file-dict)))))) ;; Index one note, isolating per-file failures. A single unreadable, ;; dangling, or malformed file must never abort the whole index — folio ;; serves every client from this one indexer, so one bad entry taking it ;; down is a fleet-wide outage. Log and skip instead. (define (index-note-safe! idx f) (guard (exn (#t (log-info "folio: skipping unindexable file" file: f))) (index-note! idx f))) ;;; Drop and rebuild the entire index from scratch. (define (index-rebuild! idx) (: folio-index? -> integer?) (sqlite-exec db "BEGIN TRANSACTION") (sqlite-exec db "DELETE FROM notes_fts") (sqlite-exec db "DELETE FROM notes_meta") (for-each (lambda (f) (index-note! idx f)) files) (for-each (lambda (f) (index-note-safe! idx f)) files) (sqlite-exec db "COMMIT") (log-info "Index rebuilt" count: (length files)) (length files)))src/folio/store.sglmodified
(fold-right (lambda (name acc) (let ((full (path-join dir name))) (cond ;; Skip dotfiles entirely: editor lock/swap files ;; (.#foo.md Emacs locks, .foo.md.swp Vim swaps), ;; .DS_Store, etc. Notes are never dotfiles, and a ;; dangling .#lock symlink otherwise matches .md and ;; crashes the indexer when index-note! reads it. ((string-starts-with? name ".") acc) ((directory? full) (append (find-md-files-recursive full) acc)) ((string-ends-with? name ".md")test/test-note.sglmodified
(assert-true (string-contains? (folio-note-body note) "Some content here.")) (assert-equal 2 (length (note-tags note)))))))));; ============================================================;; store-note-files skips editor lock/dotfiles;; ============================================================(test-group "store-note-files dotfile filtering" (test "excludes .# Emacs lock files, keeps real notes" (call-with-temp-directory (lambda (dir) (let ((store (make-folio-store dir))) (note-create! store "Real Note" '("x") "Body.") ;; Simulate an Emacs lock next to the note. It ends in .md, so the ;; pre-fix walk included it, then index-note! crashed reading the ;; (normally dangling) symlink. The walk must filter it by name. (write-file-string (path-join (store-notes-dir store) ".#real-note.md") "[email protected]") (let ((files (store-note-files store))) (assert-equal 1 (length files)) (assert-true (string-ends-with? (car files) "real-note.md"))))))));; ============================================================;; note-edit!;; ============================================================