{"record":{"id":"c181a65f60542ce7","repo":"siyuan-note/siyuan","slug":"read-history-dir-failed-w","errorCode":null,"errorMessage":"read history dir failed: %w","messagePattern":"read history dir failed: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/crypto.go","lineNumber":793,"sourceCode":"// （box 目录已删）。因此 DisableEncryptedNotebook 不能只靠 ListAllEncryptedBoxIDs 判定——\n// 已删除加密笔记本的历史仍依赖当前 MasterSalt/KEKVerifier 才能恢复，禁用并删除备份会让这些\n// 历史永久锁死，违反设计 §19。本函数扫描历史目录识别这类依赖。\n//\n// 判定信号：历史条目 <HistoryDir>/<ts>-<op>/<boxID>/.siyuan/ 下存在\n// notebook-crypto-backup.json（专为 box 删除后的恢复设计），或 conf.json 标记 Encrypted=true。\n// boxID 用 ast.IsNodeIDPattern 校验，避免误判 assets/storage 等非 box 目录。\nfunc scanEncryptedNotebookHistory() (bool, error) {\n\tboxDirs, err := encryptedNotebookHistoryBoxDirs()\n\treturn len(boxDirs) > 0, err\n}\n\nfunc encryptedNotebookHistoryBoxDirs() (ret []string, err error) {\n\tentries, err := os.ReadDir(util.HistoryDir)\n\tif err != nil {\n\t\tif os.IsNotExist(err) {\n\t\t\treturn nil, nil\n\t\t}\n\t\treturn nil, fmt.Errorf(\"read history dir failed: %w\", err)\n\t}\n\tfor _, entry := range entries {\n\t\tif !entry.IsDir() {\n\t\t\tcontinue\n\t\t}\n\t\t// 历史快照目录：<ts>-<op>，其下是各 boxID 子目录\n\t\tsnapshotDir := filepath.Join(util.HistoryDir, entry.Name())\n\t\tboxEntries, readErr := os.ReadDir(snapshotDir)\n\t\tif readErr != nil {\n\t\t\treturn nil, fmt.Errorf(\"read history snapshot [%s] failed: %w\", entry.Name(), readErr)\n\t\t}\n\t\tfor _, boxEntry := range boxEntries {\n\t\t\tif !boxEntry.IsDir() || !ast.IsNodeIDPattern(boxEntry.Name()) {\n\t\t\t\tcontinue\n\t\t\t}\n\t\t\tencrypted, checkErr := isEncryptedHistoryBoxDir(filepath.Join(snapshotDir, boxEntry.Name()))\n\t\t\tif checkErr != nil {\n\t\t\t\treturn nil, checkErr","sourceCodeStart":775,"sourceCodeEnd":811,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/crypto.go#L775-L811","documentation":"encryptedNotebookHistoryBoxDirs enumerates history snapshot directories under util.HistoryDir to find encrypted notebook box dirs. If os.ReadDir on the history dir fails with any error other than NotExist, it is wrapped as 'read history dir failed'. The library throws it to surface underlying I/O problems instead of silently treating history as absent, because callers rely on this scan to decide whether recovery material exists.","triggerScenarios":"Calling scanEncryptedNotebookHistory, verifyKEKAgainstEncryptedHistory or HasEncryptedNotebookHistory when kernel/util.HistoryDir exists but cannot be listed: permission denied on the directory, a path component is actually a file, or a low-level I/O error (bad sectors, network drive drop). os.IsNotExist errors are deliberately returned as nil, so this error only fires for other failures.","commonSituations":"Running SiYuan with a workspace on a removable/network drive that got disconnected; the data/history path was replaced by a regular file after a botched restore; restrictive file permissions after copying a workspace between users or containers; antivirus or backup software holding locks on Windows.","solutions":["Check the wrapped cause (%w) with errors.Unwrap / %v to see the OS error; fix that root cause first","Verify util.HistoryDir (workspace/data/history) exists and is a directory: ls -la it, and ensure it is not a file","Fix permissions so the kernel process user has read access to the history directory (chmod/chown)","If the history dir is corrupt or a stray file, move it aside and let SiYuan recreate it (after backing it up — it holds recovery material)"],"exampleFix":"// before: history path is a regular file, ReadDir fails\n// data/history -> plain file\n// after: ensure it is a directory\nrm data/history\nmkdir -p data/history","handlingStrategy":"try-catch","validationCode":"if fi, err := os.Stat(util.HistoryDir); err != nil || !fi.IsDir() { return fmt.Errorf(\"history dir unusable: %w\", err) }","typeGuard":"func historyDirUsable(p string) bool { fi, err := os.Stat(p); return err == nil && fi.IsDir() }","tryCatchPattern":"dirs, err := encryptedNotebookHistoryBoxDirs()\nif err != nil {\n    var pe *os.PathError\n    if errors.As(err, &pe) { log.Printf(\"history scan blocked at %s: %v\", pe.Path, pe.Err) }\n    // treat as 'history presence unknown': do NOT delete recovery material\n    return\n}","preventionTips":["Keep data/history as a directory owned by the kernel process user","Avoid pointing the workspace at removable/network drives without remount supervision","Never replace history with a file or symlink during restores","Check HasEncryptedNotebookHistory semantics: on scan error it assumes history exists"],"tags":["filesystem","io","history","go"],"backgroundTag":"file-read-failed","analyzedSha":"8641553a1f07374001902d3ce773285db1292b2d","analyzedAt":"2026-09-11T16:08:28.414Z","contentChangedAt":"2026-09-11T16:08:28.414Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}