{"record":{"id":"9c717eba0881b5ef","repo":"siyuan-note/siyuan","slug":"asset-path-must-be-a-file","errorCode":null,"errorMessage":"asset path must be a file","messagePattern":"asset path must be a file","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/history.go","lineNumber":931,"sourceCode":"\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) {\n\thistoryDir, err := getHistoryDir(HistoryOpUpdate)\n\tif err != nil {\n\t\treturn fmt.Errorf(\"get history directory failed: %w\", err)\n\t}\n\n\tfor _, file := range assets {\n\t\tassetRelPath, relErr := filepath.Rel(filepath.Join(util.DataDir, \"assets\"), file)\n\t\tif relErr != nil || assetRelPath == \".\" || strings.HasPrefix(assetRelPath, \"..\"+string(filepath.Separator)) {\n\t\t\treturn errors.New(\"asset path must be under assets\")\n\t\t}\n\t\thistoryPath := filepath.Join(historyDir, \"assets\", assetRelPath)\n\t\tif err = os.MkdirAll(filepath.Dir(historyPath), 0755); err != nil {\n\t\t\treturn fmt.Errorf(\"create history directory [%s] failed: %w\", filepath.Dir(historyPath), err)","sourceCodeStart":913,"sourceCodeEnd":949,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/9f775e8a12daef8255556097396f9b2739078892/kernel/model/history.go#L913-L949","documentation":"After both path checks pass, CreateAssetHistory stats the resolved file. If the path exists but is a directory, snapshotting is meaningless and the kernel returns this error. (A missing path surfaces as the wrapped os.Stat error instead.)","triggerScenarios":"Calling CreateAssetHistory with assets/ itself, an asset subfolder like assets/images/, or any directory path inside assets/.","commonSituations":"Passing the assets folder rather than a concrete file; deriving the target from a block reference that points at a folder; scripts iterating directory entries without filtering IsDir.","solutions":["Pass the full path to an individual file inside assets/, not a directory.","Filter directory entries out before calling in bulk-snapshot loops.","Check os.Stat(...).IsDir() client-side first if the input is user-supplied."],"exampleFix":"// before\nCreateAssetHistory(\"assets/images\")\n// after\nCreateAssetHistory(\"assets/images/logo.png\")","handlingStrategy":"validation","validationCode":"const st = fs.statSync(absPath);\nif (st.isDirectory()) {\n  throw new Error(\"expected a file, got directory: \" + absPath);\n}","typeGuard":"function isFile(p) {\n  try { return fs.statSync(p).isFile(); } catch { return false; }\n}","tryCatchPattern":null,"preventionTips":["Pass individual files, never directories.","Filter IsDir entries in bulk loops.","Check stat before calling when input is user-supplied."],"tags":["validation","assets","filesystem"],"backgroundTag":"path-is-not-a-directory","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"}