siyuan-note/siyuan · error
asset path does not belong to a notebook
Error message
asset path does not belong to a notebook: %s
What it means
When the path targets a notebook-level assets folder ("<notebookID>/assets/..."), ResolveDataAssetPath verifies the notebook actually exists by checking for <data>/<notebookID>/.siyuan/conf.json. If that config file is missing, the first path segment is not a real notebook ID, so the path is rejected. (Paths into an existing encrypted notebook are separately rejected with a different error.)
Solutions
- Verify the notebook exists (it appears in the doc tree and <data>/<id>/.siyuan/conf.json is present) and correct the ID in the path
- Look up the current notebook ID via the API (e.g. /api/notebook/lsNotebooks) instead of reusing a stored/stale ID
- If the asset should be global, move it to the workspace-level assets/ folder and use "assets/<file>" without a notebook prefix
- Restore the missing notebook folder (or its .siyuan/conf.json) from backup/sync if the notebook was deleted by accident
Example fix
// before
rel, abs, err := model.ResolveDataAssetPath("20240101120000-abcdefg/assets/img.png") // notebook deleted
// after
boxes := model.ListNotebooks() // pick an existing notebook ID
rel, abs, err := model.ResolveDataAssetPath(boxes[0].ID + "/assets/img.png") Defensive patterns
Strategy: validation
Validate before calling
func notebookExists(dataDir, id string) bool {
if !ast.IsNodeIDPattern(id) { return false }
_, err := os.Stat(filepath.Join(dataDir, id, ".siyuan", "conf.json"))
return err == nil
} Type guard
func isNotebookAssetPath(p string) (string, bool) {
parts := strings.Split(p, "/")
if len(parts) > 2 && ast.IsNodeIDPattern(parts[0]) { return parts[0], true }
return "", false
} Try / catch
rel, abs, err := model.ResolveDataAssetPath(p)
if err != nil {
if strings.HasPrefix(err.Error(), "asset path does not belong to a notebook") {
// refresh notebook list and retry with a valid ID, or fall back to global assets
}
return err
} Prevention
- Resolve notebook IDs dynamically via /api/notebook/lsNotebooks instead of hardcoding them
- After deleting or re-importing notebooks, update any stored references to their assets
- Check that <data>/<id>/.siyuan/conf.json exists when constructing notebook-scoped paths
- Prefer global assets/ for assets that must outlive individual notebooks
When it happens
Trigger: Passing "<notebookID>/assets/img.png" where notebookID does not match any notebook in the workspace — e.g. a stale/hardcoded notebook ID after the notebook was deleted, an ID from another workspace, a typo, or a path whose first segment merely looks like a block ID (matches IsNodeIDPattern) but is not a notebook directory.
Common situations: Restored or synced data where the notebook folder was removed but documents referencing its assets remain; scripts hardcoding notebook IDs copied from another machine; renamed/re-imported notebooks generating new IDs; constructing notebook asset paths by string concatenation with an invalid ID.
Understand the failure class
Background: 'Could not be found', 'does not exist', 'not found in database': the resource-not-found family when an ID, slug, key, or URI lookup comes back empty — this error's family across 20 libraries.
Related errors
- 106
- asset path escapes data directory
- asset path is required
- asset path must be a file
- asset path must be under assets
AI-assisted analysis of siyuan-note/siyuan@9f775e8a12 (2026-09-19).
Data as JSON: /api/errors/394ab542e84ecfb2.
Report an issue: GitHub.
Appendix: source
Thrown at kernel/model/assets.go:1060
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]):
for i := 1; i < len(parts)-1; i++ {
if parts[i] == "assets" {
assetDirIndex = i
break
}
}
if assetDirIndex > 0 {
boxConfPath := filepath.Join(util.DataDir, parts[0], ".siyuan", "conf.json")
if !filelock.IsExist(boxConfPath) {
err = fmt.Errorf("asset path does not belong to a notebook: %s", assetPath)
return
}
if IsEncryptedBox(parts[0]) {
err = fmt.Errorf("accessing assets in encrypted notebook [%s] is not supported", parts[0])
return
}
}
}
if assetDirIndex < 0 {
err = fmt.Errorf("path is not under an assets directory: %s", assetPath)
return
}
assetRootParts := parts[:assetDirIndex+1]
assetRoot := filepath.Join(util.DataDir, filepath.FromSlash(strings.Join(assetRootParts, "/")))
if !gulu.File.IsSubPath(assetRoot, absPath) {
err = fmt.Errorf("path is not a child of assets directory: %s", assetPath)
returnView on GitHub (pinned to 9f775e8a12)