{"record":{"id":"9b7d0dae7e4d9ad2","repo":"siyuan-note/siyuan","slug":"read-history-snapshot-s-failed-w","errorCode":null,"errorMessage":"read history snapshot [%s] failed: %w","messagePattern":"read history snapshot \\[(.+?)\\] failed: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/crypto.go","lineNumber":803,"sourceCode":"}\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\n\t\t\t}\n\t\t\tif encrypted {\n\t\t\t\tret = append(ret, filepath.Join(snapshotDir, boxEntry.Name()))\n\t\t\t}\n\t\t}\n\t}\n\treturn ret, nil\n}\n\nfunc hasEncryptedNotebookDeleteHistory(boxID string) (bool, error) {","sourceCodeStart":785,"sourceCodeEnd":821,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/crypto.go#L785-L821","documentation":"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.","triggerScenarios":"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.","commonSituations":"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>.","solutions":["Check the wrapped OS error for the snapshot named in [%s] and fix its permissions/availability","Re-run the operation outside the history-retention window, or stop the pruning/cleanup job to avoid the delete race","Restore the affected snapshot directory from backup if it is corrupt or a dangling symlink","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"],"exampleFix":"// before: prune while verifying\nv.eachSnapshot(func(d string) { os.RemoveAll(d) })\ngo verifyKEKAgainstEncryptedHistory()\n// after: verify first, prune after\nverifyKEKAgainstEncryptedHistory()\nthenPruneOldSnapshots()","handlingStrategy":"retry","validationCode":"for _, e := range mustRead(util.HistoryDir) { if !e.IsDir() { continue }; if _, err := os.ReadDir(filepath.Join(util.HistoryDir, e.Name())); err != nil { return err } }","typeGuard":null,"tryCatchPattern":"dirs, err := encryptedNotebookHistoryBoxDirs()\nif err != nil {\n    if isTransient(err) { time.Sleep(retryDelay); retry() } // e.g. pruning race\n    return fmt.Errorf(\"history snapshot scan aborted, keeping recovery material: %w\", err)\n}","preventionTips":["Pause history retention/pruning while running KEK verification or migration","Back up data/history before restore operations that move snapshot dirs","Avoid partial (interrupted) snapshot copies between workspaces","Re-run the scan once after fixing transient I/O errors before concluding data loss"],"tags":["filesystem","io","history","race-condition"],"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"}