{"record":{"id":"a1f0b66ccf97b98e","repo":"siyuan-note/siyuan","slug":"345","errorCode":"345","errorMessage":"read Markdown [%s]: %w","messagePattern":"read Markdown \\[(.+?)\\]: %w","errorType":"error_code","errorClass":"obsidianUserError","httpStatus":null,"severity":"error","filePath":"kernel/model/import_obsidian.go","lineNumber":953,"sourceCode":"\tif err != nil {\n\t\treturn nil, err\n\t}\n\tafter, err := os.Stat(file.AbsPath)\n\tif err != nil {\n\t\treturn nil, err\n\t}\n\tif before.Size() != after.Size() || !before.ModTime().Equal(after.ModTime()) {\n\t\treturn nil, fmt.Errorf(\"%w while reading\", errObsidianSourceChanged)\n\t}\n\treturn data, nil\n}\n\nfunc newObsidianReadUserError(file *obsidianSourceFile, err error) error {\n\tlanguage := 345\n\tif errors.Is(err, errObsidianSourceChanged) {\n\t\tlanguage = 346\n\t}\n\treturn newObsidianUserError(language, file.RelPath, fmt.Errorf(\"read Markdown [%s]: %w\", file.RelPath, err))\n}\n\nfunc validateObsidianReadableFile(file *obsidianSourceFile) error {\n\topened, err := os.Open(file.AbsPath)\n\tif err != nil {\n\t\treturn err\n\t}\n\tdefer opened.Close()\n\tbuffer := make([]byte, 1)\n\t_, err = opened.Read(buffer)\n\tif err != nil && !errors.Is(err, io.EOF) {\n\t\treturn err\n\t}\n\treturn validateObsidianSourceMetadata(file)\n}\n\nfunc buildObsidianHeadingIndex(doc *obsidianDocPlan, tree *parse.Tree) {\n\tstack := make([]string, 6)","sourceCodeStart":935,"sourceCodeEnd":971,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/import_obsidian.go#L935-L971","documentation":"newObsidianReadUserError wraps any error returned by readStableObsidianFile during analysis/staging. Language code 345 ('Unable to read Markdown file [%s]') for ordinary read failures, or 346 ('File [%s] changed after analysis…') when the cause is the errObsidianSourceChanged sentinel. readStableObsidianFile fails on os.Stat failure, on a detected size/mtime drift, or on os.ReadFile error.","triggerScenarios":"File deleted or permission revoked between scan and read; file modified mid-read (size/mtime differ between the pre-Stat, ReadFile, and post-Stat snapshots — the 'stable read' guard fires errObsidianSourceChanged); disk I/O error during ReadFile. The relative path is included so the user can locate it.","commonSituations":"User kept editing the vault in Obsidian during import; a sync client (iCloud/Dropbox) rewrote files mid-analysis; antivirus quarantined the file; file on a flaky external drive; clock skew making mtime comparisons unstable.","solutions":["If the message indicates 'changed after analysis' (code 346): stop editing/syncing the vault, then re-run StartObsidianVaultAnalysis to get a fresh snapshot.","Confirm the file still exists and is readable (ls + cat as the SiYuan user); restore from backup if it was deleted/quarantined.","Pause or exclude the vault in cloud-sync clients and antivirus during import, then retry.","For mtime-drift false positives on network filesystems, copy the vault to a local FS and import from there."],"exampleFix":"// before: Obsidian app open and auto-saving while importing\n//   -> readStableObsidianFile sees mtime change -> code 346\n\n// after: close Obsidian, pause sync, then analyse\nStartObsidianVaultAnalysis(localCopyPath)","handlingStrategy":"retry","validationCode":"// Ensure the vault is quiescent: no mtime changes during a probe read.\nfunc vaultStable(root string) error {\n    return filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error {\n        if err != nil || d.IsDir() { return err }\n        i1, e := os.Stat(p); if e != nil { return e }\n        b, e := os.ReadFile(p); if e != nil { return e }\n        i2, e := os.Stat(p); if e != nil { return e }\n        if i1.Size() != i2.Size() || !i1.ModTime().Equal(i2.ModTime()) {\n            return fmt.Errorf(\"%s changed during read\", p)\n        }\n        _ = b\n        return nil\n    })\n}","typeGuard":"null","tryCatchPattern":"// Retry on read errors; re-analyse on drift.\ntaskID, err := StartObsidianVaultAnalysis(path)\nfor attempt := 0; attempt < 3 && isTransientReadErr(err); attempt++ {\n    time.Sleep(backoff(attempt))\n    taskID, err = StartObsidianVaultAnalysis(path)\n}","preventionTips":["Close Obsidian and pause sync before importing.","Run the importer from a local copy on a stable filesystem.","Disable auto-save / touch-on-open tools for the vault."],"tags":["obsidian-import","filesystem","read","concurrency","stable-read"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}