UNPKG

@adonisjs/bodyparser

Version:

BodyParser middleware for AdonisJS http server to read and parse request body

258 lines (257 loc) 7.42 kB
import { dirname, extname, join } from "node:path"; import Macroable from "@poppinss/macroable"; import string from "@poppinss/utils/string"; import { Exception, RuntimeException } from "@poppinss/utils/exception"; import mediaTyper from "media-typer"; import { fileTypeFromBuffer, supportedExtensions } from "file-type"; import { access, copyFile, mkdir, rename, unlink } from "node:fs/promises"; const supportMagicFileTypes = supportedExtensions; function parseMimeType(mime) { try { const { type, subtype } = mediaTyper.parse(mime); return { type, subtype }; } catch (error) { return null; } } async function getFileType(fileContents) { const magicType = await fileTypeFromBuffer(fileContents); if (magicType) return Object.assign({ ext: magicType.ext.toLowerCase() }, parseMimeType(magicType.mime)); return null; } function computeFileTypeFromName(clientName, headers) { return Object.assign({ ext: extname(clientName).replace(/^\./, "").toLowerCase() }, parseMimeType(headers["content-type"])); } async function pathExists(filePath) { try { await access(filePath); return true; } catch { return false; } } async function moveFile(sourcePath, destinationPath, options = { overwrite: true }) { if (!sourcePath || !destinationPath) throw new RuntimeException("\"sourcePath\" and \"destinationPath\" required"); if (!options.overwrite && await pathExists(destinationPath)) throw new RuntimeException(`The destination file already exists: "${destinationPath}"`); await mkdir(dirname(destinationPath), { recursive: true, mode: options.directoryMode }); try { await rename(sourcePath, destinationPath); } catch (error) { if (error.code === "EXDEV") { await copyFile(sourcePath, destinationPath); await unlink(sourcePath); } else throw error; } } const formBodyNormalizers = { trimWhitespaces(value) { return value.trim(); }, trimWhitespacesAndConvertToNull(value) { value = value.trim(); return value === "" ? null : value; }, convertToNull(value) { return value === "" ? null : value; } }; var SizeValidator = class { #file; #maximumAllowedLimit; #bytesLimit = 0; validated = false; get maxLimit() { return this.#maximumAllowedLimit; } set maxLimit(limit) { if (this.#maximumAllowedLimit !== void 0) throw new Error("Cannot reset sizeLimit after file has been validated"); this.validated = false; this.#maximumAllowedLimit = limit; if (this.#maximumAllowedLimit) this.#bytesLimit = string.bytes.parse(this.#maximumAllowedLimit); } constructor(file) { this.#file = file; } #reportError() { this.#file.errors.push({ fieldName: this.#file.fieldName, clientName: this.#file.clientName, message: `File size should be less than ${string.bytes.format(this.#bytesLimit)}`, type: "size" }); } #validateWhenGettingStreamed() { if (this.#file.size > this.#bytesLimit) { this.validated = true; this.#reportError(); } } #validateAfterConsumed() { this.validated = true; if (this.#file.size > this.#bytesLimit) this.#reportError(); } validate() { if (this.validated) return; if (this.#maximumAllowedLimit === void 0) { this.validated = true; return; } if (this.#file.state === "streaming") { this.#validateWhenGettingStreamed(); return; } if (this.#file.state === "consumed") { this.#validateAfterConsumed(); return; } } }; var ExtensionValidator = class { #file; #allowedExtensions = []; validated = false; get extensions() { return this.#allowedExtensions; } set extensions(extnames) { if (this.#allowedExtensions && this.#allowedExtensions.length) throw new Error("Cannot update allowed extension names after file has been validated"); this.validated = false; this.#allowedExtensions = extnames; } constructor(file) { this.#file = file; } #reportError() { const suffix = this.#allowedExtensions.length === 1 ? "is" : "are"; const message = [`Invalid file extension ${this.#file.extname}.`, `Only ${this.#allowedExtensions.join(", ")} ${suffix} allowed`].join(" "); this.#file.errors.push({ fieldName: this.#file.fieldName, clientName: this.#file.clientName, message, type: "extname" }); } #validateWhenGettingStreamed() { if (!this.#file.extname) return; this.validated = true; if (this.#allowedExtensions.includes(this.#file.extname)) return; this.#reportError(); } #validateAfterConsumed() { this.validated = true; if (this.#allowedExtensions.includes(this.#file.extname || "")) return; this.#reportError(); } validate() { if (this.validated) return; if (!Array.isArray(this.#allowedExtensions) || this.#allowedExtensions.length === 0) { this.validated = true; return; } if (this.#file.state === "streaming") { this.#validateWhenGettingStreamed(); return; } if (this.#file.state === "consumed") this.#validateAfterConsumed(); } }; const STORE_IN_FLASH = Symbol.for("store_in_flash"); var MultipartFile = class extends Macroable { [STORE_IN_FLASH] = false; sizeValidator = new SizeValidator(this); extensionValidator = new ExtensionValidator(this); isMultipartFile = true; fieldName; clientName; headers; size = 0; extname; errors = []; type; subtype; filePath; fileName; tmpPath; meta = {}; state = "idle"; get validated() { return this.sizeValidator.validated && this.extensionValidator.validated; } get isValid() { return this.errors.length === 0; } get hasErrors() { return !this.isValid; } get sizeLimit() { return this.sizeValidator.maxLimit; } set sizeLimit(limit) { this.sizeValidator.maxLimit = limit; } get allowedExtensions() { return this.extensionValidator.extensions; } set allowedExtensions(extensions) { this.extensionValidator.extensions = extensions; } constructor(data, validationOptions) { super(); this.sizeLimit = validationOptions.size; this.allowedExtensions = validationOptions.extnames; this.fieldName = data.fieldName; this.clientName = data.clientName; this.headers = data.headers; } validate() { this.extensionValidator.validate(); this.sizeValidator.validate(); } markAsMoved(fileName, filePath) { this.filePath = filePath; this.fileName = fileName; this.state = "moved"; } async move(location, options) { if (!this.tmpPath) throw new Exception("property \"tmpPath\" must be set on the file before moving it", { status: 500, code: "E_MISSING_FILE_TMP_PATH" }); options = Object.assign({ name: `${string.uuid()}.${this.extname ?? "unknown"}`, overwrite: true }, options); const filePath = join(location, options.name); try { await moveFile(this.tmpPath, filePath, { overwrite: options.overwrite }); this.markAsMoved(options.name, filePath); } catch (error) { if (error.message.includes("destination file already")) throw new Exception(`"${options.name}" already exists at "${location}". Set "overwrite = true" to overwrite it`); throw error; } } toJSON() { return { fieldName: this.fieldName, clientName: this.clientName, size: this.size, filePath: this.filePath, fileName: this.fileName, type: this.type, extname: this.extname, subtype: this.subtype, state: this.state, isValid: this.isValid, validated: this.validated, errors: this.errors, meta: this.meta }; } }; export { supportMagicFileTypes as a, getFileType as i, computeFileTypeFromName as n, formBodyNormalizers as r, MultipartFile as t };