agalwood/Motrix · error · AppError
TaskCreateDedupExhausted
TaskCreateDedupExhausted
Error message
Too many files with name "${desiredName}" already exist in ${saveDir} What it means
Thrown by final-name-picker.ts:33 when picking a de-duplicated final name: it tried `desiredName`, then `desiredName (1)`, `(2)`, ... up to `MAX_DEDUP_ATTEMPTS` and every candidate already exists (as either the final path or its `.motrix` temp sibling). No unique slot could be found.
Source
Thrown at src/core/task/final-name-picker.ts:33
export class FinalNamePickerImpl implements FinalNamePicker {
constructor(private readonly fs: FsProbe) {}
async pick(saveDir: string, desiredName: string): Promise<string> {
if (!(await this.isTaken(saveDir, desiredName))) {
return desiredName
}
const { base, ext } = splitNameExt(desiredName)
for (let n = 1; n <= MAX_DEDUP_ATTEMPTS; n++) {
const candidate = ext ? `${base} (${n})${ext}` : `${base} (${n})`
if (!(await this.isTaken(saveDir, candidate))) {
return candidate
}
}
throw new AppError(
ErrorCode.TaskCreateDedupExhausted,
`Too many files with name "${desiredName}" already exist in ${saveDir}`
)
}
private async isTaken(dir: string, name: string): Promise<boolean> {
const finalPath = path.join(dir, name)
const tempPath = finalPath + INCOMPLETE_SUFFIX
const [f, t] = await Promise.all([
this.fs.exists(finalPath),
this.fs.exists(tempPath),
])
return f || t
}
}
/**
* Split "foo.mp4" into { base: "foo", ext: ".mp4" }.View on GitHub (pinned to 1a708ee577)
Solutions
- Clean up or move aside the existing files in `saveDir` so a candidate slot is free.
- Download to a fresh subdirectory per task rather than a shared flat dir.
- Raise MAX_DEDUP_ATTEMPTS if a high-collision dir is intentional and supported.
- Have the caller supply an explicit unique `filename` so dedup is bypassed.
Example fix
// before
for (let n = 1; n <= MAX_DEDUP_ATTEMPTS; n++) {
const candidate = ext ? `${base} (${n})${ext}` : `${base} (${n})`
if (!(await this.isTaken(saveDir, candidate))) return candidate
}
throw new AppError(ErrorCode.TaskCreateDedupExhausted, `Too many files with name "${desiredName}" already exist in ${saveDir}`)
// after — fall back to a timestamped unique name instead of hard-failing
const candidate = ext ? `${base}-${Date.now()}${ext}` : `${base}-${Date.now()}`
if (!(await this.isTaken(saveDir, candidate))) return candidate
throw new AppError(ErrorCode.TaskCreateDedupExhausted, `Too many files with name "${desiredName}" already exist in ${saveDir}`) Defensive patterns
Strategy: validation
Validate before calling
async function freeSlotExists(fs, saveDir, base, ext, max) {
if (!(await isTaken(saveDir, ext ? `${base}${ext}` : base))) return true
for (let n = 1; n <= max; n++) {
const c = ext ? `${base} (${n})${ext}` : `${base} (${n})`
if (!(await isTaken(saveDir, c))) return true
}
return false
}
if (!(await freeSlotExists(fs, saveDir, base, ext, MAX_DEDUP_ATTEMPTS))) {
// ask user for an explicit filename, or pick a new subdir
} Type guard
function isDedupExhausted(e) { return e instanceof AppError && e.code === ErrorCode.TaskCreateDedupExhausted } Try / catch
try {
finalName = await finalNamePicker.pick(saveDir, desiredName)
} catch (e) {
if (e instanceof AppError && e.code === ErrorCode.TaskCreateDedupExhausted) {
finalName = await finalNamePicker.pick(saveDir, `${desiredName}-${Date.now()}`)
} else throw e
} Prevention
- Use a per-task subdirectory rather than a flat shared dir for repeated names.
- Have the caller supply an explicit unique filename to bypass dedup.
- Clean up old completed files that share the base name.
- Raise MAX_DEDUP_ATTEMPTS if high-collision dirs are intentional.
When it happens
Trigger: The save directory already contains MAX_DEDUP_ATTEMPTS+1 files sharing the same base name (e.g. 50+ 'report.pdf' / 'report (1).pdf' ...); repeated re-downloads of the same filename into a busy directory; an external process is racing to create the same candidates.
Common situations: Bulk re-downloading the same attachment many times; a shared save dir used by many concurrent tasks with templated names; old completed downloads never cleaned; a sync/mirror folder already populated with numbered copies.
Related errors
- TaskCreateTorrentMetaFailed
- TaskFinalizeRenameFailed
- MagnetResolveFailed
- plugin.ffmpeg.destination_phase_disallowed
- IpcInvalidPayload
AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12).
Data as JSON: /api/errors/055eb15efbacd6dc.
Report an issue: GitHub.