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

  1. Clean up or move aside the existing files in `saveDir` so a candidate slot is free.
  2. Download to a fresh subdirectory per task rather than a shared flat dir.
  3. Raise MAX_DEDUP_ATTEMPTS if a high-collision dir is intentional and supported.
  4. 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

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


AI-assisted analysis of agalwood/Motrix@1a708ee577 (2026-08-12). Data as JSON: /api/errors/055eb15efbacd6dc. Report an issue: GitHub.