first commit
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
# `initiate.ts`
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the request body and response shape for `POST /uploads/initiate`, the chunked upload bootstrap endpoint. The client declares the file's total size, SHA-256 hash, and optional MIME type up-front; the server returns the `uploadId`, server-chosen `chunkSize`, and `totalChunks` so the client can deterministically slice the file.
|
||||
|
||||
## Features
|
||||
|
||||
- Client-declared `totalSha256` is the contract the server later verifies on `complete` — mismatch raises `UPLOAD.HASH_MISMATCH` (HTTP 422).
|
||||
- `mimeType` defaults to `application/octet-stream` so clients uploading binary payloads need not guess.
|
||||
- `chunkSize` may be overridden by the client; absent, the server uses the configured `defaultChunkSize`.
|
||||
- 255-char `fileName` cap matches POSIX `NAME_MAX`.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { InitiateUploadBodySchema } from "@suaveplan/api-plugin-file-upload/schemas/routes";
|
||||
|
||||
const body = InitiateUploadBodySchema.parse(await c.req.json());
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import { InitiateUploadBodySchema } from "@suaveplan/api-plugin-file-upload";
|
||||
import fc from "fast-check";
|
||||
|
||||
fc.assert(
|
||||
fc.property(
|
||||
fc.record({
|
||||
fileName: fc.string({ minLength: 1, maxLength: 255 }),
|
||||
totalSize: fc.integer({ min: 1, max: 1024 * 1024 * 1024 }),
|
||||
totalSha256: fc.hexaString({ minLength: 64, maxLength: 64 }),
|
||||
}),
|
||||
(input) => InitiateUploadBodySchema.safeParse(input).success
|
||||
),
|
||||
{ numRuns: 200 }
|
||||
);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `InitiateUploadBodySchema` — Zod schema for the request body.
|
||||
- `InitiateUploadResponseSchema` — Zod schema for the success response (`uploadId`, `chunkSize`, `totalChunks`).
|
||||
- `InitiateUploadBody` / `InitiateUploadResponse` — `z.infer` aliases.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The server intentionally trusts the client-declared `totalSha256` only as a verification target — the actual hash is computed server-side from the assembled bytes. A mismatch surfaces as a 422 so clients can re-derive and retry without rebuilding the entire upload state.
|
||||
- `chunkSize` is server-bounded by `defaultChunkSize` to prevent pathological tiny-chunk attacks; the validator at the service layer caps client-supplied values.
|
||||
@@ -0,0 +1,44 @@
|
||||
# `single-shot.ts`
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the typed JSON response returned by both `POST /uploads` (single-shot) and `POST /uploads/{uploadId}/complete` (chunked finalisation). Centralising the response shape keeps the OpenAPI doc in lockstep across the two surfaces — clients written against `POST /uploads` work unchanged against the chunked `complete` route.
|
||||
|
||||
## Features
|
||||
|
||||
- 64-char SHA-256 length constraint catches obvious truncation bugs at the schema boundary.
|
||||
- Non-negative `size` so a zero-byte upload still validates cleanly.
|
||||
- `mimeType` is required because the validator always populates it (declared or sniffed).
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { SingleShotResponseSchema } from "@suaveplan/api-plugin-file-upload/schemas/routes";
|
||||
|
||||
const parsed = SingleShotResponseSchema.parse(await response.json());
|
||||
console.log(parsed.uploadId, parsed.sha256);
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
SingleShotResponseSchema,
|
||||
type SingleShotResponse,
|
||||
} from "@suaveplan/api-plugin-file-upload";
|
||||
|
||||
function describeUpload(rec: SingleShotResponse): string {
|
||||
return `Uploaded ${rec.fileName} (${rec.size} bytes, sha256=${rec.sha256.slice(0, 8)}…)`;
|
||||
}
|
||||
const ok: SingleShotResponse = SingleShotResponseSchema.parse(json);
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `SingleShotResponseSchema` — Zod object schema.
|
||||
- `SingleShotResponse` — `z.infer<typeof SingleShotResponseSchema>` type alias.
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- Reusing the same shape across two routes is a deliberate ergonomic choice: callers that switch from a small single-shot file to a large chunked upload do not have to adjust their response parsing.
|
||||
- The schema omits `storageKey` and `principalId` from the response so internal storage layout and ownership metadata never leak through the wire surface.
|
||||
@@ -0,0 +1,60 @@
|
||||
# `status.ts`
|
||||
|
||||
## Purpose
|
||||
|
||||
Defines the path-parameter and response shapes for the remaining chunked-upload routes: `PUT /uploads/{uploadId}/chunks/{chunkIndex}`, `POST /uploads/{uploadId}/complete`, `GET /uploads/{uploadId}`, and `DELETE /uploads/{uploadId}`. The status response is the resume contract: clients call `GET` after a crash and pick up from the next missing chunk.
|
||||
|
||||
## Features
|
||||
|
||||
- `ChunkParamsSchema` constrains `chunkIndex` to a non-negative integer via regex — invalid values reject at the boundary so the handler never sees malformed paths.
|
||||
- `CompleteUploadBodySchema` allows an optional `totalSha256` override for clients that recomputed the hash mid-flight.
|
||||
- `UploadStatusResponseSchema` exposes a discriminated `state` literal so consumers can switch on `"in-progress"` vs `"complete"` without ambiguity.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { UploadStatusResponseSchema } from "@suaveplan/api-plugin-file-upload/schemas/routes";
|
||||
|
||||
const status = UploadStatusResponseSchema.parse(await response.json());
|
||||
const missing: number[] = [];
|
||||
for (let i = 0; i < status.totalChunks; i++) {
|
||||
if (!status.receivedChunks.includes(i)) missing.push(i);
|
||||
}
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
```ts
|
||||
import {
|
||||
ChunkParamsSchema,
|
||||
UploadStatusResponseSchema,
|
||||
} from "@suaveplan/api-plugin-file-upload";
|
||||
|
||||
async function resume(uploadId: string, fetchFn: typeof fetch) {
|
||||
const status = UploadStatusResponseSchema.parse(
|
||||
await (await fetchFn(`/uploads/${uploadId}`)).json()
|
||||
);
|
||||
if (status.state === "complete") return;
|
||||
for (let i = 0; i < status.totalChunks; i++) {
|
||||
if (status.receivedChunks.includes(i)) continue;
|
||||
const params = ChunkParamsSchema.parse({
|
||||
uploadId,
|
||||
chunkIndex: String(i),
|
||||
});
|
||||
await uploadChunk(params, sliceFor(i));
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
- `UploadIdParamsSchema` — path parameter `{ uploadId }`.
|
||||
- `ChunkParamsSchema` — path parameter `{ uploadId, chunkIndex }`.
|
||||
- `CompleteUploadBodySchema` — optional body with `totalSha256` override; defaults to `{}` when omitted.
|
||||
- `UploadStatusResponseSchema` — `{ uploadId, totalChunks, receivedChunks, state }`.
|
||||
- `AppendChunkResponseSchema` — `{ uploadId, chunkIndex, receivedChunks }` (the count, not the set).
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- `chunkIndex` is a string in the params (URL paths are strings) and parsed to a number in the handler — the schema enforces only that the path segment is decimal-digit-only.
|
||||
- The status response returns `receivedChunks` as an array, not a set, so it serialises cleanly through JSON; clients reconstruct the set if needed.
|
||||
Reference in New Issue
Block a user