@adonisjs/bodyparser
Version:
BodyParser middleware for AdonisJS http server to read and parse request body
258 lines (257 loc) • 7.42 kB
JavaScript
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 };