{"record":{"id":"0af14c53042fcaac","repo":"schollz/croc","slug":"choose-at-least-one-file-0af14c","errorCode":null,"errorMessage":"Choose at least one file","messagePattern":"Choose at least one file","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"web/src/protocol/stored.ts","lineNumber":295,"sourceCode":"    for (;;) {\n      checkAbort(signal);\n      const { done, value } = await reader.read();\n      if (done) break;\n      await engine.sha256Update(handle, value);\n    }\n    return await engine.sha256Final(handle);\n  } finally {\n    reader.releaseLock();\n  }\n}\n\nexport async function prepareStoredFiles(\n  selected: File[],\n  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({","sourceCodeStart":277,"sourceCodeEnd":313,"githubUrl":"https://github.com/schollz/croc/blob/e25f1bdc04f07f094d50b0a1bf67e2563944b57a/web/src/protocol/stored.ts#L277-L313","documentation":"prepareStoredFiles() rejects an empty selection up front: a stored transfer must contain at least one file. The check runs before any hashing or limits, so it is a pure input-validation error signaling the caller (usually UI state) allowed a submit with nothing picked.","triggerScenarios":"Calling prepareStoredFiles([]) — e.g., a file input that was cleared, drag-select that matched nothing, or a folder with no files passed through.","commonSituations":"UI bugs where the selection state resets before submit; programmatic pipelines that glob a directory and pass the empty result without checking.","solutions":["Disable the send button until files.length > 0 in the UI","Guard at the call site: if (!files.length) return, before invoking prepareStoredFiles","Show a file-chooser error message so the user re-selects files"],"exampleFix":"// before\nconst prepared = await prepareStoredFiles(selected, settings);\n\n// after\nif (selected.length === 0) { alert(\"Choose at least one file\"); return; }\nconst prepared = await prepareStoredFiles(selected, settings);","handlingStrategy":"validation","validationCode":"if (selected.length === 0) { showError(\"Choose at least one file\"); return; }","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Disable submit actions while the file selection is empty","Validate selection emptiness in the UI layer, not after starting the hashing pipeline"],"tags":["validation","ui","input"],"backgroundTag":null,"analyzedSha":"e25f1bdc04f07f094d50b0a1bf67e2563944b57a","analyzedAt":"2026-08-15T12:53:39.096Z","schemaVersion":2},"datasetVersion":"2026-08-15T17:31:12.345Z"}