siyuan-note/siyuan · error

asset path must be relative to data directory

Error message

asset path must be relative to data directory: %s

What it means

ResolveDataAssetPath only accepts paths relative to the workspace data directory. After converting to the OS-native form, it rejects any input that is absolute (filepath.IsAbs), carries a Windows volume name (C:...), or starts with a path separator. The error echoes the offending path so the caller can correct it.

Solutions

  1. Strip the data directory prefix and pass only the remainder, e.g. "assets/img.png" instead of "/abs/path/to/workspace/data/assets/img.png"
  2. Use filepath.Rel(util.DataDir, absPath) to convert an absolute path you already have into the required data-relative form before calling
  3. Remove leading slashes / file:// scheme from the input; for URLs, extract and decode the path portion first
  4. If the path comes from GetAssetAbsPath or similar, use the corresponding relative-path accessor instead

Example fix

// before
rel, abs, err := model.ResolveDataAssetPath("/home/me/siyuan/data/assets/pic.png")
// after
relPath, _ := filepath.Rel(util.DataDir, "/home/me/siyuan/data/assets/pic.png") // "assets/pic.png"
rel, abs, err := model.ResolveDataAssetPath(filepath.ToSlash(relPath))
Defensive patterns

Strategy: validation

Validate before calling

func toDataRelative(absPath, dataDir string) (string, error) {
    rel, err := filepath.Rel(dataDir, absPath)
    if err != nil || strings.HasPrefix(rel, "..") || filepath.IsAbs(rel) {
        return "", fmt.Errorf("not under data dir: %s", absPath)
    }
    return filepath.ToSlash(rel), nil
}

Type guard

func isRelative(p string) bool {
    n := filepath.FromSlash(p)
    return !filepath.IsAbs(n) && filepath.VolumeName(n) == "" && !os.IsPathSeparator(n[0])
}

Try / catch

rel, abs, err := model.ResolveDataAssetPath(p)
if err != nil {
    if strings.HasPrefix(err.Error(), "asset path must be relative") {
        p = strings.TrimPrefix(p, util.DataDir+string(filepath.Separator))
        rel, abs, err = model.ResolveDataAssetPath(p)
    }
    if err != nil { return err }
}

Prevention

When it happens

Trigger: Passing an absolute path like "/home/user/SiYuan/data/assets/img.png" or "C:\\data\\assets\\img.png", a path starting with a leading slash even if otherwise relative ("/assets/img.png"), or a full file:// URL to ResolveDataAssetPath (directly or through assetStat, PrepareAgentMessageImage, ResolveUnusedDataAssetPath, etc.).

Common situations: Scripts or plugins use paths copied from the OS file manager or from an API response containing absolute paths; code builds paths with filepath.Join(util.DataDir, ...) before calling the resolver, double-qualifying the path; Windows clients send backslash-prefixed drive paths.

Understand the failure class

Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19). Data as JSON: /api/errors/aab882f460cda625. Report an issue: GitHub.

Appendix: source

Thrown at kernel/model/assets.go:1032

	}
	return
}

func GetAssetAbsPath(relativePath string) (string, error) {
	return GetAssetAbsPathWithOpt(relativePath, false)
}

// ResolveDataAssetPath 解析 data 相对资源路径,并确保目标位于全局或普通笔记本的资源目录中。
func ResolveDataAssetPath(assetPath string) (relativePath, absPath string, err error) {
	if assetPath == "" {
		err = errors.New("asset path is required")
		return
	}

	nativePath := filepath.FromSlash(assetPath)
	if filepath.IsAbs(nativePath) || filepath.VolumeName(nativePath) != "" ||
		(len(nativePath) > 0 && os.IsPathSeparator(nativePath[0])) {
		err = fmt.Errorf("asset path must be relative to data directory: %s", assetPath)
		return
	}

	nativePath = filepath.Clean(nativePath)
	absPath = filepath.Join(util.DataDir, nativePath)
	dataRelativePath, relErr := filepath.Rel(util.DataDir, absPath)
	if relErr != nil || dataRelativePath == "." || dataRelativePath == ".." ||
		strings.HasPrefix(dataRelativePath, ".."+string(filepath.Separator)) {
		err = fmt.Errorf("asset path escapes data directory: %s", assetPath)
		return
	}

	parts := strings.Split(filepath.ToSlash(dataRelativePath), "/")
	assetDirIndex := -1
	switch {
	case len(parts) > 1 && parts[0] == "assets":
		assetDirIndex = 0
	case len(parts) > 2 && ast.IsNodeIDPattern(parts[0]):

View on GitHub (pinned to 9f775e8a12)