{"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":"validation","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/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/import_obsidian.go#L217-L253","documentation":"A sentinel error returned when a Vault source file's size or modification time changes between the initial scan and a re-read, indicating the file was edited during analysis/import. readStableObsidianFile stats the file before and after reading; if size or mtime differ, it wraps this sentinel with 'during analysis' or 'while reading'. newObsidianReadUserError maps it to i18n key 346. This is a consistency guard: SiYuan needs the file to be stable to build accurate link/block-ID indices.","triggerScenarios":"During analyzeObsidianVault or importObsidianVaultTask, readStableObsidianFile (line 926) or validateObsidianSourceMetadata (line 1860) detects that a file's Size or ModTime changed between the scan snapshot and the read. Happens if Obsidian or another editor is actively saving notes into the Vault while import is running.","commonSituations":"Obsidian is left open and auto-saving while SiYuan imports the Vault; a cloud-sync client (Dropbox, OneDrive, iCloud) is writing files into the Vault mid-import; a user edits notes during a long analysis; antivirus or backup tools touch file metadata.","solutions":["Close Obsidian and pause any cloud-sync clients operating on the Vault before starting analysis.","Re-run startObsidianVaultAnalysis after the Vault is quiescent — the fresh scan re-snapshots file metadata.","Copy the Vault to a stable local directory and import the copy instead of the live Vault.","If recurring, exclude the Vault from backup/antivirus scanning during import."],"exampleFix":"// before: Obsidian auto-saving during import triggers mtime change\n// readStableObsidianFile -> errObsidianSourceChanged\n\n// after: copy vault to a quiet location first\ncp -r /live/Vault /tmp/VaultCopy\n// then POST startObsidianVaultAnalysis with localPath=/tmp/VaultCopy","handlingStrategy":"validation","validationCode":"// Check no file changed between two stat snapshots (proxy for stability)\nfunc vaultIsStable(vaultDir string, snapshot map[string]fs.FileInfo) (bool, error) {\n    for rel, old := range snapshot {\n        cur, err := os.Stat(filepath.Join(vaultDir, rel))\n        if err != nil { return false, err }\n        if cur.Size() != old.Size() || !cur.ModTime().Equal(old.ModTime()) {\n            return false, nil\n        }\n    }\n    return true, nil\n}","typeGuard":null,"tryCatchPattern":"if err := analyzeVault(ctx, localPath); err != nil {\n    if errors.Is(err, errObsidianSourceChanged) {\n        // advise the user to pause sync/Obsidian and retry\n        return errors.New(\"Vault files changed during analysis; close Obsidian and sync, then retry\")\n    }\n}","preventionTips":["Close Obsidian and pause cloud-sync before importing.","Copy the Vault to a quiet local directory for import.","Avoid importing Vaults that are actively being edited."],"tags":["obsidian","import","filesystem","race-condition","sentinel-error"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}