siyuan-note/siyuan · error

stat encrypted notebook history backup [%s] failed: %w

Error message

stat encrypted notebook history backup [%s] failed: %w

What it means

isEncryptedHistoryBoxDir decides whether a history box dir is an encrypted notebook's box by probing .siyuan/notebook-crypto-backup.json. A failed os.Stat that is not NotExist is wrapped as 'stat encrypted notebook history backup [%s] failed'. The library throws it because an inaccessible backup marker must abort classification rather than classify the box as unencrypted (which could authorize destructive operations on recovery material).

Source

Thrown at kernel/model/crypto.go:939

func HasEncryptedNotebookHistory() bool {
	hasHistory, err := scanEncryptedNotebookHistory()
	if err != nil {
		logging.LogErrorf("scan encrypted notebook history failed: %s", err)
		return true
	}
	return hasHistory
}

// isEncryptedHistoryBoxDir 判断历史目录中的 boxID 子目录是否属于加密笔记本。
// 优先看 notebook-crypto-backup.json(删除前随 box 目录整体备份,是加密身份的权威标识),
// 再 fallback 到 conf.json 的 Encrypted 标志。
func isEncryptedHistoryBoxDir(boxDir string) (bool, error) {
	siyuanDir := filepath.Join(boxDir, ".siyuan")
	backupPath := filepath.Join(siyuanDir, "notebook-crypto-backup.json")
	if _, err := os.Stat(backupPath); err == nil {
		return true, nil
	} else if !os.IsNotExist(err) {
		return false, fmt.Errorf("stat encrypted notebook history backup [%s] failed: %w", boxDir, err)
	}
	confPath := filepath.Join(siyuanDir, "conf.json")
	if _, err := os.Stat(confPath); err != nil {
		if os.IsNotExist(err) {
			return false, nil
		}
		return false, fmt.Errorf("stat encrypted notebook history conf [%s] failed: %w", boxDir, err)
	}
	data, err := filelock.ReadFile(confPath)
	if err != nil {
		return false, fmt.Errorf("read encrypted notebook history conf [%s] failed: %w", boxDir, err)
	}
	var boxConf conf.BoxConf
	if err = gulu.JSON.UnmarshalJSON(data, &boxConf); err != nil {
		return false, fmt.Errorf("parse encrypted notebook history conf [%s] failed: %w", boxDir, err)
	}
	return boxConf.Encrypted, nil
}

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Read the wrapped OS error and fix access to <boxDir>/.siyuan/notebook-crypto-backup.json (chmod/chown or ACLs)
  2. Verify .siyuan is still a directory and not a file/symlink; restore it from backup if broken
  3. Re-mount or repair the storage if the error is I/O level (ESTALE, EIO)
  4. Retry the scan after fixing; do not delete or rewrite history boxes while the error persists

Example fix

// before: unreadable .siyuan in snapshot box
ls -l data/history/20260901-delete/20240101120000-abc123/
# .siyuan  root:root 000
// after
sudo chmod -R u+rwX data/history/20260901-delete/20240101120000-abc123/.siyuan
sudo chown -R $USER data/history/20260901-delete/20240101120000-abc123/.siyuan
Defensive patterns

Strategy: try-catch

Validate before calling

p := filepath.Join(boxDir, ".siyuan", "notebook-crypto-backup.json")
if _, err := os.Stat(p); err != nil && !os.IsNotExist(err) { return err }

Type guard

func backupStatable(boxDir string) bool { _, err := os.Stat(filepath.Join(boxDir, ".siyuan", "notebook-crypto-backup.json")); return err == nil || os.IsNotExist(err) }

Try / catch

enc, err := isEncryptedHistoryBoxDir(boxDir)
if err != nil {
    // unknown status: treat as encrypted to protect recovery material
    handleAsEncrypted(boxDir)
    return
}

Prevention

When it happens

Trigger: encryptedNotebookHistoryBoxDirs, hasEncryptedNotebookDeleteHistory, or IsEncryptedHistoryPath statting the backup file when permission is denied on the box dir or its .siyuan subdir, the path component is not a directory, or the filesystem returns an I/O error (e.g. EXDEV/ESTALE on network mounts).

Common situations: Snapshots restored with root ownership; .siyuan replaced by a file; corrupted network-share mounts; macOS/Windows ACLs blocking the kernel process after workspace moves between machines.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


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