{"record":{"id":"a27c7bef7aa0b689","repo":"schollz/croc","slug":"duplicate-filename-name","errorCode":null,"errorMessage":"Duplicate filename: ${name}","messagePattern":"Duplicate filename: (.+?)","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"web/src/protocol/stored.ts","lineNumber":309,"sourceCode":"  settings: StoredSettings,\n  callbacks: { onStatus?(status: string): void } = {},\n  signal?: AbortSignal,\n) {\n  if (selected.length === 0) throw new Error(\"Choose at least one file\");\n  if (selected.length > settings.maxFiles) {\n    throw new Error(`Stored transfers can contain at most ${settings.maxFiles} files`);\n  }\n  const total = selected.reduce((sum, file) => sum + file.size, 0);\n  if (!Number.isSafeInteger(total) || total > settings.maxTransferBytes) {\n    throw new Error(`Stored transfer exceeds the ${settings.maxTransferBytes} byte limit`);\n  }\n  const names = new Set<string>();\n  const prepared: StoredPreparedFile[] = [];\n  let firstChunk = 0;\n  for (let index = 0; index < selected.length; index += 1) {\n    const file = selected[index];\n    const name = normalizeOutgoingFileName(file.name);\n    if (names.has(name)) throw new Error(`Duplicate filename: ${name}`);\n    names.add(name);\n    callbacks.onStatus?.(`Hashing ${index + 1}/${selected.length}: ${name}`);\n    const chunkCount = Math.ceil(file.size / storedChunkSize);\n    prepared.push({\n      file,\n      name,\n      size: file.size,\n      hash: new Uint8Array(),\n      sha256: await sha256Blob(file, signal),\n      modified: new Date(file.lastModified).toISOString(),\n      firstChunk,\n      chunkCount,\n    });\n    firstChunk += chunkCount;\n  }\n  return prepared;\n}\n","sourceCodeStart":291,"sourceCodeEnd":327,"githubUrl":"https://github.com/schollz/croc/blob/e25f1bdc04f07f094d50b0a1bf67e2563944b57a/web/src/protocol/stored.ts#L291-L327","documentation":"Stored manifests are flat (no directory structure), so prepareStoredFiles() normalizes each name with normalizeOutgoingFileName and rejects any second file that normalizes to an already-seen name. Two different inputs (a/report.pdf and b/report.pdf, or names that differ only in characters the normalizer strips) collide and throw.","triggerScenarios":"Selecting two files with the same basename from different folders; names that are distinct on disk but identical after normalization (case folding, reserved-character stripping, or path removal).","commonSituations":"Users selecting multiple folders each containing README or invoice.pdf; case-insensitive filesystems where Photo.jpg and photo.jpg coexist but would collide on download targets.","solutions":["Rename one of the colliding files before selecting, or move them into a single folder with unique names","Send folders via the direct (relay) transfer mode, which preserves structure, instead of stored mode","Pre-scan the selection for post-normalization duplicates and surface them in the UI before prepareStoredFiles runs"],"exampleFix":"// pre-flight duplicate check\nconst seen = new Set<string>();\nfor (const f of selected) {\n  const name = normalizeOutgoingFileName(f.name);\n  if (seen.has(name)) throw new Error(`Duplicate filename: ${name}`);\n  seen.add(name);\n}","handlingStrategy":"validation","validationCode":"const seen = new Set<string>();\nfor (const f of selected) { const n = normalizeOutgoingFileName(f.name); if (seen.has(n)) { showError(`Duplicate filename: ${n}`); return; } seen.add(n); }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Warn when a multi-folder selection contains equal basenames","Use direct relay transfer when folder structure must be preserved; stored transfers flatten it"],"tags":["validation","filenames","collision","stored-transfer"],"backgroundTag":null,"analyzedSha":"e25f1bdc04f07f094d50b0a1bf67e2563944b57a","analyzedAt":"2026-08-15T12:53:39.096Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}