{"record":{"id":"50779f10dc9f0cad","repo":"siyuan-note/siyuan","slug":"s-is-not-an-asset-path-must-start-with-assets","errorCode":null,"errorMessage":"[%s] is not an asset path (must start with assets/)","messagePattern":"\\[(.+?)\\] is not an asset path \\(must start with assets/\\)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/assets.go","lineNumber":1227,"sourceCode":"\tcleanPath = filepath.ToSlash(relativePath)\n\treturn\n}\n\n// GetAssetAbsPathInBox 在指定 box 内解析资源绝对路径，不进行全局遍历。\n// relativePath 必须以 assets/ 前缀开头，boxID 为空且路径没有 box 查询参数时只解析普通/全局资源，不遍历加密 box。\n// 加密 box 直接从 <boxID>/assets/ 查找，不依赖后缀匹配。\nfunc GetAssetAbsPathInBox(relativePath, boxID string) (string, error) {\n\tvar err error\n\trelativePath, boxID, err = assetPathAndBox(relativePath, boxID)\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\trelativePath = path.Clean(relativePath)\n\tif relativePath == \".\" || strings.HasPrefix(relativePath, \"../\") || relativePath == \"..\" || path.IsAbs(relativePath) {\n\t\treturn \"\", fmt.Errorf(\"[%s] is not an asset path\", relativePath)\n\t}\n\tif !strings.HasPrefix(relativePath, \"assets/\") {\n\t\treturn \"\", fmt.Errorf(\"[%s] is not an asset path (must start with assets/)\", relativePath)\n\t}\n\tif boxID != \"\" && !ast.IsNodeIDPattern(boxID) {\n\t\treturn \"\", fmt.Errorf(\"[%s] is not a box id\", boxID)\n\t}\n\n\tif boxID == \"\" {\n\t\treturn GetAssetAbsPathWithOpt(relativePath, false)\n\t}\n\n\tp := filepath.Join(util.DataDir, boxID, relativePath)\n\tif gulu.File.IsExist(p) {\n\t\tif !gulu.File.IsSubPath(util.WorkspaceDir, p) {\n\t\t\treturn \"\", fmt.Errorf(\"[%s] is not sub path of workspace\", p)\n\t\t}\n\t\t// 解析符号链接/目录联接，防止软链接跳出资产根目录\n\t\tif realP, evalErr := filepath.EvalSymlinks(p); evalErr == nil && realP != p {\n\t\t\tif !gulu.File.IsSubPath(util.WorkspaceDir, realP) {\n\t\t\t\treturn \"\", fmt.Errorf(\"symlink [%s] resolves outside workspace: [%s]\", p, realP)","sourceCodeStart":1209,"sourceCodeEnd":1245,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/assets.go#L1209-L1245","documentation":"After cleaning, GetAssetAbsPathInBox requires the asset path to begin with the 'assets/' prefix. A relative path that passes the traversal check but points outside an assets directory (e.g. 'foo.png', 'images/foo.png', a .sy document path) cannot be resolved as an asset and triggers this error. Companion message to 'is not an asset path' for the prefix-specific case.","triggerScenarios":"Calling GetAssetAbsPathInBox with \"foo.png\" or \"20250101120000-abc/assets/img.png\" (box-prefixed form) instead of the required bare \"assets/img.png\" form; passing a document path like \"20250101120000-abc/20250101120000-def.sy\".","commonSituations":"Reusing a full data-relative path (boxID/assets/...) when the function already takes boxID separately; feeding document paths or file annotation paths into an asset resolver; template/plugin code that built the path from a parsed link without stripping the notebook prefix.","solutions":["Strip the box-ID prefix so the path starts with assets/ and pass the notebook ID via the boxID parameter instead","Prepend assets/ if the file genuinely lives in the notebook's assets folder","Use GetAssetAbsPath / ResolveDataAssetPath instead if you need to accept full data-relative paths with embedded box IDs"],"exampleFix":"// before: box-prefixed path with separate boxID\nmodel.GetAssetAbsPathInBox(\"20250101120000-abc/assets/img.png\", \"20250101120000-abc\")\n// after: bare assets path + boxID\nmodel.GetAssetAbsPathInBox(\"assets/img.png\", \"20250101120000-abc\")","handlingStrategy":"validation","validationCode":"if !strings.HasPrefix(path.Clean(p), \"assets/\") {\n\treturn fmt.Errorf(\"%q must start with assets/\", p)\n}","typeGuard":"func isAssetsPath(p string) bool {\n\treturn strings.HasPrefix(path.Clean(strings.TrimSpace(p)), \"assets/\")\n}","tryCatchPattern":"abs, err := model.GetAssetAbsPathInBox(ref, boxID)\nif err != nil && strings.Contains(err.Error(), \"must start with assets/\") {\n\t// strip a box-ID prefix, then retry\n\tif parts := strings.SplitN(path.Clean(ref), \"/\", 3); len(parts) == 3 && parts[1] == \"assets\" {\n\t\tabs, err = model.GetAssetAbsPathInBox(\"assets/\"+parts[2], boxID)\n\t}\n}","preventionTips":["Use the canonical form assets/<file> and pass the notebook ID separately","Do not reuse data-relative (boxID/assets/...) paths with InBox resolvers","Centralize asset-path normalization in one helper used by all callers"],"tags":["validation","asset-resolution","argument-format"],"backgroundTag":"invalid-argument-format","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"}