Skip to main content
zerotal

Documentation


Documentation / zerotal / http / UploadedFile

Class: UploadedFile

Defined in: packages/core/src/http/UploadedFile.ts:46

Wraps a browser File from a multipart form upload and adds Zerotal-native validation and one-line storage.

Instances are created by HttpContext.file() / HttpContext.files().

Example

async updateAvatar(ctx: HttpContext): HttpResponse {
  const avatar = await ctx.file('avatar');

  if (!avatar?.isValid({ maxSize: 2 * 1024 * 1024, mimes: ['image/jpeg', 'image/png'] })) {
    return redirect().back().withErrors({ avatar: 'Must be a JPEG or PNG under 2 MB.' });
  }

  const path = await avatar.store('avatars', Storage.disk());
  await ctx.user!.update({ avatarPath: path });
  return redirect('/profile').withSuccess('Avatar updated!');
}

Constructors

Constructor

new UploadedFile(file): UploadedFile

Defined in: packages/core/src/http/UploadedFile.ts:49

Parameters

file

File

Returns

UploadedFile

Accessors

originalName

Get Signature

get originalName(): string

Defined in: packages/core/src/http/UploadedFile.ts:54

Original filename as sent by the browser (may be untrusted — sanitise before display).

Returns

string


mimeType

Get Signature

get mimeType(): string

Defined in: packages/core/src/http/UploadedFile.ts:59

MIME type as reported by the browser (e.g. 'image/jpeg').

Returns

string


size

Get Signature

get size(): number

Defined in: packages/core/src/http/UploadedFile.ts:64

File size in bytes.

Returns

number

Methods

extension()

extension(): string

Defined in: packages/core/src/http/UploadedFile.ts:76

Lowercase extension derived from the original filename, without the dot, stripped to [a-z0-9].

The filename is client-supplied, so anything outside that set — a null byte, a slash, a %00 — is a smuggling attempt rather than an extension. This is what the client called the file; store does not trust it when naming what it writes.

Returns

string


isValid()

isValid(options?): boolean

Defined in: packages/core/src/http/UploadedFile.ts:89

Returns true when the file satisfies all given rules. Pass no options to simply confirm a file was received.

Parameters

options?

FileValidationOptions = {}

Returns

boolean

Example

avatar.isValid({ maxSize: 5 * 1024 * 1024, mimes: ['image/jpeg', 'image/png'] })

bytes()

bytes(): Promise<Uint8Array<ArrayBufferLike>>

Defined in: packages/core/src/http/UploadedFile.ts:99

Read the file as raw bytes.

Returns

Promise<Uint8Array<ArrayBufferLike>>


text()

text(): Promise<string>

Defined in: packages/core/src/http/UploadedFile.ts:104

Read the file as a UTF-8 string.

Returns

Promise<string>


store()

store(directory, disk, filename?): Promise<string>

Defined in: packages/core/src/http/UploadedFile.ts:134

Write the file to a disk and return the stored path.

The filename defaults to <uuid>.<ext> — predictable, safe, and collision-free. Pass filename to override it.

Both the extension and the stored Content-Type come from the file's own bytes, not from the client. The multipart part's Content-Type and the filename's suffix are claims the uploader controls: avatar sent as x.html with text/html would otherwise be written as avatars/<uuid>.html and served as HTML, which is stored XSS on whatever origin serves the disk. Bytes that match no known format are stored as application/octet-stream with a .bin extension, which downloads rather than executes. See sniffContentType.

An explicit filename is taken at face value — you chose it, so it is yours to get right — but the sniffed content type still applies.

Parameters

directory

string

Target directory, e.g. 'avatars' or 'uploads/docs'

disk

StorageDisk

Any StorageDisk — typically Storage.disk() or Storage.disk('s3')

filename?

string

Optional override; defaults to <uuid>.<sniffed-ext>

Returns

Promise<string>

The stored path, e.g. 'avatars/f47ac10b.jpg'

Example

const path = await avatar.store('avatars', Storage.disk());
const path = await avatar.store('docs', Storage.disk('s3'), 'terms-v2.pdf');

detectType()

detectType(): Promise<SniffedType>

Defined in: packages/core/src/http/UploadedFile.ts:156

What the file's own bytes say it is, ignoring both client-supplied claims.

Use it to reject an upload whose contents disagree with its declared type — a .jpg whose bytes are a ZIP, say — before storing it.

Returns

Promise<SniffedType>

The detected content type, canonical extension, and whether detection succeeded.

Example

const { contentType, recognised } = await avatar.detectType();
if (!recognised || !contentType.startsWith('image/')) return badRequest('Not an image.');

storeAndGetUrl()

storeAndGetUrl(directory, disk, filename?): Promise<string>

Defined in: packages/core/src/http/UploadedFile.ts:167

Store the file and immediately return its public URL.

Parameters

directory

string

disk

StorageDisk

filename?

string

Returns

Promise<string>

Example

const url = await avatar.storeAndGetUrl('avatars', Storage.disk('s3'));
await user.update({ avatarUrl: url });

fake()

static fake(name?, options?): UploadedFile

Defined in: packages/core/src/http/UploadedFile.ts:191

Build an UploadedFile from scratch, for unit-testing the code that receives one without going through a multipart request.

The contents are arbitrary bytes, so the type this reports is the type you declare — which is the point for a size or extension check, but means detectType and store will see unrecognised bytes and fall back to application/octet-stream. When the test turns on what the bytes actually are, build a real one with fakeFile from @zerotal/testing and pass it here.

Parameters

name?

string = "file.txt"

Filename the "client" sent.

options?
type?

string

MIME type to report from mimeType.

size?

number

Size in bytes; the file is padded to it.

content?

string | Uint8Array<ArrayBuffer> | File

Exact contents, overriding size.

Returns

UploadedFile

Example

const file = UploadedFile.fake('avatar.png', { type: 'image/png', size: 1024 });
expect(file.isValid({ maxSize: 2048, mimes: ['image/png'] })).toBe(true);