Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add support for multiple files upload #49

Merged
merged 1 commit into from
May 11, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"inversify": "6.0.2",
"jsonwebtoken": "9.0.2",
"knex": "3.1.0",
"mime": "^4.0.3",
"node-fetch": "3.3.2",
"pino": "9.0.0",
"sharp": "0.33.3",
Expand Down
6 changes: 2 additions & 4 deletions apps/backend/src/common/types/http/httpRequest.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,15 @@
/* eslint-disable @typescript-eslint/no-explicit-any */

import { type Readable } from 'node:stream';

export interface AttachedFile {
readonly name: string;
readonly type: string;
readonly data: Readable;
readonly filePath: string;
}

export interface HttpRequest<Body = any, QueryParams = any, PathParams = any> {
readonly body: Body;
readonly queryParams: QueryParams;
readonly pathParams: PathParams;
readonly headers: Record<string, string>;
readonly file?: AttachedFile | undefined;
readonly files?: AttachedFile[] | undefined;
}
35 changes: 25 additions & 10 deletions apps/backend/src/core/httpRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import { TypeClone } from '@sinclair/typebox';
import { type FastifyInstance, type FastifyReply, type FastifyRequest, type FastifySchema } from 'fastify';
import { createWriteStream } from 'node:fs';
import { pipeline } from 'node:stream';
import { promisify } from 'node:util';

import { coreSymbols } from './symbols.js';
import { ApplicationError } from '../common/errors/base/applicationError.js';
Expand All @@ -20,6 +23,8 @@ import { type LoggerService } from '../libs/logger/services/loggerService/logger
import { ForbiddenAccessError } from '../modules/authModule/application/errors/forbiddenAccessError.js';
import { UnauthorizedAccessError } from '../modules/authModule/application/errors/unathorizedAccessError.js';

const streamPipeline = promisify(pipeline);

export interface RegisterControllersPayload {
controllers: HttpController[];
}
Expand Down Expand Up @@ -82,17 +87,27 @@ export class HttpRouter {
headers: fastifyRequest.headers,
});

let attachedFile: AttachedFile | undefined;
let attachedFiles: AttachedFile[] | undefined;

if (fastifyRequest.isMultipart()) {
const file = await fastifyRequest.file();

if (file) {
attachedFile = {
name: file.filename,
type: file.mimetype,
data: file.file,
};
attachedFiles = [];

const files = fastifyRequest.files();

for await (const file of files) {
const { filename, mimetype, file: data } = file;

const filePath = `/tmp/${filename}`;

const writer = createWriteStream(filePath);

await streamPipeline(data, writer);

attachedFiles.push({
name: filename,
type: mimetype,
filePath,
});
}
}

Expand All @@ -105,7 +120,7 @@ export class HttpRouter {
pathParams: fastifyRequest.params,
queryParams: fastifyRequest.query,
headers: fastifyRequest.headers as Record<string, string>,
file: attachedFile,
files: attachedFiles,
});

fastifyReply.status(statusCode);
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/src/core/httpServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export class HttpServer {
});

this.fastifyInstance.setSerializerCompiler(() => {
return (data) => JSON.stringify(data);
return (data): string => JSON.stringify(data);
});

