{"record":{"id":"3c65e41123f69c7c","repo":"siyuan-note/siyuan","slug":"copy-unused-asset-s-to-history-failed-w","errorCode":null,"errorMessage":"copy unused asset [%s] to history failed: %w","messagePattern":"copy unused asset \\[(.+?)\\] to history failed: %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"kernel/model/assets.go","lineNumber":1458,"sourceCode":"\tindexHistoryDir(filepath.Base(historyDir), util.NewLute())\n\tcache.LoadAssets()\n\treturn\n}\n\nfunc RemoveUnusedAsset(p string) (ret string, err error) {\n\trelativePath, absPath, err := ResolveUnusedDataAssetPath(p)\n\tif err != nil {\n\t\treturn\n\t}\n\thistoryDir, err := getHistoryDir(HistoryOpClean)\n\tif err != nil {\n\t\tlogging.LogErrorf(\"get history dir failed: %s\", err)\n\t\treturn\n\t}\n\n\thistoryPath := filepath.Join(historyDir, filepath.FromSlash(relativePath))\n\tif err = filelock.Copy(absPath, historyPath); err != nil {\n\t\terr = fmt.Errorf(\"copy unused asset [%s] to history failed: %w\", absPath, err)\n\t\treturn\n\t}\n\n\thash, _ := util.GetEtag(absPath)\n\tsql.BatchRemoveAssetsQueue([]string{hash})\n\tcache.RemoveAssetHash(hash)\n\n\tif util.IsMobileContainer() {\n\t\tHandleAssetsRemoveEvent(absPath)\n\t}\n\n\tif err = filelock.RemoveWithoutFatal(absPath); err != nil {\n\t\tlogging.LogErrorf(\"remove unused asset [%s] failed: %s\", absPath, err)\n\t\tutil.PushErrMsg(fmt.Sprintf(\"%s\", err), 7000)\n\t\terr = fmt.Errorf(\"remove unused asset [%s] failed: %w\", absPath, err)\n\t\treturn\n\t}\n\tret = absPath","sourceCodeStart":1440,"sourceCodeEnd":1476,"githubUrl":"https://github.com/siyuan-note/siyuan/blob/251596fc0de2f9528c00c224252fd073a99973f4/kernel/model/assets.go#L1440-L1476","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Check the wrapped error (err) for the OS cause — ENOSPC (free disk), EACCES/EPERM (permissions), or ENOENT (source vanished); fix the filesystem condition.","Ensure util.DataDir and the history path are writable and on the same volume as the assets folder so filelock.Copy can rename atomically.","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."],"exampleFix":"// before: generic abort on first copy failure\nif err = filelock.Copy(absPath, historyPath); err != nil {\n    err = fmt.Errorf(\"copy unused asset [%s] to history failed: %w\", absPath, err)\n    return\n}\n\n// after: tolerate a vanished source (already cleaned) and surface the rest\nif err = filelock.Copy(absPath, historyPath); err != nil {\n    if os.IsNotExist(err) {\n        return // nothing to do, asset already gone\n    }\n    err = fmt.Errorf(\"copy unused asset [%s] to history failed: %w\", absPath, err)\n    return\n}","handlingStrategy":"validation","validationCode":"// Before calling /api/asset/removeUnusedAsset, confirm the file exists and the dir is writable.\nconst exists = await fetchPost('/api/file/getFile', { path: assetPath })\nif (!exists) { /* already gone; nothing to do */ return }","typeGuard":null,"tryCatchPattern":"// Treat a vanished source as non-fatal since history copy is the precondition.\ntry {\n  await fetchPost('/api/asset/removeUnusedAsset', { path: p })\n} catch (e) {\n  if (!/not exist|no such file/i.test(String(e.message))) throw e\n}","preventionTips":["Ensure the data dir and history path share a volume so filelock.Copy can rename atomically.","Close external viewers holding the asset before cleanup.","Monitor free disk space; ENOSPC is the most common copy-failure cause."],"tags":["assets","filesystem","history","cleanup","go"],"backgroundTag":null,"analyzedSha":"251596fc0de2f9528c00c224252fd073a99973f4","analyzedAt":"2026-08-12T21:18:37.123Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}