agalwood/Motrix · error · AppError
TaskFinalizeRenameFailed
TaskFinalizeRenameFailed
Error message
Failed to rename file: ${cause} What it means
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.).
Source
Thrown at src/core/task/actions/finalize-task.ts:230
// RPC error) is logged and we fall through with whatever we already had.
await refreshTaskBytesBeforeFinalize(task, deps)
// removeDownloadResult before rename so aria2 releases the file
// handle — on Windows an open handle causes a sharing violation.
await deps.adapter.removeDownloadResult(task.engineTaskId)
try {
await deps.fs.renameAtomic(task.diskPath, desiredFinalPath)
} catch (e) {
const cause = (e as Error).message
const errorMessage = `Failed to rename file: ${cause}`
await failFinalize(task, deps, {
errorMessage,
errorDetailKey: 'task.error.detail.renameFileFailed',
errorDetailParams: { cause },
hookCode: 'TASK_FINALIZE_RENAME_FAILED',
})
throw new AppError(ErrorCode.TaskFinalizeRenameFailed, errorMessage, e)
}
const completedAt = Date.now()
// Commit staged plugin metadata now that rename succeeded. The SQLite
// tx body itself is empty here — the rename happened outside the tx
// (it's async IO; SQLite transactions must complete synchronously).
try {
finalizeOutcome.commit(() => {})
} catch (err) {
deps.log.warn(
{ taskId: task.id, err: (err as Error).message },
'finalize_http_metadata_commit_failed'
)
}
task.finalPath = desiredFinalPath
const previousStatus = task.status
completeTaskAfterRename(View on GitHub (pinned to 1a708ee577)
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`.
Example fix
// before
await deps.fs.renameAtomic(task.diskPath, desiredFinalPath)
// after (cross-volume safe)
try {
await deps.fs.renameAtomic(task.diskPath, desiredFinalPath)
} catch (e) {
if ((e as NodeJS.ErrnoException).code === 'EXDEV') {
await deps.fs.copyFile(task.diskPath, desiredFinalPath)
await deps.fs.unlink(task.diskPath)
} else {
throw e
}
} Defensive patterns
Strategy: try-catch
Validate before calling
// Before finalize, confirm same-filesystem + writability
import path from 'node:path'
async function canRenameAtomic(fs, src, dst) {
try {
const [s, d] = await Promise.all([fs.stat(path.dirname(src)), fs.stat(path.dirname(dst))])
if (s.dev !== d.dev) return { ok: false, reason: 'cross-device' }
await fs.access(path.dirname(dst), fs.constants.W_OK)
return { ok: true }
} catch (e) { return { ok: false, reason: (e).message } }
}
const pre = await canRenameAtomic(deps.fs, task.diskPath, desiredFinalPath)
if (!pre.ok) { /* copy+delete fallback or surface clear error */ } Type guard
function isAppError(e, code = ErrorCode.TaskFinalizeRenameFailed) { return e instanceof AppError && e.code === code } Try / catch
try {
await deps.fs.renameAtomic(task.diskPath, desiredFinalPath)
} catch (e) {
const errno = (e).code
if (errno === 'EXDEV') {
await deps.fs.copyFile(task.diskPath, desiredFinalPath)
await deps.fs.unlink(task.diskPath)
} else if (errno === 'ENOSPC') {
// surface disk-full to user, do not retry blindly
throw e
} else {
throw e
}
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- TaskCreateTorrentMetaFailed
- TaskCreateDedupExhausted
- MagnetResolveFailed
- GeoIPDownloadFailed
- GeoIPDatabaseInvalid
AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12).
Data as JSON: /api/errors/7dde711b6c9ff3bd.
Report an issue: GitHub.