{"record":{"id":"7e4b3a0fbafc5fda","repo":"siyuan-note/siyuan","slug":"path-is-not-under-an-assets-directory-s","errorCode":null,"errorMessage":"path is not under an assets directory: %s","messagePattern":"path is not under an assets directory: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/assets.go","lineNumber":938,"sourceCode":"\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\n\t}\n\n\tresolvedRoot, evalErr := filepath.EvalSymlinks(assetRoot)\n\tif evalErr != nil {\n\t\terr = fmt.Errorf(\"resolve assets directory [%s] failed: %w\", assetRoot, evalErr)\n\t\treturn\n\t}\n\tif assetDirIndex > 0 {\n\t\tnotebookRoot := filepath.Join(util.DataDir, parts[0])\n\t\tresolvedDataDir, dataEvalErr := filepath.EvalSymlinks(util.DataDir)","sourceCodeStart":920,"sourceCodeEnd":956,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/assets.go#L920-L956","documentation":"Returned by ResolveDataAssetPath when no \"assets\" segment can be found in the cleaned data-relative path (assetDirIndex stays -1). The resolver requires either a top-level assets/ dir (parts[0]==\"assets\") or a notebook-prefixed assets dir (<id>/.../assets/...); any path without an assets component is rejected before symlink resolution.","triggerScenarios":"Passing \"docs/note.md\", \"foo/bar.png\", \"<boxID>/data/x.sy\", or any path that never enters an assets directory. The resolver is asset-only by design.","commonSituations":"Caller used ResolveDataAssetPath for a non-asset file (a .sy doc, a config file); a path constructed without the assets/ prefix; confusion between ResolveDataAssetPath and a general data-path resolver.","solutions":["Ensure the path includes an assets/ segment, e.g. \"assets/x.png\" or \"<boxID>/assets/x.png\".","For non-asset data files, use the appropriate file API instead of ResolveDataAssetPath.","Build asset paths only from values returned by the asset insertion/listing APIs."],"exampleFix":"// before\nrel, abs, err := model.ResolveDataAssetPath(\"<boxID>/20200101.siyuan\") // no assets segment\n\n// after\nrel, abs, err := model.ResolveDataAssetPath(\"<boxID>/assets/image-20200101.png\")","handlingStrategy":"validation","validationCode":"parts := strings.Split(filepath.ToSlash(filepath.Clean(assetPath)), \"/\")\nhasAssets := false\nfor _, p := range parts {\n    if p == \"assets\" { hasAssets = true; break }\n}\nif !hasAssets {\n    return fmt.Errorf(\"path is not under an assets directory: %s\", assetPath)\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Only call ResolveDataAssetPath for asset files — never for .sy docs or config files.","Always include the assets/ segment in constructed paths.","Derive asset paths from insertion/listing APIs rather than building them by hand."],"tags":["validation","assets","path-resolution"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}