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

  1. Pass the full path to an individual file inside assets/, not a directory.
  2. Filter directory entries out before calling in bulk-snapshot loops.
  3. 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

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


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)