본문으로 건너뛰기
cccskills
무료GitHub에 공개

web-files-file-upload-patterns

File upload patterns - drag-drop dropzones, chunked and resumable uploads, presigned URL flows, file validation (MIME type, magic bytes), progress tracking, accessibility (ARIA)

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

설치 방법 보기

포함된 파일(10)

  • SKILL.md12.2 KB
  • examples/accessibility.md7.9 KB
  • examples/core.md15.5 KB
  • examples/presigned-upload.md13.3 KB
  • examples/preview.md3.4 KB
  • examples/progress.md13.4 KB
  • examples/resumable.md18.0 KB
  • examples/validation.md12.7 KB
  • metadata.yaml365 B
  • reference.md5.0 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

File Upload Patterns

Quick Guide: A dropzone is a keyboard-operable button wrapping a hidden file input, with drag as an enhancement. Validate for the user's benefit on the client — extension, MIME type, then the file's own magic bytes — and again on the server, because none of the client checks are security. Progress needs XMLHttpRequest; fetch has no upload progress event. Past roughly 100MB, chunk the file so a failure costs one chunk. Large files go straight to storage on a presigned URL the server issues, so no request body is ever proxied.

Detailed Resources:

  • examples/core.md — file input, dropzone, file list state and rendering, the assembled component
  • examples/validation.md — rule-based validator, magic-byte detection, dimension checks, a validation hook
  • examples/progress.md — XHR progress with speed and ETA, progress bar, formatters, concurrent uploads
  • examples/preview.md — a preview thumbnail for a selected file, with cleanup
  • examples/presigned-upload.md — PUT and POST-policy uploads, the server contract, multipart parts, the whole flow as a hook
  • examples/resumable.md — chunked uploader with retry, resume across a reload, a tus client, the tus server contract
  • examples/accessibility.md — announcing selection and progress, focus return after the file dialog
  • reference.md — method selection by size, expiry guidance, validation order, CORS, review checklist

Which path applies

The destination decides almost everything else.

  • The file goes to your own endpoint — one POST with FormData, progress from XHR, and a size cap the server can enforce. examples/core.md and examples/progress.md are the whole of it.
  • The file goes to object storage — the server issues a presigned URL and the browser uploads to it directly, so no bytes pass through your application. examples/presigned-upload.md.
  • The file is large enough that a failure hurts — split it, upload the parts with a concurrency limit, and record which parts landed so a retry resumes. examples/resumable.md.

<critical_requirements>

Before writing upload code

Validate on the server as well as in the browser. Client validation exists to tell the user quickly what will be rejected; anyone can skip it entirely, so it settles nothing about safety.

Read the file's first bytes when the type matters. Extensions and MIME types are both supplied by whoever made the file, and a renamed executable passes every check that trusts them.

Revoke every object URL you create. A preview holds the whole file in memory until URL.revokeObjectURL() runs, so a user who changes their mind three times leaks three files.

Make the dropzone reachable from the keyboard. role="button", tabIndex={0} and an Enter/Space handler that opens the file dialog, with drag layered on top — mobile has no drag at all, so the click path is the real one.

Have the server issue a short-lived presigned URL rather than proxying the body. The upload then costs your application nothing, and no storage credential is ever in reach of the browser.

</critical_requirements>


Auto-detection: dropzone, dataTransfer.files, dragenter, dragleave, dragover, input type="file", event.target.files, accept attribute, xhr.upload.addEventListener, lengthComputable, presigned URL, uploadUrl, multipart upload, UploadPart, ETag, chunked upload, file.slice, Content-Range, resumable upload, tus, Tus-Resumable, Upload-Offset, magic bytes, file signature, FormData append file

Applies to:

  • Selecting files by click, keyboard or drag
  • Validating type, size and dimensions before anything is sent
  • Reporting progress, speed and remaining time, and cancelling
  • Uploading straight to storage on a URL the server signed
  • Splitting a large file into chunks and resuming an interrupted upload
  • Announcing selection, progress and failure to a screen reader

Handled elsewhere:

  • Receiving, scanning and storing the bytes once they arrive
  • Resizing, cropping or converting an image before it is sent — this skill sends the File it is given
  • Where the stored object lives, how it is served, and what its URL looks like
  • Streaming playback of media that was uploaded

