siyuan-note/siyuan · error · obsidianUserError

345

345

Error message

read Markdown [%s]: %w

What it means

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.

Source

Thrown at kernel/model/import_obsidian.go:953

	if err != nil {
		return nil, err
	}
	after, err := os.Stat(file.AbsPath)
	if err != nil {
		return nil, err
	}
	if before.Size() != after.Size() || !before.ModTime().Equal(after.ModTime()) {
		return nil, fmt.Errorf("%w while reading", errObsidianSourceChanged)
	}
	return data, nil
}

func newObsidianReadUserError(file *obsidianSourceFile, err error) error {
	language := 345
	if errors.Is(err, errObsidianSourceChanged) {
		language = 346
	}
	return newObsidianUserError(language, file.RelPath, fmt.Errorf("read Markdown [%s]: %w", file.RelPath, err))
}

func validateObsidianReadableFile(file *obsidianSourceFile) error {
	opened, err := os.Open(file.AbsPath)
	if err != nil {
		return err
	}
	defer opened.Close()
	buffer := make([]byte, 1)
	_, err = opened.Read(buffer)
	if err != nil && !errors.Is(err, io.EOF) {
		return err
	}
	return validateObsidianSourceMetadata(file)
}

func buildObsidianHeadingIndex(doc *obsidianDocPlan, tree *parse.Tree) {
	stack := make([]string, 6)

View on GitHub (pinned to 251596fc0d)

Solutions

  1. If the message indicates 'changed after analysis' (code 346): stop editing/syncing the vault, then re-run StartObsidianVaultAnalysis to get a fresh snapshot.
  2. Confirm the file still exists and is readable (ls + cat as the SiYuan user); restore from backup if it was deleted/quarantined.
  3. Pause or exclude the vault in cloud-sync clients and antivirus during import, then retry.
  4. For mtime-drift false positives on network filesystems, copy the vault to a local FS and import from there.

Example fix

// before: Obsidian app open and auto-saving while importing
//   -> readStableObsidianFile sees mtime change -> code 346

// after: close Obsidian, pause sync, then analyse
StartObsidianVaultAnalysis(localCopyPath)
Defensive patterns

Strategy: retry

Validate before calling

// Ensure the vault is quiescent: no mtime changes during a probe read.
func vaultStable(root string) error {
    return filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error {
        if err != nil || d.IsDir() { return err }
        i1, e := os.Stat(p); if e != nil { return e }
        b, e := os.ReadFile(p); if e != nil { return e }
        i2, e := os.Stat(p); if e != nil { return e }
        if i1.Size() != i2.Size() || !i1.ModTime().Equal(i2.ModTime()) {
            return fmt.Errorf("%s changed during read", p)
        }
        _ = b
        return nil
    })
}

Type guard

null

Try / catch

// Retry on read errors; re-analyse on drift.
taskID, err := StartObsidianVaultAnalysis(path)
for attempt := 0; attempt < 3 && isTransientReadErr(err); attempt++ {
    time.Sleep(backoff(attempt))
    taskID, err = StartObsidianVaultAnalysis(path)
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/a1f0b66ccf97b98e. Report an issue: GitHub.