siyuan-note/siyuan · error
stat encrypted notebook history conf [%s] failed: %w
Error message
stat encrypted notebook history conf [%s] failed: %w
What it means
After the crypto-backup marker is absent, isEncryptedHistoryBoxDir falls back to reading .siyuan/conf.json and checking BoxConf.Encrypted. A failed os.Stat on conf.json that is not NotExist is wrapped as 'stat encrypted notebook history conf [%s] failed'. It is thrown because the unencrypted-marker fallback path must fail loudly rather than classify an encrypted box as unencrypted.
Source
Thrown at kernel/model/crypto.go:946
}
// 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
}
// cachedDEKs 缓存已解锁加密笔记本的 DEK,按 boxID 索引。
// KEK 不全局缓存("严格每笔记本单独解锁"语义):UnlockBox 临时派生 KEK 解出 DEK 后即丢弃 KEK,
// 仅保留 per-box DEK 供后续读写加解密。
var (
cachedDEKs = map[string][]byte{}
cachedDEKsLock sync.RWMutexView on GitHub (pinned to 8641553a1f)
Solutions
- Inspect the wrapped OS error and repair permissions on <boxDir>/.siyuan/conf.json
- Confirm conf.json is a regular readable file; replace it from backup or the live notebook's .siyuan/conf.json if it is corrupt or a special file
- Check for sync-conflict artifacts and resolve them
- Re-run the history scan once the file is stat-able
Example fix
// before: conf.json is a zero-byte sync conflict stub mv data/history/<snap>/<box>/.siyuan/conf.json conf.json.conflict // after: restore from the live notebook cp data/notebooks/<boxID>/.siyuan/conf.json data/history/<snap>/<box>/.siyuan/conf.json
Defensive patterns
Strategy: type-guard
Validate before calling
fi, err := os.Stat(filepath.Join(boxDir, ".siyuan", "conf.json"))
if err != nil && !os.IsNotExist(err) { return err }
if err == nil && !fi.Mode().IsRegular() { return fmt.Errorf("conf.json is not a regular file") } Type guard
func confFileRegular(boxDir string) bool {
fi, err := os.Stat(filepath.Join(boxDir, ".siyuan", "conf.json"))
return err == nil && fi.Mode().IsRegular()
} Try / catch
enc, err := isEncryptedHistoryBoxDir(boxDir)
if err != nil {
log.Printf("cannot classify history box %s: %v", boxDir, err)
return err // abort scan; do not classify as unencrypted
} Prevention
- Resolve sync-conflict files next to conf.json promptly
- Exclude the workspace from antivirus quarantine rules
- Keep one process owning the workspace; avoid concurrent tools writing .siyuan
- After restores, verify each snapshot's .siyuan contains regular files
When it happens
Trigger: Callers (encryptedNotebookHistoryBoxDirs, hasEncryptedNotebookDeleteHistory, IsEncryptedHistoryPath) classifying a box dir where .siyuan/conf.json exists but cannot be stat'ed: permission denied, .siyuan or conf.json replaced by a non-directory, dangling state after partial restore, or I/O errors on network storage.
Common situations: Restored snapshots with mismatched ownership/ACLs; sync clients (e.g. Syncthing) leaving conflict files like conf.json (sync-conflict); antivirus quarantine touching conf.json on Windows.
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
- stat encrypted notebook history backup [%s] failed: %w
- get history directory failed: %w
- read session file failed: %w
- create session dir failed: %w
- remove community package [%s] failed
AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11).
Data as JSON: /api/errors/738f3748fde1739f.
Report an issue: GitHub.