siyuan-note/siyuan · error

read history snapshot [%s] failed: %w

Error message

read history snapshot [%s] failed: %w

What it means

When iterating a history snapshot directory (<ts>-<op> under data/history) to find per-box subdirectories, a failed os.ReadDir on that snapshot directory is wrapped as 'read history snapshot [%s] failed'. The library throws it because an unreadable snapshot prevents a complete scan, and callers (e.g. KEK verification, migration recovery) must not silently skip potentially encrypted history.

Source

Thrown at kernel/model/crypto.go:803

}

func encryptedNotebookHistoryBoxDirs() (ret []string, err error) {
	entries, err := os.ReadDir(util.HistoryDir)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, nil
		}
		return nil, fmt.Errorf("read history dir failed: %w", err)
	}
	for _, entry := range entries {
		if !entry.IsDir() {
			continue
		}
		// 历史快照目录:<ts>-<op>,其下是各 boxID 子目录
		snapshotDir := filepath.Join(util.HistoryDir, entry.Name())
		boxEntries, readErr := os.ReadDir(snapshotDir)
		if readErr != nil {
			return nil, fmt.Errorf("read history snapshot [%s] failed: %w", entry.Name(), readErr)
		}
		for _, boxEntry := range boxEntries {
			if !boxEntry.IsDir() || !ast.IsNodeIDPattern(boxEntry.Name()) {
				continue
			}
			encrypted, checkErr := isEncryptedHistoryBoxDir(filepath.Join(snapshotDir, boxEntry.Name()))
			if checkErr != nil {
				return nil, checkErr
			}
			if encrypted {
				ret = append(ret, filepath.Join(snapshotDir, boxEntry.Name()))
			}
		}
	}
	return ret, nil
}

func hasEncryptedNotebookDeleteHistory(boxID string) (bool, error) {

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Check the wrapped OS error for the snapshot named in [%s] and fix its permissions/availability
  2. Re-run the operation outside the history-retention window, or stop the pruning/cleanup job to avoid the delete race
  3. Restore the affected snapshot directory from backup if it is corrupt or a dangling symlink
  4. If the snapshot is disposable, move it out of data/history and re-run the scan; never delete recovery material of encrypted notebooks without confirming unencrypted history exists

Example fix

// before: prune while verifying
v.eachSnapshot(func(d string) { os.RemoveAll(d) })
go verifyKEKAgainstEncryptedHistory()
// after: verify first, prune after
verifyKEKAgainstEncryptedHistory()
thenPruneOldSnapshots()
Defensive patterns

Strategy: retry

Validate before calling

for _, e := range mustRead(util.HistoryDir) { if !e.IsDir() { continue }; if _, err := os.ReadDir(filepath.Join(util.HistoryDir, e.Name())); err != nil { return err } }

Try / catch

dirs, err := encryptedNotebookHistoryBoxDirs()
if err != nil {
    if isTransient(err) { time.Sleep(retryDelay); retry() } // e.g. pruning race
    return fmt.Errorf("history snapshot scan aborted, keeping recovery material: %w", err)
}

Prevention

When it happens

Trigger: scanEncryptedNotebookHistory or verifyKEKAgainstEncryptedHistory encountering a snapshot dir whose listing fails: permission denied on the snapshot dir, the snapshot entry is a symlink to an inaccessible target resolved differently per platform, or a race where the snapshot is deleted (history retention pruning) mid-scan so ReadDir gets ENOENT on the directory itself.

Common situations: History retention job concurrently deleting old snapshots while the KEK verification/migration scan runs; partial snapshot copy from backup or sync leaving broken entries; filesystem permission inconsistencies inside data/history/<ts>-<op>.

Understand the failure class

Background: "failed to read file", EACCES, ENOENT and "could not read <path>" errors: when a program can't read a file from disk — this error's family across 49 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11). Data as JSON: /api/errors/9b7d0dae7e4d9ad2. Report an issue: GitHub.