siyuan-note/siyuan · error
asset path must be a file
Error message
asset path must be a file
What it means
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".
Source
Thrown at kernel/model/history.go:882
// CreateAssetHistory 为指定资源文件创建历史快照。
func CreateAssetHistory(assetPath string) (err error) {
assetPath = strings.TrimPrefix(filepath.ToSlash(filepath.Clean(filepath.FromSlash(assetPath))), "/")
if !strings.HasPrefix(assetPath, "assets/") {
return errors.New("asset path must be under assets")
}
assetAbsPath := filepath.Join(util.DataDir, filepath.FromSlash(assetPath))
assetsDir := filepath.Join(util.DataDir, "assets")
if !gulu.File.IsSubPath(assetsDir, assetAbsPath) {
return errors.New("asset path must be under assets")
}
info, statErr := os.Stat(assetAbsPath)
if statErr != nil {
return statErr
}
if info.IsDir() {
return errors.New("asset path must be a file")
}
return createAssetsHistory([]string{assetAbsPath})
}
func createAssetsHistory(assets []string) (err error) {
historyDir, err := getHistoryDir(HistoryOpUpdate)
if err != nil {
return fmt.Errorf("get history directory failed: %w", err)
}
for _, file := range assets {
assetRelPath, relErr := filepath.Rel(filepath.Join(util.DataDir, "assets"), file)
if relErr != nil || assetRelPath == "." || strings.HasPrefix(assetRelPath, ".."+string(filepath.Separator)) {
return errors.New("asset path must be under assets")
}
historyPath := filepath.Join(historyDir, "assets", assetRelPath)
if err = os.MkdirAll(filepath.Dir(historyPath), 0755); err != nil {
return fmt.Errorf("create history directory [%s] failed: %w", filepath.Dir(historyPath), err)View on GitHub (pinned to 251596fc0d)
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.
Example fix
// before
await post('/api/history/createAssetHistory', { assetPath: 'assets/screenshots/' })
// after — pass a file path, or iterate files in the directory
await post('/api/history/createAssetHistory', { assetPath: 'assets/screenshots/capture.png' }) Defensive patterns
Strategy: validation
Validate before calling
// Verify the asset path resolves to a file, not a directory
const stat = await fs.stat(path.join(dataDir, assetPath))
if (stat.isFile()) {
await post('/api/history/createAssetHistory', { assetPath })
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- asset path must be under assets
- import path is not sub path of import dir
- invalid asset type [%s]
- --id is required
- --file is required
AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12).
Data as JSON: /api/errors/9c717eba0881b5ef.
Report an issue: GitHub.