| /** | |
| * Parses and persists HTTP `multipart/form-data` request bodies. | |
| * | |
| * `Multipart` turns incoming byte streams into typed form parts. Text parts | |
| * become decoded fields, while upload parts stay as streamed files until they | |
| * are collected or written to scoped temporary files. The persisted | |
| * representation can then be decoded with schemas for handlers that receive | |
| * fields and uploaded files together. This module also includes multipart error | |
| * types, schema helpers for persisted files, and parser limit settings. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| import * as Arr from "../../Array.ts" | |
| import * as Cause from "../../Cause.ts" | |
| import * as Channel from "../../Channel.ts" | |
| import * as Context from "../../Context.ts" | |
| import * as Data from "../../Data.ts" | |
| import * as Effect from "../../Effect.ts" | |
| import * as Exit from "../../Exit.ts" | |
| import * as FileSystem from "../../FileSystem.ts" | |
| import { constant, dual } from "../../Function.ts" | |
| import * as Inspectable from "../../Inspectable.ts" | |
| import * as Option from "../../Option.ts" | |
| import * as Path from "../../Path.ts" | |
| import * as Predicate from "../../Predicate.ts" | |
| import * as Pull from "../../Pull.ts" | |
| import * as Schema from "../../Schema.ts" | |
| import type { ParseOptions } from "../../SchemaAST.ts" | |
| import * as SchemaTransformation from "../../SchemaTransformation.ts" | |
| import type * as Scope from "../../Scope.ts" | |
| import * as Stream from "../../Stream.ts" | |
| import * as UndefinedOr from "../../UndefinedOr.ts" | |
| import * as IncomingMessage from "./HttpIncomingMessage.ts" | |
| import * as MP from "./Multipasta.ts" | |
| /** | |
| * Type identifier used to brand multipart part values. | |
| * | |
| * @category type IDs | |
| * @since 4.0.0 | |
| */ | |
| export const TypeId = "~effect/http/Multipart" | |
| /** | |
| * A parsed multipart part. | |
| * | |
| * **Details** | |
| * | |
| * A part is either a text `Field` or a streamed `File`. | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export type Part = Field | File | |
| /** | |
| * Namespace containing shared multipart part model types. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| export declare namespace Part { | |
| /** | |
| * Common protocol implemented by multipart part values. | |
| * | |
| * **Details** | |
| * | |
| * It provides the multipart type identifier, tag, and inspectable behavior shared | |
| * by fields, files, and persisted files. | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface Proto extends Inspectable.Inspectable { | |
| readonly [TypeId]: typeof TypeId | |
| readonly _tag: string | |
| } | |
| } | |
| /** | |
| * Multipart form field containing a decoded text value. | |
| * | |
| * **Details** | |
| * | |
| * The `key` is the field name, `contentType` is the part media type, and `value` | |
| * is the decoded field content. | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface Field extends Part.Proto { | |
| readonly _tag: "Field" | |
| readonly key: string | |
| readonly contentType: string | |
| readonly value: string | |
| } | |
| /** | |
| * Returns `true` when a value is a multipart `Part`. | |
| * | |
| * @category guards | |
| * @since 4.0.0 | |
| */ | |
| export const isPart = (u: unknown): u is Part => Predicate.hasProperty(u, TypeId) | |
| /** | |
| * Returns `true` when a value is a multipart text `Field`. | |
| * | |
| * @category guards | |
| * @since 4.0.0 | |
| */ | |
| export const isField = (u: unknown): u is Field => isPart(u) && u._tag === "Field" | |
| /** | |
| * Multipart file part. | |
| * | |
| * **Gotchas** | |
| * | |
| * The file content is exposed as a byte stream. `contentEffect` collects the full | |
| * file into memory and should be used only when the file size is acceptable. | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface File extends Part.Proto { | |
| readonly _tag: "File" | |
| readonly key: string | |
| readonly name: string | |
| readonly contentType: string | |
| readonly content: Stream.Stream<Uint8Array, MultipartError> | |
| readonly contentEffect: Effect.Effect<Uint8Array, MultipartError> | |
| } | |
| /** | |
| * Returns `true` when a value is a multipart `File`. | |
| * | |
| * @category guards | |
| * @since 4.0.0 | |
| */ | |
| export const isFile = (u: unknown): u is File => isPart(u) && u._tag === "File" | |
| /** | |
| * Multipart file part that has been written to the filesystem. | |
| * | |
| * **Details** | |
| * | |
| * The `path` points to the persisted file while the scope used to persist the | |
| * multipart data remains open. | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface PersistedFile extends Part.Proto { | |
| readonly _tag: "PersistedFile" | |
| readonly key: string | |
| readonly name: string | |
| readonly contentType: string | |
| readonly path: string | |
| } | |
| /** | |
| * Returns `true` when a value is a persisted multipart file. | |
| * | |
| * @category guards | |
| * @since 4.0.0 | |
| */ | |
| export const isPersistedFile = (u: unknown): u is PersistedFile => | |
| Predicate.hasProperty(u, TypeId) && Predicate.isTagged(u, "PersistedFile") | |
| /** | |
| * Record representation of persisted multipart data. | |
| * | |
| * **Details** | |
| * | |
| * Field names map to text values, arrays of text values, or arrays of | |
| * `PersistedFile` values. | |
| * | |
| * @category models | |
| * @since 4.0.0 | |
| */ | |
| export interface Persisted { | |
| readonly [key: string]: ReadonlyArray<PersistedFile> | ReadonlyArray<string> | string | |
| } | |
| const MultipartErrorTypeId = "~effect/http/Multipart/MultipartError" | |
| /** | |
| * Error reason carried by a `MultipartError`. | |
| * | |
| * **Details** | |
| * | |
| * It identifies parser and limit failures such as oversized files or fields, too | |
| * many parts, total body size limits, parse errors, and internal errors. | |
| * | |
| * @category errors | |
| * @since 4.0.0 | |
| */ | |
| export class MultipartErrorReason extends Data.Error<{ | |
| readonly _tag: "FileTooLarge" | "FieldTooLarge" | "BodyTooLarge" | "TooManyParts" | "InternalError" | "Parse" | |
| readonly cause?: unknown | |
| }> {} | |
| /** | |
| * Error raised while parsing, streaming, or persisting multipart form data. | |
| * | |
| * **Details** | |
| * | |
| * The `reason` field contains the concrete `MultipartErrorReason`. | |
| * | |
| * @category errors | |
| * @since 4.0.0 | |
| */ | |
| export class MultipartError extends Data.TaggedError("MultipartError")<{ | |
| readonly reason: MultipartErrorReason | |
| }> { | |
| /** | |
| * Creates a multipart error from a reason tag and optional cause. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| static fromReason(reason: MultipartErrorReason["_tag"], cause?: unknown): MultipartError { | |
| return new MultipartError({ reason: new MultipartErrorReason({ _tag: reason, cause }) }) | |
| } | |
| /** | |
| * Marks this value as a multipart error for runtime guards. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| readonly [MultipartErrorTypeId] = MultipartErrorTypeId | |
| /** | |
| * Uses the concrete multipart error reason as the public message. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| override get message(): string { | |
| return this.reason._tag | |
| } | |
| } | |
| /** | |
| * Schema type for persisted multipart files. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export interface PersistedFileSchema extends Schema.declare<PersistedFile> {} | |
| /** | |
| * Schema for persisted multipart files. | |
| * | |
| * **Details** | |
| * | |
| * The encoded form contains the field key, original file name, content type, and | |
| * filesystem path. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const PersistedFileSchema: PersistedFileSchema = Schema.declare( | |
| isPersistedFile, | |
| { | |
| typeConstructor: { | |
| _tag: "effect/http/PersistedFile" | |
| }, | |
| generation: { | |
| runtime: `Multipart.PersistedFileSchema`, | |
| Type: `Multipart.PersistedFile`, | |
| importDeclaration: `import * as Multipart from "effect/unstable/http/Multipart"` | |
| }, | |
| expected: "PersistedFile", | |
| toCodecJson: () => | |
| Schema.link<PersistedFile>()( | |
| Schema.Struct({ | |
| key: Schema.String, | |
| name: Schema.String, | |
| contentType: Schema.String.annotate({ contentEncoding: "binary" }), | |
| path: Schema.String | |
| }), | |
| SchemaTransformation.transform({ | |
| decode: ({ contentType, key, name, path }) => new PersistedFileImpl(key, name, contentType, path), | |
| encode: (file) => ({ | |
| key: file.key, | |
| name: file.name, | |
| contentType: file.contentType, | |
| path: file.path | |
| }) | |
| }) | |
| ) | |
| } | |
| ) | |
| /** | |
| * Schema for an array of persisted multipart files. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const FilesSchema: Schema.$Array<PersistedFileSchema> = Schema.Array(PersistedFileSchema) | |
| /** | |
| * Schema for exactly one persisted multipart file. | |
| * | |
| * **Details** | |
| * | |
| * The encoded form is a one-element file array, while the decoded value is the | |
| * single `PersistedFile`. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const SingleFileSchema: Schema.decodeTo<PersistedFileSchema, Schema.$Array<PersistedFileSchema>> = FilesSchema | |
| .check( | |
| Schema.isLengthBetween(1, 1) | |
| ).pipe( | |
| Schema.decodeTo( | |
| PersistedFileSchema, | |
| SchemaTransformation.transform({ | |
| decode: ([file]) => file, | |
| encode: (file) => [file] | |
| }) | |
| ) | |
| ) | |
| /** | |
| * Creates a decoder for persisted multipart data using the supplied schema. | |
| * | |
| * **Details** | |
| * | |
| * The returned function decodes an unknown input into the schema output and fails | |
| * with `SchemaError` when validation fails. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const schemaPersisted = <A, I extends Partial<Persisted>, RD, RE>( | |
| schema: Schema.Codec<A, I, RD, RE> | |
| ): (input: unknown, options?: ParseOptions) => Effect.Effect<A, Schema.SchemaError, RD> => | |
| Schema.decodeUnknownEffect(schema) | |
| /** | |
| * Creates a decoder for a JSON-encoded field in persisted multipart data. | |
| * | |
| * **Details** | |
| * | |
| * The selected field is parsed from a JSON string and decoded with the supplied | |
| * schema. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export const schemaJson = <A, I, RD, RE>(schema: Schema.Codec<A, I, RD, RE>, options?: ParseOptions | undefined): { | |
| ( | |
| field: string | |
| ): (persisted: Persisted) => Effect.Effect<A, Schema.SchemaError, RD> | |
| ( | |
| persisted: Persisted, | |
| field: string | |
| ): Effect.Effect<A, Schema.SchemaError, RD> | |
| } => { | |
| const fromJson = Schema.fromJsonString(schema) | |
| return dual(2, (persisted: Persisted, field: string): Effect.Effect<A, Schema.SchemaError, RD> => | |
| Effect.map( | |
| Schema.decodeUnknownEffect(Schema.Struct({ [field]: fromJson }))(persisted, options), | |
| (_) => _[field] | |
| )) | |
| } | |
| /** | |
| * Builds the low-level multipart parser configuration from request headers and | |
| * the current fiber context. | |
| * | |
| * **Details** | |
| * | |
| * Parser limits are read from the multipart references, including maximum parts, | |
| * field size, file size, total body size, and field MIME type overrides. | |
| * | |
| * @category configuration | |
| * @since 4.0.0 | |
| */ | |
| export const makeConfig = ( | |
| headers: Record<string, string> | |
| ): Effect.Effect<MP.BaseConfig> => | |
| Effect.withFiber((fiber) => { | |
| const mimeTypes = Context.get(fiber.context, FieldMimeTypes) | |
| return Effect.succeed<MP.BaseConfig>({ | |
| headers, | |
| maxParts: fiber.getRef(MaxParts), | |
| maxFieldSize: Number(fiber.getRef(MaxFieldSize)), | |
| maxPartSize: UndefinedOr.map(fiber.getRef(MaxFileSize), Number), | |
| maxTotalSize: UndefinedOr.map(fiber.getRef(IncomingMessage.MaxBodySize), Number), | |
| isFile: mimeTypes.length === 0 ? undefined : (info: MP.PartInfo): boolean => | |
| !mimeTypes.some( | |
| (_) => info.contentType.includes(_) | |
| ) && MP.defaultIsFile(info) | |
| }) | |
| }) | |
| /** | |
| * Creates a channel that parses multipart byte chunks into multipart parts. | |
| * | |
| * **Details** | |
| * | |
| * The channel consumes non-empty batches of `Uint8Array` chunks and emits | |
| * non-empty batches of parsed `Part` values, failing with `MultipartError` for | |
| * parser and limit failures. | |
| * | |
| * @category Parsers | |
| * @since 4.0.0 | |
| */ | |
| export const makeChannel = <IE>(headers: Record<string, string>): Channel.Channel< | |
| Arr.NonEmptyReadonlyArray<Part>, | |
| MultipartError | IE, | |
| void, | |
| Arr.NonEmptyReadonlyArray<Uint8Array>, | |
| IE, | |
| unknown | |
| > => | |
| Channel.fromTransform((upstream) => | |
| Effect.map(makeConfig(headers), (config) => { | |
| let partsBuffer: Array<Part> = [] | |
| let exit = Option.none<Exit.Exit<never, IE | MultipartError | Cause.Done>>() | |
| const parser = MP.make({ | |
| ...config, | |
| onField(info, value) { | |
| partsBuffer.push(new FieldImpl(info.name, info.contentType, MP.decodeField(info, value))) | |
| }, | |
| onFile(info) { | |
| let chunks: Array<Uint8Array> = [] | |
| let finished = false | |
| const pullChunks = Channel.fromPull( | |
| Effect.succeed(Effect.suspend(function loop(): Pull.Pull<Arr.NonEmptyReadonlyArray<Uint8Array>> { | |
| if (!Arr.isReadonlyArrayNonEmpty(chunks)) { | |
| return finished ? Cause.done() : Effect.flatMap(pump, loop) | |
| } | |
| const chunk = chunks | |
| chunks = [] | |
| return Effect.succeed(chunk) | |
| })) | |
| ) | |
| partsBuffer.push(new FileImpl(info, pullChunks)) | |
| return function(chunk) { | |
| if (chunk === null) { | |
| finished = true | |
| } else { | |
| chunks.push(chunk) | |
| } | |
| } | |
| }, | |
| onError(error_) { | |
| exit = Option.some(Exit.fail(convertError(error_))) | |
| }, | |
| onDone() { | |
| exit = Option.some(Exit.fail(Cause.Done())) | |
| } | |
| }) | |
| const pump = upstream.pipe( | |
| Effect.flatMap((chunk) => { | |
| for (let i = 0; i < chunk.length; i++) { | |
| parser.write(chunk[i]) | |
| } | |
| return Effect.void | |
| }), | |
| Effect.catchCause((cause) => { | |
| if (Pull.isDoneCause(cause)) { | |
| parser.end() | |
| } else { | |
| exit = Option.some(Exit.failCause(cause)) as any | |
| } | |
| return Effect.void | |
| }) | |
| ) | |
| return pump.pipe( | |
| Effect.flatMap(function loop(): Pull.Pull<Arr.NonEmptyReadonlyArray<Part>, IE | MultipartError> { | |
| if (!Arr.isReadonlyArrayNonEmpty(partsBuffer)) { | |
| if (Option.isSome(exit)) { | |
| return exit.value | |
| } | |
| return Effect.flatMap(pump, loop) | |
| } | |
| const parts = partsBuffer | |
| partsBuffer = [] | |
| return Effect.succeed(parts) | |
| }) | |
| ) | |
| }) | |
| ) | |
| function convertError(cause: MP.MultipartError): MultipartError { | |
| switch (cause._tag) { | |
| case "ReachedLimit": { | |
| switch (cause.limit) { | |
| case "MaxParts": { | |
| return MultipartError.fromReason("TooManyParts", cause) | |
| } | |
| case "MaxFieldSize": { | |
| return MultipartError.fromReason("FieldTooLarge", cause) | |
| } | |
| case "MaxPartSize": { | |
| return MultipartError.fromReason("FileTooLarge", cause) | |
| } | |
| case "MaxTotalSize": { | |
| return MultipartError.fromReason("BodyTooLarge", cause) | |
| } | |
| } | |
| } | |
| default: { | |
| return MultipartError.fromReason("Parse", cause) | |
| } | |
| } | |
| } | |
| abstract class PartBase extends Inspectable.Class { | |
| readonly [TypeId]: typeof TypeId | |
| constructor() { | |
| super() | |
| this[TypeId] = TypeId | |
| } | |
| } | |
| class FieldImpl extends PartBase implements Field { | |
| readonly _tag = "Field" | |
| readonly key: string | |
| readonly contentType: string | |
| readonly value: string | |
| constructor( | |
| key: string, | |
| contentType: string, | |
| value: string | |
| ) { | |
| super() | |
| this.key = key | |
| this.contentType = contentType | |
| this.value = value | |
| } | |
| toJSON(): unknown { | |
| return { | |
| _id: "@effect/platform/Multipart/Part", | |
| _tag: "Field", | |
| key: this.key, | |
| contentType: this.contentType, | |
| value: this.value | |
| } | |
| } | |
| } | |
| class FileImpl extends PartBase implements File { | |
| readonly _tag = "File" | |
| readonly key: string | |
| readonly name: string | |
| readonly contentType: string | |
| readonly content: Stream.Stream<Uint8Array, MultipartError> | |
| readonly contentEffect: Effect.Effect<Uint8Array, MultipartError> | |
| constructor( | |
| info: MP.PartInfo, | |
| channel: Channel.Channel<Arr.NonEmptyReadonlyArray<Uint8Array>> | |
| ) { | |
| super() | |
| this.key = info.name | |
| this.name = info.filename ?? info.name | |
| this.contentType = info.contentType | |
| this.content = Stream.fromChannel(channel) | |
| this.contentEffect = channel.pipe( | |
| collectUint8Array, | |
| Effect.mapError((cause) => MultipartError.fromReason("InternalError", cause)) | |
| ) | |
| } | |
| toJSON(): unknown { | |
| return { | |
| _id: "@effect/platform/Multipart/Part", | |
| _tag: "File", | |
| key: this.key, | |
| name: this.name, | |
| contentType: this.contentType | |
| } | |
| } | |
| } | |
| const defaultWriteFile = (path: string, file: File) => | |
| Effect.flatMap( | |
| FileSystem.FileSystem, | |
| (fs) => | |
| Effect.mapError( | |
| Stream.run(file.content, fs.sink(path)), | |
| (cause) => MultipartError.fromReason("InternalError", cause) | |
| ) | |
| ) | |
| /** | |
| * Runs a channel of byte chunks and collects all output into a single | |
| * `Uint8Array`. | |
| * | |
| * **Gotchas** | |
| * | |
| * This materializes the full content in memory. | |
| * | |
| * @category converting | |
| * @since 4.0.0 | |
| */ | |
| export const collectUint8Array = <OE, OD, R>( | |
| self: Channel.Channel<Arr.NonEmptyReadonlyArray<Uint8Array>, OE, OD, unknown, unknown, unknown, R> | |
| ): Effect.Effect<Uint8Array<ArrayBuffer>, OE, R> => | |
| Channel.runFold(self, constant(new Uint8Array(0)), (accumulator, chunk) => { | |
| const totalLength = chunk.reduce((sum, element) => sum + element.length, accumulator.length) | |
| const newAccumulator = new Uint8Array(totalLength) | |
| newAccumulator.set(accumulator, 0) | |
| let offset = accumulator.length | |
| for (const element of chunk) { | |
| newAccumulator.set(element, offset) | |
| offset += element.length | |
| } | |
| return newAccumulator | |
| }) | |
| /** | |
| * Persists a stream of multipart parts into a record. | |
| * | |
| * **Details** | |
| * | |
| * Text fields are collected as strings, and file parts are written to files in a | |
| * scoped temporary directory. | |
| * | |
| * **Gotchas** | |
| * | |
| * Persisted file paths remain valid for the lifetime of the scope. | |
| * | |
| * @category converting | |
| * @since 4.0.0 | |
| */ | |
| export const toPersisted = ( | |
| stream: Stream.Stream<Part, MultipartError>, | |
| writeFile = defaultWriteFile | |
| ): Effect.Effect<Persisted, MultipartError, FileSystem.FileSystem | Path.Path | Scope.Scope> => | |
| Effect.gen(function*() { | |
| const fs = yield* FileSystem.FileSystem | |
| const path_ = yield* Path.Path | |
| const dir = yield* fs.makeTempDirectoryScoped() | |
| const persisted: Record<string, Array<PersistedFile> | Array<string> | string> = Object.create(null) | |
| yield* Stream.runForEach(stream, (part) => { | |
| if (part._tag === "Field") { | |
| if (!(part.key in persisted)) { | |
| persisted[part.key] = part.value | |
| } else if (typeof persisted[part.key] === "string") { | |
| persisted[part.key] = [persisted[part.key] as string, part.value] | |
| } else { | |
| ;(persisted[part.key] as Array<string>).push(part.value) | |
| } | |
| return Effect.void | |
| } else if (part.name === "") { | |
| return Effect.void | |
| } | |
| const file = part | |
| const path = path_.join(dir, path_.basename(file.name).slice(-128)) | |
| const filePart = new PersistedFileImpl( | |
| file.key, | |
| file.name, | |
| file.contentType, | |
| path | |
| ) | |
| if (Array.isArray(persisted[part.key])) { | |
| ;(persisted[part.key] as Array<PersistedFile>).push(filePart) | |
| } else { | |
| persisted[part.key] = [filePart] | |
| } | |
| return writeFile(path, file) | |
| }) | |
| return persisted | |
| }).pipe( | |
| Effect.catchTag("PlatformError", (cause) => Effect.fail(MultipartError.fromReason("InternalError", cause))) | |
| ) | |
| class PersistedFileImpl extends PartBase implements PersistedFile { | |
| readonly _tag = "PersistedFile" | |
| readonly key: string | |
| readonly name: string | |
| readonly contentType: string | |
| readonly path: string | |
| constructor( | |
| key: string, | |
| name: string, | |
| contentType: string, | |
| path: string | |
| ) { | |
| super() | |
| this.key = key | |
| this.name = name | |
| this.contentType = contentType | |
| this.path = path | |
| } | |
| toJSON(): unknown { | |
| return { | |
| _id: "@effect/platform/Multipart/Part", | |
| _tag: "PersistedFile", | |
| key: this.key, | |
| name: this.name, | |
| contentType: this.contentType, | |
| path: this.path | |
| } | |
| } | |
| } | |
| /** | |
| * Creates a context containing multipart parser limit settings. | |
| * | |
| * **Details** | |
| * | |
| * The context can provide maximum part count, field size, file size, total body | |
| * size, and MIME types that should be parsed as fields. | |
| * | |
| * @category references | |
| * @since 4.0.0 | |
| */ | |
| export const limitsServices = (options: { | |
| readonly maxParts?: number | undefined | |
| readonly maxFieldSize?: FileSystem.SizeInput | undefined | |
| readonly maxFileSize?: FileSystem.SizeInput | undefined | |
| readonly maxTotalSize?: FileSystem.SizeInput | undefined | |
| readonly fieldMimeTypes?: ReadonlyArray<string> | undefined | |
| }): Context.Context<never> => { | |
| const map = new Map<string, unknown>() | |
| if (options.maxParts !== undefined) { | |
| map.set(MaxParts.key, options.maxParts) | |
| } | |
| if (options.maxFieldSize !== undefined) { | |
| map.set(MaxFieldSize.key, FileSystem.Size(options.maxFieldSize)) | |
| } | |
| if (options.maxFileSize !== undefined) { | |
| map.set(MaxFileSize.key, UndefinedOr.map(options.maxFileSize, FileSystem.Size)) | |
| } | |
| if (options.maxTotalSize !== undefined) { | |
| map.set(IncomingMessage.MaxBodySize.key, UndefinedOr.map(options.maxTotalSize, FileSystem.Size)) | |
| } | |
| if (options.fieldMimeTypes !== undefined) { | |
| map.set(FieldMimeTypes.key, options.fieldMimeTypes) | |
| } | |
| return Context.makeUnsafe(map) | |
| } | |
| /** | |
| * Namespace containing multipart parser limit option types. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| export declare namespace withLimits { | |
| /** | |
| * Options for overriding multipart parser limits. | |
| * | |
| * **Details** | |
| * | |
| * These settings control maximum part count, field size, file size, total body | |
| * size, and MIME types that should be treated as fields instead of files. | |
| * | |
| * @category fiber refs | |
| * @since 4.0.0 | |
| */ | |
| export type Options = { | |
| readonly maxParts?: number | undefined | |
| readonly maxFieldSize?: FileSystem.SizeInput | undefined | |
| readonly maxFileSize?: FileSystem.SizeInput | undefined | |
| readonly maxTotalSize?: FileSystem.SizeInput | undefined | |
| readonly fieldMimeTypes?: ReadonlyArray<string> | undefined | |
| } | |
| } | |
| /** | |
| * Context reference for the maximum number of multipart parts allowed. | |
| * | |
| * **Details** | |
| * | |
| * The default is `undefined`, meaning no explicit part-count limit. | |
| * | |
| * @category references | |
| * @since 4.0.0 | |
| */ | |
| export const MaxParts = Context.Reference<number | undefined>("effect/http/Multipart/MaxParts", { | |
| defaultValue: () => undefined | |
| }) | |
| /** | |
| * Context reference for the maximum size of a multipart field value. | |
| * | |
| * **Details** | |
| * | |
| * The default limit is 10 MiB. | |
| * | |
| * @category references | |
| * @since 4.0.0 | |
| */ | |
| export const MaxFieldSize = Context.Reference<FileSystem.SizeInput>("effect/http/Multipart/MaxFieldSize", { | |
| defaultValue: constant(FileSystem.Size(10 * 1024 * 1024)) | |
| }) | |
| /** | |
| * Context reference for the maximum size of a multipart file part. | |
| * | |
| * **Details** | |
| * | |
| * The default is `undefined`, meaning no explicit per-file limit. | |
| * | |
| * @category references | |
| * @since 4.0.0 | |
| */ | |
| export const MaxFileSize = Context.Reference<FileSystem.SizeInput | undefined>( | |
| "effect/http/Multipart/MaxFileSize", | |
| { defaultValue: () => undefined } | |
| ) | |
| /** | |
| * Context reference for MIME type fragments that should be parsed as multipart | |
| * fields instead of files. | |
| * | |
| * **Details** | |
| * | |
| * The default treats `application/json` parts as fields. | |
| * | |
| * @category references | |
| * @since 4.0.0 | |
| */ | |
| export const FieldMimeTypes = Context.Reference<ReadonlyArray<string>>("effect/http/Multipart/FieldMimeTypes", { | |
| defaultValue: constant(["application/json"]) | |
| }) | |
Xet Storage Details
- Size:
- 23.3 kB
- Xet hash:
- 2c7b8bbb599e425df037d9d2b1736b08a19fb9fb946902c074dbb6ce3b59cb33
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.