Files
upstream-rocketchat/apps/meteor/server/api/lib/getUploadFormData.ts
upstream-mirror 30810eecd7
Some checks failed
Create issues from TODOs / todos (push) Failing after 3s
Close inactive issues / close-issues (push) Has been cancelled
Deploy GitHub Pages / deploy-preview (push) Has been cancelled
CI / ⚙️ Variables Setup (push) Has been cancelled
CI / ⚙️ Test Guard (push) Has been cancelled
CI / 🚀 Notify external services - draft (push) Has been cancelled
CI / 📦 Build Packages (push) Has been cancelled
CI / 📦 Meteor Build (coverage) (push) Has been cancelled
CI / 📦 Meteor Build (production) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [account-service presence-service omnichannel-transcript-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [authorization-service queue-worker-service ddp-streamer-service], fips) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Build Docker (amd64, [rocketchat], fips) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [account-service presence-service omnichannel-transcript-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [authorization-service queue-worker-service ddp-streamer-service], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], ${{ (github.event_name != 'release' && github.ref != 'refs/heads/develop') && 'coverage' || 'production' }}) (push) Has been cancelled
CI / 🚢 Build Docker (arm64, [rocketchat], coverage) (push) Has been cancelled
CI / 🚢 Publish Docker Images (ghcr.io) (push) Has been cancelled
CI / 📦 Track Image Sizes (push) Has been cancelled
CI / 🔎 Code Check - Action Lint (push) Has been cancelled
CI / 🔎 Code Check (push) Has been cancelled
CI / 🔨 Test Storybook (push) Has been cancelled
CI / 🔨 Test Unit (push) Has been cancelled
CI / 🔨 Test API (CE) (push) Has been cancelled
CI / 🔨 Test API Livechat (CE) (push) Has been cancelled
CI / 🔨 Test UI (CE) (push) Has been cancelled
CI / 🔨 Test API (EE) (push) Has been cancelled
CI / 🔨 Test API Livechat (EE) (push) Has been cancelled
CI / 🔨 Test API Apps (deno-runtime - EE) (push) Has been cancelled
CI / 🔨 Test UI (EE) (push) Has been cancelled
CI / 🔨 Test API (FIPS) (push) Has been cancelled
CI / 🔨 Test API Livechat (FIPS) (push) Has been cancelled
CI / 🔨 Test UI (FIPS) (push) Has been cancelled
CI / 🔨 Test Federation Matrix (push) Has been cancelled
CI / 📊 Report Coverage (push) Has been cancelled
CI / ✅ Tests Done (push) Has been cancelled
CI / 🚀 Publish build assets (push) Has been cancelled
CI / 🚀 Publish Docker Images (DockerHub) (push) Has been cancelled
CI / 🚀 Notify external services (push) Has been cancelled
CI / Update Version Durability (push) Has been cancelled
Release / Release (push) Has been cancelled
Auto-close duplicate issues / auto-close-duplicates (push) Failing after 1s
Code scanning - action / CodeQL-Build (push) Failing after 2s
snapshot of https://github.com/RocketChat/Rocket.Chat.git (develop, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:21:02 +05:00

176 lines
4.3 KiB
TypeScript

