uploads
Every export of @kitr/uploads, @kitr/uploads/react, @kitr/uploads/server, @kitr/uploads/provider, read from the published type definitions. The guide shows them in use.
@kitr/uploads
accepts
function accepts(type: string, name: string, accept?: string[]): boolean;
image/* matches image/png; exact types match themselves; .pdf matches by extension.
checkFile
function checkFile(file: File, rules?: UploadRules): string | null;
Client-side check against the same rules the server enforces, so errors show before the request.
formatBytes
function formatBytes(bytes: number): string;
Formats a byte count for people: formatBytes(10 * 1024 * 1024) is '10.0 MB'.
upload
function upload(files: File | File[], options?: UploadOptions): Promise<UploadedFile[]>;
Upload one or more files with progress. XMLHttpRequest is the only browser API that reports upload progress, so this is XHR with a promise around it; a fetch fallback runs elsewhere.
UploadError
class UploadError extends Error {
readonly status: number;
readonly body?: unknown | undefined;
constructor(message: string, status: number, body?: unknown | undefined);
}
What upload() rejects with: the server's message and HTTP status (0 for network errors and
cancels), and the parsed response body, which carries fieldErrors for rule violations.
defineConfig
const defineConfig: <const C extends UploadsConfig>(config: C) => C;
Typed identity helper for config/uploads.ts.
ServedFile
interface ServedFile {
/** The storage key: `<id>/<name>`. */
key: string;
/** The file name, from the key. */
name: string;
/** The type the file will be served with (sniffed from its bytes, or allow-listed). */
type: string;
/** Size in bytes. */
size: number;
}
The stored file authorize is asked about.
UploadAccess
interface UploadAccess {
/** The h3 event of the request. */
event: H3Event;
/** The request. */
request: Request;
/** The app for this request (a fork per request on a kitr server): `app.container.use('auth')` for the session. */
app: KitrApp;
}
Who is asking, as authorize sees it.
UploadedFile
interface UploadedFile {
/** The random id the file was stored under: the first segment of `key`. */
id: string;
/** The original file name, as the browser sent it. */
name: string;
/** Size in bytes. */
size: number;
/** The MIME type the browser declared. Not sniffed from the bytes. */
type: string;
/** Where it can be fetched, when the driver serves files. */
url?: string;
/** The storage key the driver wrote. */
key: string;
}
What the server returns for every stored file, and what the client resolves with.
UploadOptions
interface UploadOptions {
/** The endpoint, default `/api/uploads`. */
url?: string;
/** Form field name, default `file`. */
field?: string;
/** Extra fields sent with the file. */
fields?: Record<string, string>;
/** Extra request headers, such as a CSRF token. */
headers?: Record<string, string>;
/** Called as bytes go out, with `percent` from 0 to 100. Only the XHR path (browsers) reports progress. */
onProgress?: (progress: {
loaded: number;
total: number;
percent: number;
}) => void;
/** Aborts the upload; the promise rejects with `UploadError('Upload cancelled', 0)`. */
signal?: AbortSignal;
/** Sent with credentials (cookies). Default true. */
credentials?: boolean;
}
Options for upload() and useUpload(): where to send, extra form fields and headers, progress and cancel.
UploadRules
interface UploadRules {
/** Bytes. Default 10 MiB. */
maxSize?: number;
/** Allowed MIME types or patterns such as `image/*`. Default: anything. */
accept?: string[];
/** Files per request. Default 1. */
maxFiles?: number;
}
The size, type and count limits checked in the browser (checkFile, useUpload) and on the server (receive).
UploadsConfig
interface UploadsConfig extends UploadRules {
/** Where files go. Default: `storage/uploads` on disk (`createDiskUploadsDriver`). */
driver?: unknown;
/** Public URL prefix the driver answers under, e.g. `/uploads`. */
publicPath?: string;
/**
* Declared types served as themselves when the file's bytes carry no signature to check (text
* formats have none), always as a download except `text/plain`. Patterns such as `text/*` work.
* Files with a recognised signature (images, PDF, audio, video, archives) are served with the
* type their bytes show, whatever was declared; anything else is `application/octet-stream`.
* Default `['text/plain', 'text/csv', 'application/json']`.
*/
allowTypes?: string[];
/**
* Secret for signed links (`uploads.signedUrl(key, { expiresIn })`), at least 16 characters.
* Set it from the environment, the same on every instance.
*/
secret?: string;
/**
* Decides who may fetch a file without a signed link. Return false (or throw) to answer 403.
* Runs for every unsigned `GET`; a valid signed link skips it. Without it, and without
* `requireSigned`, files are public to anyone who knows the key.
*/
authorize?: (access: UploadAccess, file: ServedFile) => boolean | Promise<boolean>;
/** Serve files only through signed links: a request without a valid signature is refused. Needs `secret`. Default false. */
requireSigned?: boolean;
}
The shape of config/uploads.ts: the default rules plus where files are stored and served.
@kitr/uploads/react
useUpload
function useUpload(options?: UploadOptions & UploadRules): {
send: (input: FileList | File[] | File | null) => Promise<UploadedFile[]>;
cancel: () => void | undefined;
reset: () => void;
files: UploadedFile[];
progress: number;
uploading: boolean;
error: Error | null;
};
Upload with progress from a component:
const { send, progress, uploading, error } = useUpload({ accept: ['image/*'] })
<input type="file" onChange={(e) => send(e.target.files)} />
send checks every file against the rules first, then uploads; it resolves with the stored files and rethrows failures.
UseUploadState
interface UseUploadState {
/** The stored files from the last successful upload. */
files: UploadedFile[];
/** Percent sent, 0 to 100. */
progress: number;
/** True while a request is in flight. */
uploading: boolean;
/** The last failure: a plain `Error` from the client-side check, or an `UploadError` from the request. */
error: Error | null;
}
The state useUpload() returns next to send, cancel and reset.
@kitr/uploads/server
createDiskUploadsDriver
function createDiskUploadsDriver(options?: {
dir?: string;
publicPath?: string;
}): UploadsDriver;
Files under a directory (default storage/uploads), loaded lazily so the browser bundle never sees node:fs.
createMemoryUploadsDriver
function createMemoryUploadsDriver(): UploadsDriver;
A driver that keeps files in a Map, for tests and demos. Everything is lost when the process exits.
createUnstorageUploadsDriver
function createUnstorageUploadsDriver(storage: UnstorageRawLike, options?: {
base?: string;
publicPath?: string;
}): UploadsDriver;
An uploads driver over any unstorage instance, so every server instance reads and writes the same files: object storage such as S3 or R2 instead of one machine's disk.
driver: createUnstorageUploadsDriver(createStorage({ driver: s3Driver({ bucket: 'acme-uploads', ... }) }))
Files live under base (default uploads) with their type and name kept beside them under
<base>:meta. publicPath gives the driver a url(key), as with the disk driver.
createUploads
function createUploads(config?: UploadsConfig): Uploads;
Creates the uploads service from a config: the driver (disk under storage/uploads by default)
and the default rules. The provider calls it with config/uploads.ts; call it yourself in tests.
mountUploads
function mountUploads<S extends {
h3: any;
app: KitrApp;
config?: {
bodyLimits?: Record<string, number>;
};
}>(server: S, path?: string): S;
Mount POST <path> (receive) and GET <path>/** (serve) on a kitr server from server/main.ts.
The service is resolved per request, so this can run before the app has booted.
resolveServedType
function resolveServedType(declared: string, data: Uint8Array, allow?: readonly string[]): {
type: string;
inline: boolean;
};
The type a stored file is served as, and whether a browser may show it inline: the type its bytes
show (so a declared image/png over HTML is not an image); failing that, the declared type if
allow-listed; otherwise application/octet-stream.
servedFileHeaders
function servedFileHeaders(type: string, name?: string, options?: ServedFileOptions): Record<string, string>;
Headers for serving a stored file: never sniffed by the browser (nosniff), sandboxed, and
inline only when that is safe; everything else is a download (Content-Disposition: attachment).
Pass the bytes in options.data so the type comes from them, not from the uploader.
signedUrl
function signedUrl(path: string, options: SignedUrlOptions): Promise<string>;
A link to path that works until it expires: path?expires=<unix seconds>&signature=<HMAC>.
The signature is HMAC-SHA-256 (Web Crypto) over the path and the expiry, so the link works for
that file only and only until then. Anyone holding the link can fetch the file, so keep
expiresIn short and send it only to the person it is for.
signedUrl('/api/uploads/3f2a/report.pdf', { expiresIn: 300, secret })
sniffType
function sniffType(data: Uint8Array): string | undefined;
The content type of a file from its first bytes, for formats that have a signature; undefined for everything else (text formats, SVG, HTML and unknown binaries have none).
Uploads are served with the sniffed type, never the one the uploader declared: a browser trusts
whatever the server says, so a declared image/png over HTML bytes would otherwise run as the site.
useUploads
function useUploads(app: KitrApp): Uploads;
Returns the uploads service the provider bound, for a controller or job: useUploads(ctx.app).
verifySignedUrl
function verifySignedUrl(url: URL, secret: string, now?: number): Promise<SignatureCheck>;
Check the expires and signature query parameters of a request URL against secret.
ServedFileOptions
interface ServedFileOptions {
/**
* The file's bytes. With them the type is sniffed from the signature (PNG, JPEG, PDF, ZIP...)
* and the declared type is only a fallback for allow-listed text formats; without them the
* declared type is taken as given.
*/
data?: Uint8Array;
/** Declared types that may be served as themselves when the bytes have no signature. Default `text/plain`, `text/csv`, `application/json`. */
allow?: string[];
/** Override `cache-control`. Default: public and immutable. */
cacheControl?: string;
}
Options of servedFileHeaders.
SignedUrlOptions
interface SignedUrlOptions {
/** Seconds the link works for. */
expiresIn: number;
/** The signing secret: at least 16 characters, the same on every instance that serves the file. */
secret: string;
/** The current time in ms; for tests. */
now?: number;
}
Options for signedUrl().
StoredFile
interface StoredFile {
key: string;
name: string;
size: number;
type: string;
}
A stored file's key, original name, size and type.
UnstorageRawLike
interface UnstorageRawLike {
getItemRaw(key: string): Promise<unknown>;
setItemRaw(key: string, value: Uint8Array): Promise<void>;
getItem(key: string): Promise<unknown>;
setItem(key: string, value: string): Promise<void>;
removeItem(key: string): Promise<void>;
}
The part of an unstorage Storage the uploads driver uses. Any
unstorage instance fits, whatever its driver: S3 and R2 (unstorage/drivers/s3), Azure Blob,
Vercel Blob, Netlify Blobs, the filesystem.
Uploads
interface Uploads {
/** Read a multipart request, apply the rules and store the files. Throws KitrError on violation. */
receive(event: H3Event, rules?: UploadRules & {
field?: string;
}): Promise<UploadedFile[]>;
/** Store bytes directly (from a job, a migration). */
store(file: {
name: string;
type: string;
data: Uint8Array;
}): Promise<UploadedFile>;
/** Read a stored file's bytes and type, or `undefined`. */
read(key: string): Promise<{
data: Uint8Array;
type: string;
} | undefined>;
/** Delete a stored file through the driver. */
remove(key: string): Promise<void>;
/**
* A link to a stored file that works for `expiresIn` seconds and no longer: the file's URL
* under the mount path (default `/api/uploads`) with an HMAC signature. Needs `secret` in
* `config/uploads.ts`. The link skips `authorize`, so hand it only to the person it is for.
*/
signedUrl(key: string, options: {
expiresIn: number;
path?: string;
}): Promise<string>;
/** Answer a `GET` for a stored file under `base`: checks the signed link or `authorize`, then sends the file with safe headers. What the mounted route runs. */
serve(event: H3Event, options?: {
base?: string;
app?: KitrApp;
}): Promise<Response>;
/** Mount `POST <path>` (receive) and `GET <path>/:key` (serve) on a kitr server. */
mount<S extends {
h3: any;
app?: KitrApp;
}>(server: S, path?: string): S;
/** The config this service was made from. */
readonly config: UploadsConfig;
/** The driver files are stored with. */
readonly driver: UploadsDriver;
}
The uploads service, as createUploads returns it and useUploads(app) reads it from the container.
UploadsDriver
interface UploadsDriver {
/** A short label, such as `disk` or `memory`. */
name: string;
/** Write the bytes under `key`, keeping `type` so `get` can return it. */
put(key: string, data: Uint8Array, meta: {
type: string;
name: string;
}): Promise<void>;
/** Read a file back, or `undefined` when the key does not exist. */
get(key: string): Promise<{
data: Uint8Array;
type: string;
} | undefined>;
/** Delete a file. Deleting a missing key is not an error. */
delete(key: string): Promise<void>;
/** A URL for the key, when files are served (CDN, signed URL, the kitr route). */
url?(key: string): string | Promise<string>;
}
Where bytes go: disk, S3, R2, GCS. Keys are relative paths the driver owns.
SignatureCheck
type SignatureCheck = 'ok' | 'unsigned' | 'expired' | 'invalid';
Why a signed link was refused, or ok; unsigned when the request carries no signature at all.
@kitr/uploads/provider
uploadsProvider
const uploadsProvider: import("@kitr/core").KitrProvider;
Binds uploads on the server from config/uploads.ts. The server module is loaded at boot so
the browser bundle of this provider never resolves node:fs.