this.addRequestPreprocessing();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,10 @@ import {
} from './schemas/findUserBucketsSchema.js';
import { type ResourceMetadataDTO } from './schemas/resourceMetadataDTO.js';
import {
type UploadResourceResponseBodyDTO,
type UploadResourcePathParamsDTO,
uploadResourceResponseBodyDTOSchema,
uploadResourcePathParamsDTOSchema,
type UploadResourcesResponseBodyDTO,
type UploadResourcesPathParamsDTO,
uploadResourcesResponseBodyDTOSchema,
uploadResourcesPathParamsDTOSchema,
} from './schemas/uploadResourceSchema.js';
import { OperationNotValidError } from '../../../../../common/errors/common/operationNotValidError.js';
import { type HttpController } from '../../../../../common/types/http/httpController.js';
Expand All @@ -62,7 +62,7 @@ import { SecurityMode } from '../../../../../common/types/http/securityMode.js';
import { type AccessControlService } from '../../../../authModule/application/services/accessControlService/accessControlService.js';
import { type FindUserBucketsQueryHandler } from '../../../../userModule/application/queryHandlers/findUserBucketsQueryHandler/findUserBucketsQueryHandler.js';
import { type DeleteResourceCommandHandler } from '../../../application/commandHandlers/deleteResourceCommandHandler/deleteResourceCommandHandler.js';
import { type UploadResourceCommandHandler } from '../../../application/commandHandlers/uploadResourceCommandHandler/uploadResourceCommandHandler.js';
import { type UploadResourcesCommandHandler } from '../../../application/commandHandlers/uploadResourcesCommandHandler/uploadResourcesCommandHandler.js';
import { type DownloadImageQueryHandler } from '../../../application/queryHandlers/downloadImageQueryHandler/downloadImageQueryHandler.js';
import { type DownloadResourceQueryHandler } from '../../../application/queryHandlers/downloadResourceQueryHandler/downloadResourceQueryHandler.js';
import { type DownloadResourcesQueryHandler } from '../../../application/queryHandlers/downloadResourcesQueryHandler/downloadResourcesQueryHandler.js';
Expand All @@ -77,7 +77,7 @@ export class ResourceHttpController implements HttpController {
private readonly deleteResourceCommandHandler: DeleteResourceCommandHandler,
private readonly findResourcesMetadataQueryHandler: FindResourcesMetadataQueryHandler,
private readonly downloadResourceQueryHandler: DownloadResourceQueryHandler,
private readonly uploadResourceCommandHandler: UploadResourceCommandHandler,
private readonly uploadResourceCommandHandler: UploadResourcesCommandHandler,
private readonly downloadResourcesQueryHandler: DownloadResourcesQueryHandler,
private readonly downloadImageQueryHandler: DownloadImageQueryHandler,
private readonly downloadVideoPreviewQueryHandler: DownloadVideoPreviewQueryHandler,
Expand Down Expand Up @@ -126,21 +126,21 @@ export class ResourceHttpController implements HttpController {
new HttpRoute({
method: HttpMethodName.post,
path: ':bucketName/resources',
handler: this.uploadResource.bind(this),
handler: this.uploadResources.bind(this),
schema: {
request: {
pathParams: uploadResourcePathParamsDTOSchema,
pathParams: uploadResourcesPathParamsDTOSchema,
},
response: {
[HttpStatusCode.created]: {
schema: uploadResourceResponseBodyDTOSchema,
description: 'Resource uploaded.',
schema: uploadResourcesResponseBodyDTOSchema,
description: 'Resources uploaded.',
},
},
},
securityMode: SecurityMode.bearer,
tags: ['Resource'],
description: `Upload a Resource.`,
description: `Upload Resources.`,
}),
new HttpRoute({
method: HttpMethodName.post,
Expand Down Expand Up @@ -320,29 +320,27 @@ export class ResourceHttpController implements HttpController {
};
}

private async uploadResource(
request: HttpRequest<undefined, undefined, UploadResourcePathParamsDTO>,
): Promise<HttpCreatedResponse<UploadResourceResponseBodyDTO>> {
private async uploadResources(
request: HttpRequest<undefined, undefined, UploadResourcesPathParamsDTO>,
): Promise<HttpCreatedResponse<UploadResourcesResponseBodyDTO>> {
const { userId } = await this.accessControlService.verifyBearerToken({
authorizationHeader: request.headers['authorization'],
});

if (!request.file) {
const files = request.files;

if (!files || files.length === 0) {
throw new OperationNotValidError({
reason: 'File is required.',
reason: 'Files are required.',
});
}

const { bucketName } = request.pathParams;

const { name, type, data } = request.file;

await this.uploadResourceCommandHandler.execute({
userId,
resourceName: name,
bucketName,
contentType: type,
data,
files,
});

return {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@ import type * as contracts from '@common/contracts';

import { type TypeExtends } from '../../../../../../common/types/schemaExtends.js';

export const uploadResourcePathParamsDTOSchema = Type.Object({
export const uploadResourcesPathParamsDTOSchema = Type.Object({
bucketName: Type.String({ minLength: 1 }),
});

export type UploadResourcePathParamsDTO = TypeExtends<
Static<typeof uploadResourcePathParamsDTOSchema>,
contracts.UploadResourcePathParams
export type UploadResourcesPathParamsDTO = TypeExtends<
Static<typeof uploadResourcesPathParamsDTOSchema>,
contracts.UploadResourcesPathParams
>;

export const uploadResourceResponseBodyDTOSchema = Type.Any();
export const uploadResourcesResponseBodyDTOSchema = Type.Null();

export type UploadResourceResponseBodyDTO = Static<typeof uploadResourceResponseBodyDTOSchema>;
export type UploadResourcesResponseBodyDTO = Static<typeof uploadResourcesResponseBodyDTOSchema>;

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { type CommandHandler } from '../../../../../common/types/commandHandler.js';
import { type AttachedFile } from '../../../../../common/types/http/httpRequest.js';

export interface UploadResourcesCommandHandlerPayload {
readonly userId: string;
readonly bucketName: string;
readonly files: AttachedFile[];
}

export type UploadResourcesCommandHandler = CommandHandler<UploadResourcesCommandHandlerPayload, void>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import mime from 'mime';
import { createReadStream } from 'node:fs';

import {
type UploadResourcesCommandHandler,
type UploadResourcesCommandHandlerPayload,
} from './uploadResourcesCommandHandler.js';
import { OperationNotValidError } from '../../../../../common/errors/common/operationNotValidError.js';
import { type LoggerService } from '../../../../../libs/logger/services/loggerService/loggerService.js';
import { type FindUserBucketsQueryHandler } from '../../../../userModule/application/queryHandlers/findUserBucketsQueryHandler/findUserBucketsQueryHandler.js';
import { type ResourceBlobService } from '../../../domain/services/resourceBlobService/resourceBlobService.js';

export class UploadResourcesCommandHandlerImpl implements UploadResourcesCommandHandler {
public constructor(
private readonly resourceBlobSerice: ResourceBlobService,
private readonly loggerService: LoggerService,
private readonly findUserBucketsQueryHandler: FindUserBucketsQueryHandler,
) {}

public async execute(payload: UploadResourcesCommandHandlerPayload): Promise<void> {
const { userId, bucketName, files } = payload;

const { buckets } = await this.findUserBucketsQueryHandler.execute({ userId });

if (!buckets.includes(bucketName)) {
throw new OperationNotValidError({
reason: 'Bucket is not assigned to this user.',
userId,
bucketName,
});
}

await Promise.all(
files.map(async (file) => {
const { name: resourceName, filePath } = file;

const contentType = mime.getType(filePath);

if (!contentType) {
throw new OperationNotValidError({
reason: 'Content type not found',
resourceName,
});
}

this.loggerService.debug({
message: 'Uploading Resource...',
userId,
bucketName,
resourceName,
contentType,
});

const data = createReadStream(filePath);

await this.resourceBlobSerice.uploadResource({
bucketName,
resourceName,
data,
contentType,
});

this.loggerService.debug({
message: 'Resource uploaded.',
userId,
bucketName,
resourceName,
contentType,
});
}),
);
}
}
Loading
Loading