<philosophy>

Philosophy

An upload is three independent problems that get conflated: choosing a file, checking it, and moving its bytes. Keeping them separate is what makes any of them replaceable.

The checking half has a rule that never bends. Client validation is a user-experience feature, and the server's is the only one that is a control. Everything the browser knows about a file — its name, its extension, its type — came from the file itself. Reading magic bytes raises the bar but does not change the category: it is still a check the client can be made to skip.

The moving half scales by a different axis: not how many files, but how long a single request is open. A short request can fail and be retried whole. A long one accumulates the probability of a dropped connection until retrying whole is unacceptable, and that is the point at which chunking starts paying for its complexity — not at a particular byte count.

</philosophy>
<patterns>

Core patterns

Pattern 1: Dropzone

Count drag events rather than tracking a boolean. dragenter and dragleave fire for every nested element, so a boolean flickers off the moment the pointer crosses a child.

const dragCounterRef = useRef(0);

<div
  onDragEnter={() => { dragCounterRef.current++; setState("drag-over"); }}
  onDragLeave={() => {
    dragCounterRef.current--;
    if (dragCounterRef.current === 0) setState("idle");
  }}
  onDragOver={(e) => e.preventDefault()} // without this, drop never fires
  onDrop={handleDrop}
  onClick={() => inputRef.current?.click()}
  onKeyDown={(e) => {
    if (e.key === "Enter" || e.key === " ") inputRef.current?.click();
  }}
  role="button"
  tabIndex={disabled ? -1 : 0}
  aria-label="File upload area. Click or drag files to upload."
>
  <input ref={inputRef} type="file" hidden aria-hidden="true" tabIndex={-1} />
</div>

Full code: examples/core.md

Pattern 2: File list state

One entry per file with its own status, so a failure is per-file rather than per-batch. Rejections come back with reasons the UI can show.

interface FileWithId {
  id: string;
  file: File;
  preview?: string;
  status: "pending" | "uploading" | "success" | "error";
  progress: number;
  error?: string;
}

// addFiles returns { added, rejected }, each rejection carrying its reason
// removeFile and clearFiles revoke any preview URL before dropping the entry

Full code: examples/core.md

Pattern 3: Progress with XHR

fetch reports download progress and not upload progress, so upload progress means XMLHttpRequest. Average the last few samples or the speed reading jitters unusably.

const xhr = new XMLHttpRequest();
xhr.upload.addEventListener("progress", (event) => {
  if (!event.lengthComputable) return; // no total: show a spinner, not a bar
  const speed = rollingAverageSpeed(event.loaded, performance.now());
  setProgress({
    loaded: event.loaded,
    total: event.total,
    percentage: Math.round((event.loaded / event.total) * 100),
    speed,
    remainingTime: (event.total - event.loaded) / speed,
  });
});

xhr.abort() is the cancel. Streaming a fetch body measures bytes you handed the stream rather than bytes on the wire, which is why it is not a substitute.

Full code: examples/progress.md

Pattern 4: Magic-byte detection

Read the first twelve bytes and compare against known signatures. Never read the whole file — a large one freezes the tab.

const FILE_SIGNATURES = [
  { mime: "image/jpeg", extension: "jpg", signature: [0xff, 0xd8, 0xff] },
  { mime: "image/png", extension: "png", signature: [0x89, 0x50, 0x4e, 0x47] },
  {
    mime: "application/pdf",
    extension: "pdf",
    signature: [0x25, 0x50, 0x44, 0x46],
  },
  {
    mime: "application/zip",
    extension: "zip",
    signature: [0x50, 0x4b, 0x03, 0x04],
  },
];

const buffer = await file.slice(0, 12).arrayBuffer();
const bytes = new Uint8Array(buffer);

Office documents are ZIP archives, so a ZIP match needs a second look: word/, xl/ or ppt/ in the first kilobyte identifies which.

Full code: examples/validation.md

Pattern 5: Presigned upload

