multipart middleware (#5809)
* fix: multipart middleware * fix: reviews
This commit is contained in:
37
server/middlewares/multipart.ts
Normal file
37
server/middlewares/multipart.ts
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { Next } from "koa";
|
||||||
|
import { bytesToHumanReadable } from "@shared/utils/files";
|
||||||
|
import { InvalidRequestError } from "@server/errors";
|
||||||
|
import { APIContext } from "@server/types";
|
||||||
|
import { getFileFromRequest } from "@server/utils/koa";
|
||||||
|
|
||||||
|
export default function multipart({
|
||||||
|
maximumFileSize,
|
||||||
|
}: {
|
||||||
|
maximumFileSize: number;
|
||||||
|
}) {
|
||||||
|
return async function multipartMiddleware(ctx: APIContext, next: Next) {
|
||||||
|
if (!ctx.is("multipart/form-data")) {
|
||||||
|
ctx.throw(
|
||||||
|
InvalidRequestError("Request type must be multipart/form-data")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const file = getFileFromRequest(ctx.request);
|
||||||
|
if (!file) {
|
||||||
|
ctx.throw(InvalidRequestError("Request must include a file parameter"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (file.size > maximumFileSize) {
|
||||||
|
ctx.throw(
|
||||||
|
InvalidRequestError(
|
||||||
|
`The selected file was larger than the ${bytesToHumanReadable(
|
||||||
|
maximumFileSize
|
||||||
|
)} maximum size`
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.input = { ...(ctx.input ?? {}), file };
|
||||||
|
return next();
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -6,7 +6,10 @@ import { APIContext, BaseReq } from "@server/types";
|
|||||||
export default function validate<T extends z.ZodType<BaseReq>>(schema: T) {
|
export default function validate<T extends z.ZodType<BaseReq>>(schema: T) {
|
||||||
return async function validateMiddleware(ctx: APIContext, next: Next) {
|
return async function validateMiddleware(ctx: APIContext, next: Next) {
|
||||||
try {
|
try {
|
||||||
ctx.input = schema.parse(ctx.request);
|
ctx.input = {
|
||||||
|
...(ctx.input ?? {}),
|
||||||
|
...schema.parse(ctx.request),
|
||||||
|
};
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (err instanceof ZodError) {
|
if (err instanceof ZodError) {
|
||||||
const { path, message } = err.issues[0];
|
const { path, message } = err.issues[0];
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
|
import formidable from "formidable";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
const BaseSchema = z.object({
|
const BaseSchema = z.object({
|
||||||
body: z.unknown(),
|
body: z.unknown(),
|
||||||
query: z.unknown(),
|
query: z.unknown(),
|
||||||
|
file: z.custom<formidable.File>().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default BaseSchema;
|
export default BaseSchema;
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import mime from "mime-types";
|
|||||||
import { Op, ScopeOptions, WhereOptions } from "sequelize";
|
import { Op, ScopeOptions, WhereOptions } from "sequelize";
|
||||||
import { TeamPreference } from "@shared/types";
|
import { TeamPreference } from "@shared/types";
|
||||||
import { subtractDate } from "@shared/utils/date";
|
import { subtractDate } from "@shared/utils/date";
|
||||||
import { bytesToHumanReadable } from "@shared/utils/files";
|
|
||||||
import slugify from "@shared/utils/slugify";
|
import slugify from "@shared/utils/slugify";
|
||||||
import documentCreator from "@server/commands/documentCreator";
|
import documentCreator from "@server/commands/documentCreator";
|
||||||
import documentImporter from "@server/commands/documentImporter";
|
import documentImporter from "@server/commands/documentImporter";
|
||||||
@@ -26,6 +25,7 @@ import {
|
|||||||
} from "@server/errors";
|
} from "@server/errors";
|
||||||
import Logger from "@server/logging/Logger";
|
import Logger from "@server/logging/Logger";
|
||||||
import auth from "@server/middlewares/authentication";
|
import auth from "@server/middlewares/authentication";
|
||||||
|
import multipart from "@server/middlewares/multipart";
|
||||||
import { rateLimiter } from "@server/middlewares/rateLimiter";
|
import { rateLimiter } from "@server/middlewares/rateLimiter";
|
||||||
import { transaction } from "@server/middlewares/transaction";
|
import { transaction } from "@server/middlewares/transaction";
|
||||||
import validate from "@server/middlewares/validate";
|
import validate from "@server/middlewares/validate";
|
||||||
@@ -53,7 +53,6 @@ import {
|
|||||||
import { APIContext } from "@server/types";
|
import { APIContext } from "@server/types";
|
||||||
import { RateLimiterStrategy } from "@server/utils/RateLimiter";
|
import { RateLimiterStrategy } from "@server/utils/RateLimiter";
|
||||||
import ZipHelper from "@server/utils/ZipHelper";
|
import ZipHelper from "@server/utils/ZipHelper";
|
||||||
import { getFileFromRequest } from "@server/utils/koa";
|
|
||||||
import parseAttachmentIds from "@server/utils/parseAttachmentIds";
|
import parseAttachmentIds from "@server/utils/parseAttachmentIds";
|
||||||
import { getTeamFromContext } from "@server/utils/passport";
|
import { getTeamFromContext } from "@server/utils/passport";
|
||||||
import { assertPresent } from "@server/validation";
|
import { assertPresent } from "@server/validation";
|
||||||
@@ -1217,26 +1216,11 @@ router.post(
|
|||||||
auth(),
|
auth(),
|
||||||
rateLimiter(RateLimiterStrategy.TwentyFivePerMinute),
|
rateLimiter(RateLimiterStrategy.TwentyFivePerMinute),
|
||||||
validate(T.DocumentsImportSchema),
|
validate(T.DocumentsImportSchema),
|
||||||
|
multipart({ maximumFileSize: env.MAXIMUM_IMPORT_SIZE }),
|
||||||
transaction(),
|
transaction(),
|
||||||
async (ctx: APIContext<T.DocumentsImportReq>) => {
|
async (ctx: APIContext<T.DocumentsImportReq>) => {
|
||||||
if (!ctx.is("multipart/form-data")) {
|
|
||||||
throw InvalidRequestError("Request type must be multipart/form-data");
|
|
||||||
}
|
|
||||||
|
|
||||||
const { collectionId, parentDocumentId, publish } = ctx.input.body;
|
const { collectionId, parentDocumentId, publish } = ctx.input.body;
|
||||||
|
const file = ctx.input.file;
|
||||||
const file = getFileFromRequest(ctx.request);
|
|
||||||
if (!file) {
|
|
||||||
throw InvalidRequestError("Request must include a file parameter");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (env.MAXIMUM_IMPORT_SIZE && file.size > env.MAXIMUM_IMPORT_SIZE) {
|
|
||||||
throw InvalidRequestError(
|
|
||||||
`The selected file was larger than the ${bytesToHumanReadable(
|
|
||||||
env.MAXIMUM_IMPORT_SIZE
|
|
||||||
)} maximum size`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { transaction } = ctx.state;
|
const { transaction } = ctx.state;
|
||||||
const { user } = ctx.state.auth;
|
const { user } = ctx.state.auth;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import emojiRegex from "emoji-regex";
|
import emojiRegex from "emoji-regex";
|
||||||
|
import formidable from "formidable";
|
||||||
import isEmpty from "lodash/isEmpty";
|
import isEmpty from "lodash/isEmpty";
|
||||||
import isUUID from "validator/lib/isUUID";
|
import isUUID from "validator/lib/isUUID";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -272,6 +273,7 @@ export const DocumentsImportSchema = BaseSchema.extend({
|
|||||||
/** Import under this parent doc */
|
/** Import under this parent doc */
|
||||||
parentDocumentId: z.string().uuid().nullish(),
|
parentDocumentId: z.string().uuid().nullish(),
|
||||||
}),
|
}),
|
||||||
|
file: z.custom<formidable.File>(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type DocumentsImportReq = z.infer<typeof DocumentsImportSchema>;
|
export type DocumentsImportReq = z.infer<typeof DocumentsImportSchema>;
|
||||||
|
|||||||
Reference in New Issue
Block a user