{"record":{"id":"2a28a1e7003272ae","repo":"siyuan-note/siyuan","slug":"obsidian-vault-is-unreadable","errorCode":null,"errorMessage":"Obsidian Vault is unreadable","messagePattern":"Obsidian Vault is unreadable","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/import_obsidian.go","lineNumber":230,"sourceCode":"}\n\nfunc (err *obsidianUserError) Error() string {\n\treturn err.Cause.Error()\n}\n\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) {","sourceCodeStart":212,"sourceCodeEnd":248,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/import_obsidian.go#L212-L248","documentation":"A sentinel base error used as the wrapping parent for any condition that makes the Obsidian Vault unreadable at a filesystem level. It is never returned bare to the user; instead it is wrapped with a descriptive suffix (e.g. 'path is empty', 'normalize Vault path', 'read Vault root', 'read Vault config directory') via fmt.Errorf with %w. The obsidianVaultErrorLanguage helper maps this sentinel to i18n key 336 for user-facing display. It is the root cause for errors 774, 775, 776, and the config-read failure branch.","triggerScenarios":"Calling POST /api/import/startObsidianVaultAnalysis with a localPath that is empty, cannot be converted to an absolute path, does not exist on disk (os.Lstat fails), or whose .obsidian config directory exists but cannot be stat'd. validateObsidianVaultRoot wraps this sentinel in each of those branches.","commonSituations":"Passing an empty string as localPath; a path containing characters invalid for filepath.Abs on the host OS; the Vault directory was deleted or moved between selection and analysis; permissions deny Lstat on the Vault root or its config dir; a network/UNC path that is unreachable.","solutions":["Verify the localPath passed to startObsidianVaultAnalysis is non-empty and points to an existing, readable directory.","Check filesystem permissions on the Vault root and ensure the SiYuan process can stat it.","If the path involves a network share or removable drive, confirm it is mounted and accessible, then retry analysis.","Inspect the wrapped suffix in the error (after the colon) — it names the exact failing step (empty path, normalize, read root, config read)."],"exampleFix":"// before\nabs, err := filepath.Abs(filepath.Clean(localPath))\nif err != nil {\n    return \"\", fmt.Errorf(\"%w: normalize Vault path: %v\", errObsidianVaultUnreadable, err)\n}\n\n// after: caller validates and gives a clear path before calling the API\nif strings.TrimSpace(localPath) == \"\" {\n    return fmt.Errorf(\"Vault path must not be empty\")\n}\n// then call startObsidianVaultAnalysis(localPath)","handlingStrategy":"validation","validationCode":"// Pre-validate the Vault path is non-empty and resolvable before calling the API\nfunc preflightVaultPath(localPath string) error {\n    if strings.TrimSpace(localPath) == \"\" { return errors.New(\"Vault path is empty\") }\n    abs, err := filepath.Abs(filepath.Clean(localPath))\n    if err != nil { return fmt.Errorf(\"cannot resolve path: %w\", err) }\n    if _, err := os.Lstat(abs); err != nil { return fmt.Errorf(\"cannot access path: %w\", err) }\n    return nil\n}","typeGuard":null,"tryCatchPattern":"task, err := model.StartObsidianVaultAnalysis(localPath)\nif err != nil {\n    if errors.Is(err, errObsidianVaultUnreadable) {\n        // show the wrapped suffix to the user as an actionable filesystem error\n        return fmt.Errorf(\"Vault cannot be read (%s). Check the path exists and is accessible\", err)\n    }\n    return err\n}","preventionTips":["Always pass an absolute, existing directory path to startObsidianVaultAnalysis.","Confirm the Vault directory and .obsidian config are readable by the SiYuan process.","Run a preflight stat on the path in the caller before invoking the API."],"tags":["obsidian","import","filesystem","validation","sentinel-error"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}