import { Readable } from 'node:stream';
import { ReadableStream } from 'node:stream/web';
import { MeteorError } from '@rocket.chat/core-services';
import type { ValidateFunction } from 'ajv';
import busboy from 'busboy';
import { getMimeType } from '../../../app/utils/lib/mimeTypes';
type UploadResult<K> = {
file: Readable & { truncated: boolean };
fieldname: string;
filename: string;
encoding: string;
mimetype: string;
fileBuffer: Buffer;
fields: K;
};
type UploadResultWithOptionalFile<K> =
| UploadResult<K>
| ({
[P in keyof Omit<UploadResult<K>, 'fields'>]: undefined;
} & {
fields: K;
});
export async function getUploadFormData<
T extends string,
K extends Record<string, string> = Record<string, string>,
V extends ValidateFunction<K> = ValidateFunction<K>,
>(
{ request }: { request: Request },
options: {
field?: T;
validate?: V;
sizeLimit?: number;
fileOptional: true;
},
): Promise<UploadResultWithOptionalFile<K>>;
export async function getUploadFormData<
T extends string,
K extends Record<string, string> = Record<string, string>,
V extends ValidateFunction<K> = ValidateFunction<K>,
>(
{ request }: { request: Request },
options?: {
field?: T;
validate?: V;
sizeLimit?: number;
fileOptional?: false | undefined;
},
): Promise<UploadResult<K>>;
export async function getUploadFormData<
T extends string,
K extends Record<string, string> = Record<string, string>,
V extends ValidateFunction<K> = ValidateFunction<K>,
>(
{ request }: { request: Request },
options: {
field?: T;
validate?: V;
sizeLimit?: number;
fileOptional?: boolean;
} = {},
): Promise<UploadResultWithOptionalFile<K>> {
if (!(request.body instanceof ReadableStream)) {
return Promise.reject(new MeteorError('Invalid request body'));
}
const limits = {
files: 1,
...(options.sizeLimit && options.sizeLimit > -1 && { fileSize: options.sizeLimit }),
};
const bb = busboy({ headers: Object.fromEntries(request.headers.entries()), defParamCharset: 'utf8', limits });
const fields = Object.create(null) as K;
let uploadedFile: UploadResultWithOptionalFile<K> | undefined = {
fields,
encoding: undefined,
filename: undefined,
fieldname: undefined,
mimetype: undefined,
fileBuffer: undefined,
file: undefined,
};
const { promise: resultPromise, resolve, reject } = Promise.withResolvers<UploadResultWithOptionalFile<K>>();
function onField(fieldname: keyof K, value: K[keyof K]) {
fields[fieldname] = value;
}
function onEnd() {
if (!uploadedFile) {
return reject(new MeteorError('No file or fields were uploaded'));
}
if (!options.fileOptional && !uploadedFile?.file) {
return reject(new MeteorError('No file uploaded'));
}
if (options.validate !== undefined && !options.validate(fields)) {
return reject(
new MeteorError(`Invalid fields ${(options.validate.errors ?? []).map((e) => `${e.instancePath} ${e.message}`).join(', ')}`),
);
}
return resolve(uploadedFile);
}
function onFile(
fieldname: string,
file: Readable & { truncated: boolean },
{ filename, encoding, mimeType: mimetype }: { filename: string; encoding: string; mimeType: string },
) {
if (options.field && fieldname !== options.field) {
file.resume();
return reject(new MeteorError('invalid-field'));
}
const fileChunks: Uint8Array[] = [];
file.on('data', (chunk) => {
fileChunks.push(chunk);
});
file.on('end', () => {
if (file.truncated) {
fileChunks.length = 0;
return reject(new MeteorError('error-file-too-large'));
}
uploadedFile = {
file,
filename,
encoding,
mimetype: getMimeType(mimetype, filename),
fieldname,
fields,
fileBuffer: Buffer.concat(fileChunks),
};
});
}
function cleanup() {
bb.removeAllListeners();
}
bb.on('field', onField);
bb.on('file', onFile);
bb.on('close', cleanup);
bb.on('end', onEnd);
bb.on('finish', onEnd);
bb.on('error', (err: Error) => {
reject(err);
});
bb.on('partsLimit', () => {
reject();
});
bb.on('filesLimit', () => {
reject('Just 1 file is allowed');
});
bb.on('fieldsLimit', () => {
reject();
});
// Unclear why typescript complains that the ReadableStream from request.body is incompatible here
Readable.fromWeb(request.body satisfies ReadableStream)
.on('error', (err) => reject(err))
.pipe(bb);
return resultPromise;
}