siyuan-note/siyuan · error
[ ] is not an asset path (must start with assets/)
Error message
[%s] is not an asset path (must start with assets/)
What it means
After cleaning, GetAssetAbsPathInBox requires the asset path to begin with the 'assets/' prefix. A relative path that passes the traversal check but points outside an assets directory (e.g. 'foo.png', 'images/foo.png', a .sy document path) cannot be resolved as an asset and triggers this error. Companion message to 'is not an asset path' for the prefix-specific case.
Solutions
- Strip the box-ID prefix so the path starts with assets/ and pass the notebook ID via the boxID parameter instead
- Prepend assets/ if the file genuinely lives in the notebook's assets folder
- Use GetAssetAbsPath / ResolveDataAssetPath instead if you need to accept full data-relative paths with embedded box IDs
Example fix
// before: box-prefixed path with separate boxID
model.GetAssetAbsPathInBox("20250101120000-abc/assets/img.png", "20250101120000-abc")
// after: bare assets path + boxID
model.GetAssetAbsPathInBox("assets/img.png", "20250101120000-abc") Defensive patterns
Strategy: validation
Validate before calling
if !strings.HasPrefix(path.Clean(p), "assets/") {
return fmt.Errorf("%q must start with assets/", p)
} Type guard
func isAssetsPath(p string) bool {
return strings.HasPrefix(path.Clean(strings.TrimSpace(p)), "assets/")
} Try / catch
abs, err := model.GetAssetAbsPathInBox(ref, boxID)
if err != nil && strings.Contains(err.Error(), "must start with assets/") {
// strip a box-ID prefix, then retry
if parts := strings.SplitN(path.Clean(ref), "/", 3); len(parts) == 3 && parts[1] == "assets" {
abs, err = model.GetAssetAbsPathInBox("assets/"+parts[2], boxID)
}
} Prevention
- Use the canonical form assets/<file> and pass the notebook ID separately
- Do not reuse data-relative (boxID/assets/...) paths with InBox resolvers
- Centralize asset-path normalization in one helper used by all callers
When it happens
Trigger: Calling GetAssetAbsPathInBox with "foo.png" or "20250101120000-abc/assets/img.png" (box-prefixed form) instead of the required bare "assets/img.png" form; passing a document path like "20250101120000-abc/20250101120000-def.sy".
Common situations: Reusing a full data-relative path (boxID/assets/...) when the function already takes boxID separately; feeding document paths or file annotation paths into an asset resolver; template/plugin code that built the path from a parsed link without stripping the notebook prefix.
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/50779f10dc9f0cad.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/model/assets.go:1227
cleanPath = filepath.ToSlash(relativePath)
return
}
// GetAssetAbsPathInBox 在指定 box 内解析资源绝对路径,不进行全局遍历。
// relativePath 必须以 assets/ 前缀开头,boxID 为空且路径没有 box 查询参数时只解析普通/全局资源,不遍历加密 box。
// 加密 box 直接从 <boxID>/assets/ 查找,不依赖后缀匹配。
func GetAssetAbsPathInBox(relativePath, boxID string) (string, error) {
var err error
relativePath, boxID, err = assetPathAndBox(relativePath, boxID)
if err != nil {
return "", err
}
relativePath = path.Clean(relativePath)
if relativePath == "." || strings.HasPrefix(relativePath, "../") || relativePath == ".." || path.IsAbs(relativePath) {
return "", fmt.Errorf("[%s] is not an asset path", relativePath)
}
if !strings.HasPrefix(relativePath, "assets/") {
return "", fmt.Errorf("[%s] is not an asset path (must start with assets/)", relativePath)
}
if boxID != "" && !ast.IsNodeIDPattern(boxID) {
return "", fmt.Errorf("[%s] is not a box id", boxID)
}
if boxID == "" {
return GetAssetAbsPathWithOpt(relativePath, false)
}
p := filepath.Join(util.DataDir, boxID, relativePath)
if gulu.File.IsExist(p) {
if !gulu.File.IsSubPath(util.WorkspaceDir, p) {
return "", fmt.Errorf("[%s] is not sub path of workspace", p)
}
// 解析符号链接/目录联接,防止软链接跳出资产根目录
if realP, evalErr := filepath.EvalSymlinks(p); evalErr == nil && realP != p {
if !gulu.File.IsSubPath(util.WorkspaceDir, realP) {
return "", fmt.Errorf("symlink [%s] resolves outside workspace: [%s]", p, realP)View on GitHub (pinned to 9f775e8a12)