siyuan-note/siyuan · error

copy unused asset [%s] to history failed: %w

Error message

copy unused asset [%s] to history failed: %w

What it means

Returned by RemoveUnusedAsset (assets.go:1458) when filelock.Copy fails while copying a single unused asset from its absPath into the HistoryOpClean history directory before deletion. The %w preserves the underlying copy error. The function has already resolved the path via ResolveUnusedDataAssetPath and obtained historyDir via getHistoryDir.

Source

Thrown at kernel/model/assets.go:1458

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

func RemoveUnusedAsset(p string) (ret string, err error) {
	relativePath, absPath, err := ResolveUnusedDataAssetPath(p)
	if err != nil {
		return
	}
	historyDir, err := getHistoryDir(HistoryOpClean)
	if err != nil {
		logging.LogErrorf("get history dir failed: %s", err)
		return
	}

	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

View on GitHub (pinned to 251596fc0d)

Solutions

  1. Check the wrapped error (err) for the OS cause — ENOSPC (free disk), EACCES/EPERM (permissions), or ENOENT (source vanished); fix the filesystem condition.
  2. Ensure util.DataDir and the history path are writable and on the same volume as the assets folder so filelock.Copy can rename atomically.
  3. Re-run RemoveUnusedAsset after the condition clears; the history copy is the precondition for safe deletion, so the asset is left in place on failure.

Example fix

// before: generic abort on first copy failure
if err = filelock.Copy(absPath, historyPath); err != nil {
    err = fmt.Errorf("copy unused asset [%s] to history failed: %w", absPath, err)
    return
}

// after: tolerate a vanished source (already cleaned) and surface the rest
if err = filelock.Copy(absPath, historyPath); err != nil {
    if os.IsNotExist(err) {
        return // nothing to do, asset already gone
    }
    err = fmt.Errorf("copy unused asset [%s] to history failed: %w", absPath, err)
    return
}
Defensive patterns

Strategy: validation

Validate before calling

// Before calling /api/asset/removeUnusedAsset, confirm the file exists and the dir is writable.
const exists = await fetchPost('/api/file/getFile', { path: assetPath })
if (!exists) { /* already gone; nothing to do */ return }

Try / catch

// Treat a vanished source as non-fatal since history copy is the precondition.
try {
  await fetchPost('/api/asset/removeUnusedAsset', { path: p })
} catch (e) {
  if (!/not exist|no such file/i.test(String(e.message))) throw e
}

Prevention

When it happens

Trigger: Invoking the single-asset cleanup API (RemoveUnusedAsset, exposed as /api/asset/removeUnusedAsset) when the source asset file is missing or unreadable between listing and copy, when the history directory cannot be written (disk full, permission denied), or when the OS rename/copy across volumes fails inside filelock.Copy.

Common situations: External process deleted or locked the asset after UnusedAssets listed it; read-only or full data directory; cross-device copy where the history dir lives on a different mount; antivirus locking the file on Windows.

Related errors


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