{"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":882,"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":864,"sourceCodeEnd":900,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/history.go#L864-L900","documentation":"Thrown by CreateAssetHistory when the resolved assetAbsPath exists but is a directory (info.IsDir() returns true). The function is designed to create history snapshots for individual asset files, not directories. A directory cannot be meaningfully snapshotted as a single asset, so the operation is rejected with \"asset path must be a file\".","triggerScenarios":"Calling POST /api/history/createAssetHistory with a path that resolves to a directory under assets/ (e.g. \"assets/subfolder/\" where subfolder is a directory). The path passes both the prefix check and the subpath check, but os.Stat reveals it is a directory.","commonSituations":"User or API client passes a directory path instead of a file path; the assets directory contains subdirectories (e.g. for organizing assets) and the caller references a subdirectory rather than a file within it; a trailing slash causes the path to resolve to a directory.","solutions":["Pass the path to a specific file within assets/, not a directory path.","If you need to snapshot all files in a directory, iterate the directory and call CreateAssetHistory for each file individually.","As an API client, verify the path resolves to a regular file (not a directory) via os.Stat before calling the API."],"exampleFix":"// before\nawait post('/api/history/createAssetHistory', { assetPath: 'assets/screenshots/' })\n\n// after — pass a file path, or iterate files in the directory\nawait post('/api/history/createAssetHistory', { assetPath: 'assets/screenshots/capture.png' })","handlingStrategy":"validation","validationCode":"// Verify the asset path resolves to a file, not a directory\nconst stat = await fs.stat(path.join(dataDir, assetPath))\nif (stat.isFile()) {\n  await post('/api/history/createAssetHistory', { assetPath })\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Pass file paths only, never directory paths, to CreateAssetHistory.","If you need to snapshot multiple assets in a directory, iterate and call per-file.","Check info.IsDir() via stat before submitting to avoid this error."],"tags":["history","asset","validation","filesystem"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}