{"record":{"id":"7dde711b6c9ff3bd","repo":"agalwood/Motrix","slug":"taskfinalizerenamefailed","errorCode":"TaskFinalizeRenameFailed","errorMessage":"Failed to rename file: ${cause}","messagePattern":"Failed to rename file: (.+?)","errorType":"exception","errorClass":"AppError","httpStatus":null,"severity":"error","filePath":"src/core/task/actions/finalize-task.ts","lineNumber":230,"sourceCode":"  // RPC error) is logged and we fall through with whatever we already had.\n  await refreshTaskBytesBeforeFinalize(task, deps)\n\n  // removeDownloadResult before rename so aria2 releases the file\n  // handle — on Windows an open handle causes a sharing violation.\n  await deps.adapter.removeDownloadResult(task.engineTaskId)\n\n  try {\n    await deps.fs.renameAtomic(task.diskPath, desiredFinalPath)\n  } catch (e) {\n    const cause = (e as Error).message\n    const errorMessage = `Failed to rename file: ${cause}`\n    await failFinalize(task, deps, {\n      errorMessage,\n      errorDetailKey: 'task.error.detail.renameFileFailed',\n      errorDetailParams: { cause },\n      hookCode: 'TASK_FINALIZE_RENAME_FAILED',\n    })\n    throw new AppError(ErrorCode.TaskFinalizeRenameFailed, errorMessage, e)\n  }\n  const completedAt = Date.now()\n\n  // Commit staged plugin metadata now that rename succeeded. The SQLite\n  // tx body itself is empty here — the rename happened outside the tx\n  // (it's async IO; SQLite transactions must complete synchronously).\n  try {\n    finalizeOutcome.commit(() => {})\n  } catch (err) {\n    deps.log.warn(\n      { taskId: task.id, err: (err as Error).message },\n      'finalize_http_metadata_commit_failed'\n    )\n  }\n\n  task.finalPath = desiredFinalPath\n  const previousStatus = task.status\n  completeTaskAfterRename(","sourceCodeStart":212,"sourceCodeEnd":248,"githubUrl":"https://github.com/agalwood/Motrix/blob/1a708ee57746c434e2c67a44bbf0906a976afea4/src/core/task/actions/finalize-task.ts#L212-L248","documentation":"Thrown by finalize-task.ts:230 when the atomic rename of a completed single-file (HTTP/FTP) download from its `.motrix` temp path to the desired final path fails. `deps.fs.renameAtomic(task.diskPath, desiredFinalPath)` rejected, so finalize cannot move the file into place; `failFinalize` records the failure before rethrowing. The cause string is the underlying FS error (EACCES, EXDEV, ENOSPC, ENAMETOOLONG, etc.).","triggerScenarios":"Calling finalize on a task whose `diskPath` and `desiredFinalPath` live on different filesystems (renameAtomic cannot cross mount boundaries); destination already exists or directory lacks write permission; disk full during the rename; path exceeds OS length limits; the `.motrix` source was already moved/deleted by another process.","commonSituations":"User changed `defaultSaveDir` to a different volume after the download started; antivirus/another process holds or quarantines the file; NAS/SMB mount with rename restrictions; finalPath target on a read-only share.","solutions":["Inspect the `cause` value in the `task.error.detail.renameFileFailed` detail — an `EXDEV` means cross-device; move both temp and final under the same filesystem or implement a copy+delete fallback.","Verify the user account has write permission on `desiredFinalPath`'s parent directory and that the volume has free space.","Ensure `diskPath` (the `.motrix` file) still exists when finalize runs — no external process is cleaning the temp dir mid-finalize.","If renaming across volumes is a supported scenario, fall back to a stream copy followed by unlink instead of `renameAtomic`."],"exampleFix":"// before\nawait deps.fs.renameAtomic(task.diskPath, desiredFinalPath)\n\n// after (cross-volume safe)\ntry {\n  await deps.fs.renameAtomic(task.diskPath, desiredFinalPath)\n} catch (e) {\n  if ((e as NodeJS.ErrnoException).code === 'EXDEV') {\n    await deps.fs.copyFile(task.diskPath, desiredFinalPath)\n    await deps.fs.unlink(task.diskPath)\n  } else {\n    throw e\n  }\n}","handlingStrategy":"try-catch","validationCode":"// Before finalize, confirm same-filesystem + writability\nimport path from 'node:path'\nasync function canRenameAtomic(fs, src, dst) {\n  try {\n    const [s, d] = await Promise.all([fs.stat(path.dirname(src)), fs.stat(path.dirname(dst))])\n    if (s.dev !== d.dev) return { ok: false, reason: 'cross-device' }\n    await fs.access(path.dirname(dst), fs.constants.W_OK)\n    return { ok: true }\n  } catch (e) { return { ok: false, reason: (e).message } }\n}\nconst pre = await canRenameAtomic(deps.fs, task.diskPath, desiredFinalPath)\nif (!pre.ok) { /* copy+delete fallback or surface clear error */ }","typeGuard":"function isAppError(e, code = ErrorCode.TaskFinalizeRenameFailed) { return e instanceof AppError && e.code === code }","tryCatchPattern":"try {\n  await deps.fs.renameAtomic(task.diskPath, desiredFinalPath)\n} catch (e) {\n  const errno = (e).code\n  if (errno === 'EXDEV') {\n    await deps.fs.copyFile(task.diskPath, desiredFinalPath)\n    await deps.fs.unlink(task.diskPath)\n  } else if (errno === 'ENOSPC') {\n    // surface disk-full to user, do not retry blindly\n    throw e\n  } else {\n    throw e\n  }\n}","preventionTips":["Keep the `.motrix` working dir and the final save dir on the same filesystem.","Pre-flight free space and write permission on the target directory before finalizing.","Ensure no external process removes the `.motrix` file mid-finalize.","Implement a cross-volume copy+delete fallback when EXDEV is a supported scenario."],"tags":["filesystem","finalize","rename","download","io"],"backgroundTag":null,"analyzedSha":"1a708ee57746c434e2c67a44bbf0906a976afea4","analyzedAt":"2026-08-12T16:18:09.346Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}