siyuan-note/siyuan · error
asset path must be a file
Error message
asset path must be a file
What it means
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.)
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.
Example fix
// before
CreateAssetHistory("assets/images")
// after
CreateAssetHistory("assets/images/logo.png") Defensive patterns
Strategy: validation
Validate before calling
const st = fs.statSync(absPath);
if (st.isDirectory()) {
throw new Error("expected a file, got directory: " + absPath);
} Type guard
function isFile(p) {
try { return fs.statSync(p).isFile(); } catch { return false; }
} Prevention
- Pass individual files, never directories.
- Filter IsDir entries in bulk loops.
- Check stat before calling when input is user-supplied.
When it happens
Trigger: Calling CreateAssetHistory with assets/ itself, an asset subfolder like assets/images/, or any directory path inside assets/.
Common situations: 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.
Related errors
- accessing assets in encrypted notebook
- ambiguous asset path
- asset must be a regular file
- asset path does not belong to a notebook
- asset path escapes data directory
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/9c717eba0881b5ef.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/model/history.go:931
// 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 9f775e8a12)