PhoenixKitKnowledgeBase.Rescan (PhoenixKitKnowledgeBase v0.1.0)

Copy Markdown View Source

Reconciles a vault's index with what is actually on disk.

There is no file watcher by design, so this is how edits made outside the web UI — vim, a script, Obsidian, git pull — become visible. Run it from the Rescan button, or call it from a Mix task or a cron job.

How change is detected

Cheap first, authoritative second. A file whose (mtime, size) still match the index is skipped without being read, which is what keeps a rescan of a large vault fast. Anything else gets hashed, and only a differing hash counts as a real change — so touch, a git checkout that rewrites mtimes, or a save that produced identical bytes cost one read and no reindex.

Renames survive

A file that disappeared plus a new file with identical content is a move, not a delete and a create. Recognising that keeps the note's uuid, and with it every permalink and every backlink pointing at it. Without this step, mv Ideas.md archive/Ideas.md in a shell would quietly break every link into that note — the single most likely thing anyone does outside the app.

Identical content is the only signal available after the fact. Two genuinely distinct notes with byte-identical content are indistinguishable here; pairing them off arbitrarily is still better than dropping both uuids, and the resulting index is correct either way.

One bad file is one error

A file the index cannot hold — binary bytes renamed .md, a path too long to index — is reported in :errors and the scan goes on. Everything per file is guarded, down to an exception: before this, one such file made the whole rescan raise, every time, until somebody found it by hand.

Once files have settled, every link in the vault is rebuilt rather than patched. Adding one note can change what an existing link resolves to, because a nearer same-name file may have appeared — an incremental update cannot see that, so it would leave links quietly pointing at the wrong note.

Summary

Types

What a rescan did, for reporting back to whoever pressed the button.

Functions

An empty report, so callers can render 'nothing to do' without a special case.

True when a rescan found nothing to do — the common case, worth saying plainly.

Reconcile one vault and return what changed.

Reconcile every vault, for a Mix task or a scheduled job.

A one-line summary of a report, in the current locale.

Types

report()

@type report() :: %{
  added: non_neg_integer(),
  updated: non_neg_integer(),
  removed: non_neg_integer(),
  renamed: non_neg_integer(),
  unchanged: non_neg_integer(),
  folders_added: non_neg_integer(),
  folders_removed: non_neg_integer(),
  errors: [%{path: String.t(), reason: String.t()}]
}

What a rescan did, for reporting back to whoever pressed the button.

Functions

empty_report()

@spec empty_report() :: report()

An empty report, so callers can render 'nothing to do' without a special case.

no_changes?(report)

@spec no_changes?(report()) :: boolean()

True when a rescan found nothing to do — the common case, worth saying plainly.

run(vault)

@spec run(PhoenixKitKnowledgeBase.Schemas.Vault.t()) ::
  {:ok, report()} | {:error, term()}

Reconcile one vault and return what changed.

Not run in a transaction, deliberately. A rescan of a big vault would hold a write transaction open for its whole duration, and every step here is independently idempotent — an interrupted rescan leaves a partially updated index that the next run finishes, rather than a rolled-back one that has to start over.

run_all(vaults)

@spec run_all([PhoenixKitKnowledgeBase.Schemas.Vault.t()]) :: [
  {PhoenixKitKnowledgeBase.Schemas.Vault.t(),
   {:ok, report()} | {:error, term()}}
]

Reconcile every vault, for a Mix task or a scheduled job.

summarize(report)

@spec summarize(report()) :: String.t()

A one-line summary of a report, in the current locale.