{"record":{"id":"82a399270f0dd0b3","repo":"siyuan-note/siyuan","slug":"encrypted-asset-history-is-missing-valid-notebook","errorCode":null,"errorMessage":"encrypted asset history is missing valid notebook context","messagePattern":"encrypted asset history is missing valid notebook context","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/history.go","lineNumber":562,"sourceCode":"\nfunc RollbackAssetsHistory(historyPath string) (err error) {\n\thistoryPath, err = validateHistoryPath(historyPath)\n\tif err != nil {\n\t\treturn\n\t}\n\n\tfrom := historyPath\n\t// 从路径提取 boxID 判断是否加密笔记本的资源\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\tencrypted := bytes.HasPrefix(data, encryptedAssetMagic)\n\tif encrypted && (len(pathParts) < 3 || !ast.IsNodeIDPattern(pathParts[1]) || !IsEncryptedBox(pathParts[1])) {\n\t\treturn errors.New(\"encrypted asset history is missing valid notebook context\")\n\t}\n\tto := filepath.Join(util.DataDir, \"assets\", filepath.Base(historyPath))\n\tif len(pathParts) >= 2 && IsEncryptedBox(pathParts[1]) {\n\t\tif !encrypted {\n\t\t\treturn fmt.Errorf(\"encrypted notebook asset history is plaintext [%s]\", pathParts[1])\n\t\t}\n\t\t// 加密笔记本的资源回滚到笔记本级 assets 目录\n\t\tto = filepath.Join(util.DataDir, pathParts[1], \"assets\", 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\tIncSync()","sourceCodeStart":544,"sourceCodeEnd":580,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/8641553a1f07374001902d3ce773285db1292b2d/kernel/model/history.go#L544-L580","documentation":"SiYuan rolls back asset-file history by inspecting the stored history path. When the history file's content is encrypted (it starts with encryptedAssetMagic) but the path does not name a valid encrypted notebook (fewer than 3 path parts, the second part is not a notebook-ID pattern, or that notebook is not an encrypted box), the rollback is refused because there is no valid notebook context to decrypt/place the asset into.","triggerScenarios":"Calling rollbackAssetsHistory (API /api/history/rollbackAssetsHistory) with a historyPath whose file bytes are ciphertext while pathParts[1] is missing, not a 20-char node ID, or an unencrypted notebook ID.","commonSituations":"Hand-crafted or plugin-supplied history paths; a history file moved/copied out of its notebook history dir and passed back; testing rollback with an encrypted asset snapshot whose path lost its notebook prefix; database/index drift listing asset histories with truncated paths.","solutions":["Pass the original historyPath exactly as returned by the history listing API so it retains <timestamp>-<op>/<boxID>/... structure","Verify the notebook at pathParts[1] is actually an encrypted box (IsEncryptedBox) before rolling back encrypted assets","Do not copy or rewrite history paths; use them verbatim from the history query results","Regenerate the asset history entry if the source path no longer maps to a notebook"],"exampleFix":"// before: truncated path loses notebook context\nrollbackAssetsHistory(historyDir + \"/20240101120000-update/assets/foo.enc\")\n// after: include the notebook ID segment\nrollbackAssetsHistory(historyDir + \"/20240101120000-update/20240101120000-xxxxxxx/assets/foo.enc\")","handlingStrategy":"validation","validationCode":"function canRollbackAssetHistory(historyPath) {\n  const parts = historyPath.split(/[\\\\/]/);\n  return parts.length >= 3 && /^[0-9a-f]{20}$|^[A-Za-z0-9]{20}$/.test(parts[1]);\n}","typeGuard":"null","tryCatchPattern":"try { await rollbackAssetsHistory(p); } catch (e) { if (String(e.msg).includes(\"missing valid notebook context\")) { /* re-list history and use unmodified path */ } else { throw e; } }","preventionTips":["Always pass history paths verbatim from the history listing API","Do not trim, rebase, or rebuild history path segments manually","Confirm notebook encryption state before restoring encrypted assets"],"tags":["history","encryption","path-validation","assets"],"backgroundTag":"invalid-argument-format","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"}