siyuan-note/siyuan · error

remove unused asset [%s] failed: %w

Error message

remove unused asset [%s] failed: %w

What it means

Returned by RemoveUnusedAsset (assets.go:1473) when filelock.RemoveWithoutFatal fails to delete the asset file after it was successfully copied to history. The asset has already been removed from the SQL index and cache (sql.BatchRemoveAssetsQueue, cache.RemoveAssetHash) and the remove event has been dispatched on mobile, so the file is the last on-disk artifact. The %w preserves the underlying remove error.

Source

Thrown at kernel/model/assets.go:1473

	historyPath := filepath.Join(historyDir, filepath.FromSlash(relativePath))
	if err = filelock.Copy(absPath, historyPath); err != nil {
		err = fmt.Errorf("copy unused asset [%s] to history failed: %w", absPath, err)
		return
	}

	hash, _ := util.GetEtag(absPath)
	sql.BatchRemoveAssetsQueue([]string{hash})
	cache.RemoveAssetHash(hash)

	if util.IsMobileContainer() {
		HandleAssetsRemoveEvent(absPath)
	}

	if err = filelock.RemoveWithoutFatal(absPath); err != nil {
		logging.LogErrorf("remove unused asset [%s] failed: %s", absPath, err)
		util.PushErrMsg(fmt.Sprintf("%s", err), 7000)
		err = fmt.Errorf("remove unused asset [%s] failed: %w", absPath, err)
		return
	}
	ret = absPath

	util.RemoveAssetText(relativePath)

	IncSync()

	indexHistoryDir(filepath.Base(historyDir), util.NewLute())
	cache.RemoveAsset(relativePath)
	return
}

func RenameAsset(oldPath, newName string) (newPath string, err error) {
	util.PushEndlessProgress(Conf.Language(110))
	defer util.PushClearProgress()

	oldCleanPath := AssetPathWithoutQuery(oldPath)

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Inspect the wrapped err for EBUSY/EACCES/EPERM and close whatever process holds the file, then retry RemoveUnusedAsset.
  2. Verify the assets directory is writable and not on a read-only mount.
  3. Note the asset was already de-indexed and copied to history; if you retry and the source is now gone the function will still attempt removal and may report ENOENT — treat a vanished file as success rather than re-erroring.

Example fix

// before: any remove error aborts after index/cache already updated
if err = filelock.RemoveWithoutFatal(absPath); err != nil {
    err = fmt.Errorf("remove unused asset [%s] failed: %w", absPath, err)
    return
}

// after: a vanished file is not fatal once history copy succeeded
if err = filelock.RemoveWithoutFatal(absPath); err != nil {
    if os.IsNotExist(err) {
        err = nil
    } else {
        err = fmt.Errorf("remove unused asset [%s] failed: %w", absPath, err)
        return
    }
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Close any external handle before deletion; the kernel cannot force-remove a locked file.
if (window.siyuan.mobile) await closeAssetViewers(absPath)

Try / catch

// The asset is already de-indexed and history-copied; a vanished file is effectively success.
try {
  await fetchPost('/api/asset/removeUnusedAsset', { path: p })
} catch (e) {
  if (/no such file|not exist/i.test(String(e.message))) return
  throw e
}

Prevention

When it happens

Trigger: Calling /api/asset/removeUnusedAsset when the asset file cannot be deleted: permission denied, file held open by another process, read-only mount, or the path is a directory unexpectedly. The preceding history copy succeeded, so the data is preserved but deletion is blocked.

Common situations: File open in an external viewer/editor (PDF reader, image editor); read-only assets directory; Windows file lock by antivirus/indexer; the asset was converted to a directory by a sync conflict.

Related errors


AI-assisted analysis of siyuan-note/siyuan@251596fc0d (2026-08-12). Data as JSON: /api/errors/9f2f16c3c706b792. Report an issue: GitHub.