{"record":{"id":"394ab542e84ecfb2","repo":"siyuan-note/siyuan","slug":"asset-path-does-not-belong-to-a-notebook-s","errorCode":null,"errorMessage":"asset path does not belong to a notebook: %s","messagePattern":"asset path does not belong to a notebook: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/assets.go","lineNumber":1060,"sourceCode":"\t\treturn\n\t}\n\n\tparts := strings.Split(filepath.ToSlash(dataRelativePath), \"/\")\n\tassetDirIndex := -1\n\tswitch {\n\tcase len(parts) > 1 && parts[0] == \"assets\":\n\t\tassetDirIndex = 0\n\tcase len(parts) > 2 && ast.IsNodeIDPattern(parts[0]):\n\t\tfor i := 1; i < len(parts)-1; i++ {\n\t\t\tif parts[i] == \"assets\" {\n\t\t\t\tassetDirIndex = i\n\t\t\t\tbreak\n\t\t\t}\n\t\t}\n\t\tif assetDirIndex > 0 {\n\t\t\tboxConfPath := filepath.Join(util.DataDir, parts[0], \".siyuan\", \"conf.json\")\n\t\t\tif !filelock.IsExist(boxConfPath) {\n\t\t\t\terr = fmt.Errorf(\"asset path does not belong to a notebook: %s\", assetPath)\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif IsEncryptedBox(parts[0]) {\n\t\t\t\terr = fmt.Errorf(\"accessing assets in encrypted notebook [%s] is not supported\", parts[0])\n\t\t\t\treturn\n\t\t\t}\n\t\t}\n\t}\n\tif assetDirIndex < 0 {\n\t\terr = fmt.Errorf(\"path is not under an assets directory: %s\", assetPath)\n\t\treturn\n\t}\n\n\tassetRootParts := parts[:assetDirIndex+1]\n\tassetRoot := filepath.Join(util.DataDir, filepath.FromSlash(strings.Join(assetRootParts, \"/\")))\n\tif !gulu.File.IsSubPath(assetRoot, absPath) {\n\t\terr = fmt.Errorf(\"path is not a child of assets directory: %s\", assetPath)\n\t\treturn","sourceCodeStart":1042,"sourceCodeEnd":1078,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/assets.go#L1042-L1078","documentation":"When the path targets a notebook-level assets folder (\"<notebookID>/assets/...\"), ResolveDataAssetPath verifies the notebook actually exists by checking for <data>/<notebookID>/.siyuan/conf.json. If that config file is missing, the first path segment is not a real notebook ID, so the path is rejected. (Paths into an existing encrypted notebook are separately rejected with a different error.)","triggerScenarios":"Passing \"<notebookID>/assets/img.png\" where notebookID does not match any notebook in the workspace — e.g. a stale/hardcoded notebook ID after the notebook was deleted, an ID from another workspace, a typo, or a path whose first segment merely looks like a block ID (matches IsNodeIDPattern) but is not a notebook directory.","commonSituations":"Restored or synced data where the notebook folder was removed but documents referencing its assets remain; scripts hardcoding notebook IDs copied from another machine; renamed/re-imported notebooks generating new IDs; constructing notebook asset paths by string concatenation with an invalid ID.","solutions":["Verify the notebook exists (it appears in the doc tree and <data>/<id>/.siyuan/conf.json is present) and correct the ID in the path","Look up the current notebook ID via the API (e.g. /api/notebook/lsNotebooks) instead of reusing a stored/stale ID","If the asset should be global, move it to the workspace-level assets/ folder and use \"assets/<file>\" without a notebook prefix","Restore the missing notebook folder (or its .siyuan/conf.json) from backup/sync if the notebook was deleted by accident"],"exampleFix":"// before\nrel, abs, err := model.ResolveDataAssetPath(\"20240101120000-abcdefg/assets/img.png\") // notebook deleted\n// after\nboxes := model.ListNotebooks() // pick an existing notebook ID\nrel, abs, err := model.ResolveDataAssetPath(boxes[0].ID + \"/assets/img.png\")","handlingStrategy":"validation","validationCode":"func notebookExists(dataDir, id string) bool {\n    if !ast.IsNodeIDPattern(id) { return false }\n    _, err := os.Stat(filepath.Join(dataDir, id, \".siyuan\", \"conf.json\"))\n    return err == nil\n}","typeGuard":"func isNotebookAssetPath(p string) (string, bool) {\n    parts := strings.Split(p, \"/\")\n    if len(parts) > 2 && ast.IsNodeIDPattern(parts[0]) { return parts[0], true }\n    return \"\", false\n}","tryCatchPattern":"rel, abs, err := model.ResolveDataAssetPath(p)\nif err != nil {\n    if strings.HasPrefix(err.Error(), \"asset path does not belong to a notebook\") {\n        // refresh notebook list and retry with a valid ID, or fall back to global assets\n    }\n    return err\n}","preventionTips":["Resolve notebook IDs dynamically via /api/notebook/lsNotebooks instead of hardcoding them","After deleting or re-importing notebooks, update any stored references to their assets","Check that <data>/<id>/.siyuan/conf.json exists when constructing notebook-scoped paths","Prefer global assets/ for assets that must outlive individual notebooks"],"tags":["assets","notebook","resource-not-found","validation"],"backgroundTag":"resource-not-found","analyzedSha":"9f775e8a12daef8255556097396f9b2739078892","analyzedAt":"2026-09-19T03:17:15.984Z","contentChangedAt":"2026-09-19T03:17:15.984Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}