{"record":{"id":"08aa1ae8416434e0","repo":"siyuan-note/siyuan","slug":"obsidian-source-file-changed","errorCode":null,"errorMessage":"Obsidian source file changed","messagePattern":"Obsidian source file changed","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"kernel/model/import_obsidian.go","lineNumber":235,"sourceCode":"\nfunc (err *obsidianUserError) Unwrap() error {\n\treturn err.Cause\n}\n\nfunc newObsidianUserError(detailLanguage int, relPath string, cause error) error {\n\treturn &obsidianUserError{DetailLanguage: detailLanguage, RelPath: relPath, Cause: cause}\n}\n\nvar (\n\tobsidianTasksMu                 sync.Mutex\n\tobsidianTasks                   = map[string]*obsidianTask{}\n\tobsidianActive                  string\n\terrObsidianVaultUnreadable      = errors.New(\"Obsidian Vault is unreadable\")\n\terrObsidianVaultNotDirectory    = errors.New(\"Obsidian Vault path is not a directory\")\n\terrObsidianVaultUnsafePath      = errors.New(\"Obsidian Vault path is unsafe\")\n\terrObsidianVaultConfigMissing   = errors.New(\"Obsidian Vault config directory is missing\")\n\terrObsidianVaultMarkdownMissing = errors.New(\"Obsidian Vault has no readable Markdown\")\n\terrObsidianSourceChanged        = errors.New(\"Obsidian source file changed\")\n\n\tobsidianBlockIDPattern  = regexp.MustCompile(`(?m)(?:^|[ \\t])\\^([A-Za-z0-9-]+)[ \\t]*$`)\n\tobsidianQuotePattern    = regexp.MustCompile(`^((?:[ \\t]*>[ \\t]?)+)(.*)$`)\n\tobsidianListItemPattern = regexp.MustCompile(`^([ \\t]*(?:[-+*]|\\d+[.)])[ \\t]+)(.*)$`)\n\tobsidianFootnotePattern = regexp.MustCompile(`(?m)\\[\\^[^\\]\\r\\n]+\\]`)\n)\n\nfunc StartObsidianVaultAnalysis(localPath string) (*ObsidianVaultTask, error) {\n\tvar replacedTaskID string\n\tobsidianTasksMu.Lock()\n\tif obsidianActive != \"\" {\n\t\tif active := obsidianTasks[obsidianActive]; active != nil && !isObsidianTerminalState(active.State) {\n\t\t\tif !isObsidianPreImportState(active.State) {\n\t\t\t\tobsidianTasksMu.Unlock()\n\t\t\t\treturn nil, errors.New(Conf.Language(329))\n\t\t\t}\n\t\t\tif active.Cancel != nil {\n\t\t\t\tactive.Cancel()","sourceCodeStart":217,"sourceCodeEnd":253,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/import_obsidian.go#L217-L253","documentation":"`errObsidianSourceChanged` signals TOCTOU protection: before analysis or after reading a Vault note, the file's size and mtime are re-stat'ed and compared to the pre-read snapshot; any difference aborts the read of that file so the importer never parses a half-written or concurrently modified file. It is intentionally opaque to users (see `TestObsidianUserErrorDataDoesNotExposeCause`).","triggerScenarios":"`obsidianReadStable` path: `before.Size() != file.Size || !before.ModTime().Equal(file.ModTime)` after analysis, or the after-read stat differs — i.e. the `.md` file changed size or mtime between stat, analysis, and `os.ReadFile`.","commonSituations":"Obsidian (or a sync client like Obsidian Sync/iCloud/Dropbox) is open and autosaving while the import runs; an editor/sorter touched the note mid-import; filesystem timestamps were rewritten by a sync scan.","solutions":["Close Obsidian and pause sync clients, then retry the import","Re-run the import — the check is per-attempt and succeeds when the file is stable","Copy the Vault to a static local folder and import from the copy"],"exampleFix":"// before\n// import while Obsidian autosaves the note -> stat mismatch\nobsidianReadFile(file) // wrapped: errObsidianSourceChanged during analysis\n// after\n// close Obsidian / pause sync, or import from a static copy\nobsidianReadFile(file) // stable stat, read succeeds","handlingStrategy":"retry","validationCode":"null","typeGuard":"null","tryCatchPattern":"for (let attempt = 0; attempt < 3; attempt++) {\n  try {\n    await api.importObsidianVault(vaultPath);\n    break;\n  } catch (e) {\n    if (!isSourceChangedError(e)) throw e;\n    await sleep(1000); // let writer settle, then retry\n  }\n}","preventionTips":["Close Obsidian and pause sync clients during import","Import from a static local copy of the vault","Avoid editing or re-timestamping vault notes while an import is running"],"tags":["obsidian","import","concurrency","file-changed"],"backgroundTag":"checksum-mismatch","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}