{"record":{"id":"1faee020d3ad5137","repo":"siyuan-note/siyuan","slug":"encrypted-attribute-view-history-is-missing-valid","errorCode":null,"errorMessage":"encrypted attribute view history is missing valid notebook context","messagePattern":"encrypted attribute view history is missing valid notebook context","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/history.go","lineNumber":692,"sourceCode":"\t\treturn\n\t}\n\t// 验证目标文件必须是 AV 定义文件\n\tif !strings.HasSuffix(historyPath, \".json\") || !strings.Contains(filepath.ToSlash(historyPath), \"/storage/av/\") {\n\t\treturn fmt.Errorf(\"invalid AV history path [%s]\", historyPath)\n\t}\n\n\tfrom := historyPath\n\t// 从路径提取 boxID 判断是否加密笔记本的 AV\n\trelPath := strings.TrimPrefix(filepath.ToSlash(historyPath), filepath.ToSlash(util.HistoryDir))\n\trelPath = strings.TrimPrefix(relPath, \"/\")\n\tpathParts := strings.SplitN(relPath, \"/\", 3)\n\tdata, readErr := filelock.ReadFile(from)\n\tif readErr != nil {\n\t\treturn readErr\n\t}\n\tciphertext := util.IsCiphertext(data)\n\tif ciphertext && (len(pathParts) < 3 || !ast.IsNodeIDPattern(pathParts[1]) || !IsEncryptedBox(pathParts[1])) {\n\t\treturn errors.New(\"encrypted attribute view history is missing valid notebook context\")\n\t}\n\tto := filepath.Join(util.DataDir, \"storage\", \"av\", filepath.Base(historyPath))\n\tif len(pathParts) >= 2 && IsEncryptedBox(pathParts[1]) {\n\t\tif !ciphertext {\n\t\t\treturn fmt.Errorf(\"encrypted notebook attribute view history is plaintext [%s]\", pathParts[1])\n\t\t}\n\t\t// 加密笔记本的 AV 定义回滚到笔记本级目录\n\t\tto = filepath.Join(util.DataDir, pathParts[1], \"storage\", \"av\", filepath.Base(historyPath))\n\t\tif err = os.MkdirAll(filepath.Dir(to), 0755); err != nil {\n\t\t\treturn\n\t\t}\n\t}\n\n\tif err = filelock.CopyNewtimes(from, to); err != nil {\n\t\tlogging.LogErrorf(\"copy file [%s] to [%s] failed: %s\", from, to, err)\n\t\treturn\n\t}\n\tavID := strings.TrimSuffix(filepath.Base(historyPath), \".json\")","sourceCodeStart":674,"sourceCodeEnd":710,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/history.go#L674-L710","documentation":"RollbackAttributeViewHistory validates that an encrypted AV history snapshot can be attributed to an encrypted notebook. The history file content is ciphertext (util.IsCiphertext), so the code requires the path under history dir to be boxID/avDir/file.json where pathParts[1] is a valid node ID matching an encrypted box. If the history path does not carry a valid encrypted notebook ID at that position, the rollback cannot determine where the AV definition belongs and it aborts. This protects against restoring encrypted AV definitions to the global storage/av directory where they would be inaccessible or wrongly placed.","triggerScenarios":"Calling RollbackAttributeViewHistory with a historyPath whose content decrypts as ciphertext but whose relative path under the history directory has fewer than 3 segments, or whose second segment (boxID) is not a node-ID pattern or does not correspond to an encrypted notebook, e.g. a hand-moved or legacy AV history file stored without a notebook-level directory.","commonSituations":"Manually copying AV history files between machines or backup restores that flatten the history directory structure; upgrading from a version where encrypted-notebook AV history was stored in a different layout; pointing the history viewer at a stale/corrupted history tree.","solutions":["Ensure the history path follows <historyDir>/<timestamp>-<op>/<boxID>/storage/av/<avID>.json where boxID is a valid node ID of an encrypted notebook","Verify the boxID segment actually belongs to an encrypted notebook (IsEncryptedBox) — regenerate or re-locate the history under the correct notebook ID directory","If the history file was manually moved, restore the original directory structure from backup","If the AV content is not actually encrypted, check why util.IsCiphertext returned true (wrong file passed in)"],"exampleFix":"// before\nRollbackAttributeViewHistory(\"/history/20240101120000-update/storage/av/20240101100000-abc.json\") // missing boxID segment\n// after\nRollbackAttributeViewHistory(\"/history/20240101120000-update/20240101100000-boxid/storage/av/20240101100000-abc.json\")","handlingStrategy":"validation","validationCode":"const rel = historyPath.replace(/\\\\/g, \"/\").split(\"/history/\").pop().replace(/^\\//, \"\");\nconst parts = rel.split(\"/\");\nconst isNodeID = (s) => /^[0-9]{14}-[0-9a-z]{7}$/.test(s);\nif (parts.length < 3 || !isNodeID(parts[1])) {\n  throw new Error(\"history path lacks notebook context: \" + historyPath);\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Never move or rename directories inside the history tree by hand","Always take history paths from the history query API, not from manual filesystem scans","Keep backups of the full history directory structure, not flattened copies"],"tags":["history","attribute-view","encryption","path-validation"],"backgroundTag":"invalid-argument-value","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"}