siyuan-note/siyuan · error

asset path contains an unresolved symbolic link [%s]

Error message

asset path contains an unresolved symbolic link [%s]

What it means

ResolveAssetPathWithMissingLeaf walks an asset path from the leaf upward to find the deepest existing prefix. When os.Stat fails with an error that is NOT 'not exist', but Lstat reveals the component is a symlink whose target cannot be resolved (dangling or looping symlink), it refuses to silently treat the path as missing-leaf material and returns 'asset path contains an unresolved symbolic link'. This guards the deferred asset-download mechanism against broken symlinks inside the data directory.

Source

Thrown at kernel/model/asset_download_read.go:46

)

// ResolveAssetPathWithMissingLeaf 校验尚未下载的路径,已存在的父目录仍须通过符号链接校验。
func ResolveAssetPathWithMissingLeaf(absPath string) (string, error) {
	current := filepath.Clean(absPath)
	var missing []string
	for {
		resolved, err := ResolveRealPath(current)
		if err == nil {
			for i := len(missing) - 1; i >= 0; i-- {
				resolved = filepath.Join(resolved, missing[i])
			}
			return resolved, nil
		}
		if !os.IsNotExist(err) {
			return "", err
		}
		if info, statErr := os.Lstat(current); statErr == nil && info.Mode()&os.ModeSymlink != 0 {
			return "", fmt.Errorf("asset path contains an unresolved symbolic link [%s]", current)
		}
		parent := filepath.Dir(current)
		if parent == current {
			return "", err
		}
		missing = append(missing, filepath.Base(current))
		current = parent
	}
}

// deferredAssetPath 只解析同步清单,不访问网络,也不以未下载状态推断文件已删除。
func deferredAssetPath(relativePath, boxID string, includeEncrypted bool) (string, error) {
	files, err := DeferredSyncAssets()
	if err != nil {
		return "", err
	}
	return deferredAssetPathFromFiles(relativePath, boxID, includeEncrypted, files)
}

View on GitHub (pinned to 8641553a1f)

Solutions

  1. Find the offending symlink with `find data -xtype l` and remove or repair it so it points to an existing target
  2. Replace the symlink with a real copy of the target file inside the assets directory
  3. Re-run the asset resolution/download so the deferred pipeline can fetch the missing file into a real path
  4. If the symlink target exists on another mount, mount it before using the workspace

Example fix

// shell
// before: data/assets/foo.png -> /mnt/old/foo.png (target gone)
// after
cp /mnt/new/foo.png data/assets/foo.png  # or rm data/assets/foo.png and re-insert the asset
Defensive patterns

Strategy: fallback

Validate before calling

info, err := os.Lstat(assetPath)
if err == nil && info.Mode()&os.ModeSymlink != 0 {
    if _, err := os.Stat(assetPath); err != nil {
        return fmt.Errorf("dangling symlink at %s, repair before use", assetPath)
    }
}

Try / catch

resolved, err := model.ResolveDataAssetPath(relPath)
if err != nil && strings.Contains(err.Error(), "unresolved symbolic link") {
    repaired, repErr := repairOrRemoveSymlink(relPath) // replace with real file
    if repErr != nil { return repErr }
    resolved, err = model.ResolveDataAssetPath(relPath)
}

Prevention

When it happens

Trigger: Calling getFile, ResolveDataAssetPath, or deferredAssetPath where a component of the asset path (e.g. assets/link -> missing/target) is a symlink pointing to a nonexistent target; also reached via unusedAssetsContainPath during asset cleanup scans.

Common situations: User copied a workspace between filesystems and symlinks broke; a sync tool replaced assets with dangling symlinks; a user manually created a symlink in assets/ to a path that no longer exists; a plugin rewrote asset paths as links.

Understand the failure class

Background: "File not found" and ENOENT errors: why libraries can't find a file that should exist — this error's family across 50 libraries.

Related errors


AI-assisted analysis of siyuan-note/siyuan@8641553a1f (2026-09-11). Data as JSON: /api/errors/0137a38a822d1b02. Report an issue: GitHub.