Four steps, and your application never holds the bytes:

  1. The client asks your server for a URL, sending name, type and size.
  2. The server authorises the request, sanitises the name, builds a key, and signs a short-lived URL.
  3. The client PUTs the file to that URL.
  4. The client tells your server the key, and the server records it.
const { uploadUrl, key } = await requestPresignedUrl(file);

const xhr = new XMLHttpRequest();
xhr.open("PUT", uploadUrl);
xhr.setRequestHeader("Content-Type", file.type);
xhr.upload.addEventListener("progress", reportProgress);
xhr.send(file);

A POST-policy URL instead of a PUT lets the storage service enforce size and content-type itself — at the cost of FormData field order mattering, with the file appended last.

Full code: examples/presigned-upload.md

Pattern 6: Chunked and resumable

Slice the file, upload the slices with a concurrency limit, and retry a failed slice with exponential backoff rather than restarting.

const DEFAULT_CHUNK_SIZE = 5 * 1024 * 1024;

const start = chunkIndex * chunkSize;
const chunk = file.slice(start, Math.min(start + chunkSize, file.size));

Persist the completed chunk indexes keyed on name-size-lastModified, so a reload resumes rather than restarts, and expire that record after a day. For an interoperable protocol rather than your own, tus is POST to create, HEAD to learn the offset, PATCH to append.

Full code: examples/resumable.md

</patterns>

<red_flags>

Red flags

Breaks at runtime:

  • No event.preventDefault() on dragover — drop never fires and the browser navigates to the file instead — prevent the default on both dragover and drop
  • A boolean for drag state — it flickers as the pointer crosses child elements — count dragenter and dragleave in a ref
  • The file input's value left set after a selection — choosing the same file twice fires no change event — assign event.target.value = "" after reading files
  • fetch used where progress is required — there is no upload progress event and stream progress measures the wrong thing — use XMLHttpRequest
  • file.text() or readAsDataURL() to inspect a type — the whole file is read into memory — slice the first 12 bytes
  • An object URL created in a render body — a new one per render, none revoked — create it in an effect and revoke in the cleanup
  • No CORS configuration on the storage bucket — a direct upload fails preflight — allow the origin and the methods, and expose ETag for multipart

Surprising behaviour:

  • A presigned URL is a bearer token: whoever holds it can perform that operation until it expires
  • Extension, MIME type and File.type all come from the client and are all forgeable
  • Mobile browsers have no drag and drop, so the click path is the only path there
  • lengthComputable is false for a request with no known length, and the percentage is meaningless until it is true
  • A multipart upload that is neither completed nor aborted leaves parts billed and invisible; set a lifecycle rule to expire them
  • Safari's drag events differ enough from Chromium's to be worth testing separately
  • A photo carries EXIF orientation that the browser applies on display and canvas processing does not

</red_flags>

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

Hugging Face Inference SDK patterns for TypeScript/Node.js — InferenceClient setup, chat completion, text generation, streaming, embeddings, image generation, audio transcription, translation, summarization, and Inference Endpoints

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

agents-inc/skills252026년 10월 17일 업데이트

LiteLLM proxy server setup, TypeScript client patterns via OpenAI SDK, model routing, fallbacks, load balancing, spend tracking, virtual keys, and production deployment

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

agents-inc/skills252026년 10월 17일 업데이트

Serverless GPU compute platform for AI model deployment — web endpoints, GPU functions, model serving, and TypeScript client patterns

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

agents-inc/skills252026년 10월 17일 업데이트

Local LLM inference with the Ollama JavaScript client -- chat, streaming, tool calling, vision, embeddings, structured output, model management, and OpenAI-compatible endpoint

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

agents-inc/skills252026년 10월 17일 업데이트

Replicate SDK patterns for TypeScript/Node.js -- client setup, predictions, streaming, webhooks, file handling, model versioning, deployments, and training

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

agents-inc/skills252026년 10월 17일 업데이트

Together AI SDK patterns for TypeScript — client setup, chat completions, streaming, structured output, function calling, embeddings, image generation, fine-tuning, and OpenAI-compatible endpoints

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

agents-inc/skills252026년 10월 17일 업데이트

agents-inc의 스킬 모두 보기

이 스킬의 문제 신고