{"record":{"id":"1fb5707693ba9110","repo":"siyuan-note/siyuan","slug":"asset-path-must-be-under-assets","errorCode":null,"errorMessage":"asset path must be under assets","messagePattern":"asset path must be under assets","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/history.go","lineNumber":918,"sourceCode":"\t})\n\treturn\n}\n\nfunc generateAssetsHistory() {\n\tassets := recentModifiedAssets()\n\tif 1 > len(assets) {\n\t\treturn\n\t}\n\tif err := createAssetsHistory(assets); err != nil {\n\t\tlogging.LogErrorf(\"generate assets history failed: %s\", err)\n\t}\n}\n\n// CreateAssetHistory 为指定资源文件创建历史快照。\nfunc CreateAssetHistory(assetPath string) (err error) {\n\tassetPath = strings.TrimPrefix(filepath.ToSlash(filepath.Clean(filepath.FromSlash(assetPath))), \"/\")\n\tif !strings.HasPrefix(assetPath, \"assets/\") {\n\t\treturn errors.New(\"asset path must be under assets\")\n\t}\n\n\tassetAbsPath := filepath.Join(util.DataDir, filepath.FromSlash(assetPath))\n\tassetsDir := filepath.Join(util.DataDir, \"assets\")\n\tif !gulu.File.IsSubPath(assetsDir, assetAbsPath) {\n\t\treturn errors.New(\"asset path must be under assets\")\n\t}\n\tinfo, statErr := os.Stat(assetAbsPath)\n\tif statErr != nil {\n\t\treturn statErr\n\t}\n\tif info.IsDir() {\n\t\treturn errors.New(\"asset path must be a file\")\n\t}\n\treturn createAssetsHistory([]string{assetAbsPath})\n}\n\nfunc createAssetsHistory(assets []string) (err error) {","sourceCodeStart":900,"sourceCodeEnd":936,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/history.go#L900-L936","documentation":"CreateAssetHistory only snapshots files that live inside the workspace's assets/ directory. After cleaning the input, the kernel requires the slash-relative path to start with assets/; anything else is rejected. This is the first of two layered path checks (the second verifies the absolute path is a true subpath of <data>/assets).","triggerScenarios":"Calling CreateAssetHistory with a path like \"images/foo.png\", an absolute path whose cleaned form retains a leading drive/root so it no longer starts with assets/, an empty string, or a path outside the workspace assets folder.","commonSituations":"Passing absolute filesystem paths instead of workspace-relative asset paths; assets stored in custom locations outside data/assets; trailing leading slash removed incorrectly producing a non-asset prefix.","solutions":["Pass a path relative to the data dir that begins with assets/, e.g. assets/image.png.","Move or copy the file into the workspace assets/ directory first.","Strip any absolute prefix so only the assets/... portion is passed."],"exampleFix":"// before\nCreateAssetHistory(\"/home/user/SiYuan/data/assets/pic.png\")\n// after\nCreateAssetHistory(\"assets/pic.png\")","handlingStrategy":"validation","validationCode":"const cleaned = path.posix.normalize(assetPath.replace(/\\\\/g, \"/\")).replace(/^\\/+/, \"\");\nif (!cleaned.startsWith(\"assets/\")) {\n  throw new Error(\"asset path must be under assets: \" + cleaned);\n}","typeGuard":"function isAssetRelPath(p) {\n  return path.posix.normalize(p.replace(/\\\\/g, \"/\")).replace(/^\\/+/, \"\").startsWith(\"assets/\");\n}","tryCatchPattern":null,"preventionTips":["Always pass data-dir-relative asset paths starting with assets/.","Convert absolute paths to relative before calling.","Centralize asset path construction in one helper."],"tags":["validation","path","assets"],"backgroundTag":"invalid-argument-value","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"}