{"record":{"id":"738f3748fde1739f","repo":"siyuan-note/siyuan","slug":"stat-encrypted-notebook-history-conf-s-failed","errorCode":null,"errorMessage":"stat encrypted notebook history conf [%s] failed: %w","messagePattern":"stat encrypted notebook history conf \\[(.+?)\\] failed: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/crypto.go","lineNumber":946,"sourceCode":"}\n\n// isEncryptedHistoryBoxDir 判断历史目录中的 boxID 子目录是否属于加密笔记本。\n// 优先看 notebook-crypto-backup.json（删除前随 box 目录整体备份，是加密身份的权威标识），\n// 再 fallback 到 conf.json 的 Encrypted 标志。\nfunc isEncryptedHistoryBoxDir(boxDir string) (bool, error) {\n\tsiyuanDir := filepath.Join(boxDir, \".siyuan\")\n\tbackupPath := filepath.Join(siyuanDir, \"notebook-crypto-backup.json\")\n\tif _, err := os.Stat(backupPath); err == nil {\n\t\treturn true, nil\n\t} else if !os.IsNotExist(err) {\n\t\treturn false, fmt.Errorf(\"stat encrypted notebook history backup [%s] failed: %w\", boxDir, err)\n\t}\n\tconfPath := filepath.Join(siyuanDir, \"conf.json\")\n\tif _, err := os.Stat(confPath); err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn false, nil\n\t\t}\n\t\treturn false, fmt.Errorf(\"stat encrypted notebook history conf [%s] failed: %w\", boxDir, err)\n\t}\n\tdata, err := filelock.ReadFile(confPath)\n\tif err != nil {\n\t\treturn false, fmt.Errorf(\"read encrypted notebook history conf [%s] failed: %w\", boxDir, err)\n\t}\n\tvar boxConf conf.BoxConf\n\tif err = gulu.JSON.UnmarshalJSON(data, &boxConf); err != nil {\n\t\treturn false, fmt.Errorf(\"parse encrypted notebook history conf [%s] failed: %w\", boxDir, err)\n\t}\n\treturn boxConf.Encrypted, nil\n}\n\n// cachedDEKs 缓存已解锁加密笔记本的 DEK，按 boxID 索引。\n// KEK 不全局缓存（\"严格每笔记本单独解锁\"语义）：UnlockBox 临时派生 KEK 解出 DEK 后即丢弃 KEK，\n// 仅保留 per-box DEK 供后续读写加解密。\nvar (\n\tcachedDEKs     = map[string][]byte{}\n\tcachedDEKsLock sync.RWMutex","sourceCodeStart":928,"sourceCodeEnd":964,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/crypto.go#L928-L964","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before: conf.json is a zero-byte sync conflict stub\nmv data/history/<snap>/<box>/.siyuan/conf.json conf.json.conflict\n// after: restore from the live notebook\ncp data/notebooks/<boxID>/.siyuan/conf.json data/history/<snap>/<box>/.siyuan/conf.json","handlingStrategy":"type-guard","validationCode":"fi, err := os.Stat(filepath.Join(boxDir, \".siyuan\", \"conf.json\"))\nif err != nil && !os.IsNotExist(err) { return err }\nif err == nil && !fi.Mode().IsRegular() { return fmt.Errorf(\"conf.json is not a regular file\") }","typeGuard":"func confFileRegular(boxDir string) bool {\n    fi, err := os.Stat(filepath.Join(boxDir, \".siyuan\", \"conf.json\"))\n    return err == nil && fi.Mode().IsRegular()\n}","tryCatchPattern":"enc, err := isEncryptedHistoryBoxDir(boxDir)\nif err != nil {\n    log.Printf(\"cannot classify history box %s: %v\", boxDir, err)\n    return err // abort scan; do not classify as unencrypted\n}","preventionTips":["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"],"tags":["filesystem","permissions","history","conf"],"backgroundTag":"permission-denied","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}