Skip to content

Commit

Permalink
Merge pull request #22 from SamagraX-Stencil/fix/refactor
Browse files Browse the repository at this point in the history
feat: 🚀 file upload
  • Loading branch information
techsavvyash authored Dec 24, 2023
2 parents b4365e7 + f38b436 commit dc460c2
Show file tree
Hide file tree
Showing 33 changed files with 7,022 additions and 26 deletions.
9 changes: 8 additions & 1 deletion packages/common/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@samagra-x/stencil",
"version": "0.0.4",
"version": "0.0.5",
"description": "Stencil - An opinionated backend framework for NodeJS based on NestJS",
"author": "Yash Mittal (@techsavvyash) & Team SamagraX",
"license": "MIT",
Expand Down Expand Up @@ -37,10 +37,17 @@
"@nestjs/common": "^10.0.0",
"@nestjs/core": "^10.0.0",
"@nestjs/platform-express": "^10.0.0",
"@nestjs/platform-fastify": "^10.3.0",
"@temporalio/client": "^1.8.6",
"@temporalio/worker": "^1.8.6",
"@types/multer": "^1.4.11",
"cache-manager": "^5.2.4",
"cache-manager-redis-store": "2",
"fastify": "^4.25.2",
"fastify-multer": "^2.0.3",
"fastify-multipart": "^5.4.0",
"minio": "^7.1.3",
"multer": "^1.4.5-lts.1",
"nestjs-temporal": "^2.0.1",
"prom-client": "^15.1.0",
"redis": "^4.6.10",
Expand Down
69 changes: 69 additions & 0 deletions packages/common/src/controllers/file-upload.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import {
Controller,
Post,
UploadedFile,
UseInterceptors,
Param,
Get,
Res,
Query,
} from '@nestjs/common';
import { FastifyFileInterceptor } from '../interceptors/file-upload.interceptor';
import { MultipartFile } from '../interfaces/file-upload.interface';
import { FileUploadService } from '../services/file-upload.service';
import { FastifyReply } from 'fastify';

@Controller('files')
export class FileUploadController {
constructor(private readonly filesService: FileUploadService) {}

@Post('upload-file')
@UseInterceptors(FastifyFileInterceptor('file', {}))
async uploadFile(
@UploadedFile() file: MultipartFile,
@Query('destination') destination: string,
@Query('filename') filename: string,
): Promise<{
statusCode?: number;
message: string;
file?: { url: string } | undefined;
}> {
try {
const directory = await this.filesService.upload(
file,
destination,
filename,
);
return {
message: 'File uploaded successfully',
file: { url: directory },
};
} catch (error) {
console.error(`Error uploading file: ${error.message}`);
return {
statusCode: 500,
message: 'File upload failed',
file: undefined,
};
}
}

@Get('download/:destination')
async downloadFile(
@Param('destination') destination: string,
@Res() res: FastifyReply,
): Promise<void> {
try {
const fileStream = await this.filesService.download(destination);
res.headers({
'Content-Type': 'application/octet-stream',
'Content-Disposition': `attachment; filename=${destination}`,
});
fileStream.pipe(res.raw);
} catch (error) {
console.log('error: ', error);
console.error(`Error downloading file: ${error.message}`);
res.status(500).send('File download failed');
}
}
}
2 changes: 2 additions & 0 deletions packages/common/src/controllers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './file-upload.controller';
export * from './prometheus.controller';
File renamed without changes.
4 changes: 3 additions & 1 deletion packages/common/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,6 @@ export * from './monitoring';
export * from './interceptors';
export * from './services';
export * from './modules';
export * from './controller';
export * from './controllers';
export * from './interfaces';
// export * from './controllers/prometheus.controller';
56 changes: 56 additions & 0 deletions packages/common/src/interceptors/file-upload.interceptor.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import {
CallHandler,
ExecutionContext,
Inject,
mixin,
NestInterceptor,
Optional,
Type,
} from '@nestjs/common';
import { Observable } from 'rxjs';
import FastifyMulter from 'fastify-multer';
import { Options, Multer } from 'multer';

type MulterInstance = any;
export function FastifyFileInterceptor(
fieldName: string,
localOptions: Options,
): Type<NestInterceptor> {
class MixinInterceptor implements NestInterceptor {
protected multer: MulterInstance;

constructor(
@Optional()
@Inject('MULTER_MODULE_OPTIONS')
options: Multer,
) {
this.multer = (FastifyMulter as any)({ ...options, ...localOptions });
}

async intercept(
context: ExecutionContext,
next: CallHandler,
): Promise<Observable<any>> {
const ctx = context.switchToHttp();

await new Promise<void>((resolve, reject) =>
this.multer.single(fieldName)(
ctx.getRequest(),
ctx.getResponse(),
(error: any) => {
if (error) {
// const error = transformException(err);
console.log(error);
return reject(error);
}
resolve();
},
),
);

return next.handle();
}
}
const Interceptor = mixin(MixinInterceptor);
return Interceptor as Type<NestInterceptor>;
}
1 change: 1 addition & 0 deletions packages/common/src/interceptors/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './response-time.interceptor';
export * from './response-format.interceptor';
export * from './file-upload.interceptor';
export * from './utils';
17 changes: 17 additions & 0 deletions packages/common/src/interfaces/file-upload.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import * as fastifyMutipart from 'fastify-multipart';

export enum STORAGE_MODE {
MINIO = 'minio',
LOCAL = 'local',
}

export interface MultipartFile {
toBuffer: () => Promise<Buffer>;
file: NodeJS.ReadableStream;
filepath: string;
fieldname: string;
filename: string;
encoding: string;
mimetype: string;
fields: fastifyMutipart.MultipartFields;
}
1 change: 1 addition & 0 deletions packages/common/src/interfaces/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './file-upload.interface';
10 changes: 10 additions & 0 deletions packages/common/src/modules/file-upload.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { Module } from '@nestjs/common';
import { FileUploadController } from '../controllers/file-upload.controller';
import { FileUploadService } from '../services/file-upload.service';

@Module({
imports: [],
controllers: [FileUploadController],
providers: [FileUploadService],
})
export class FileUploadModule {}
1 change: 1 addition & 0 deletions packages/common/src/modules/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './temporal.module';
export * from './file-upload.module';
123 changes: 123 additions & 0 deletions packages/common/src/services/file-upload.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { FastifyInstance } from 'fastify';
import * as fs from 'fs';
import * as path from 'path';
import * as fastify from 'fastify';
import { InternalServerErrorException, Logger } from '@nestjs/common';
import { Client } from 'minio';
import { STORAGE_MODE } from '../interfaces/file-upload.interface';

export class FileUploadService {
private readonly storage: any;
private readonly useMinio: boolean;
private readonly fastifyInstance: FastifyInstance;
private logger: Logger;
private useSSL = false;

constructor() {
this.logger = new Logger('FileUploadService');
this.useMinio = process.env.STORAGE_MODE?.toLowerCase() === 'minio';
this.useSSL = !process.env.STORAGE_USE_SSL
? false
: process.env.STORAGE_USE_SSL?.toLocaleLowerCase() === 'true';

switch (process.env.STORAGE_MODE?.toLowerCase()) {
case STORAGE_MODE.MINIO:
this.storage = new Client({
endPoint: process.env.STORAGE_ENDPOINT,
port: parseInt(process.env.STORAGE_PORT),
useSSL: this.useSSL,
accessKey: process.env.STORAGE_ACCESS_KEY,
secretKey: process.env.STORAGE_SECRET_KEY,
});
break;
default:
this.fastifyInstance = fastify();
}
}

async uploadToMinio(filename: string, file: any): Promise<string> {
const metaData = {
'Content-Type': file.mimetype,
};
return new Promise((resolve, reject) => {
this.storage.putObject(
process.env.MINIO_BUCKETNAME,
filename,
file.buffer,
metaData,
function (err) {
if (err) {
console.log('err: ', err);
reject(err);
}
resolve(
`${this.useSSL ? 'https' : 'http'}://${process.env.STORAGE_ENDPOINT
}:${process.env.STORAGE_PORT}/${process.env.MINIO_BUCKETNAME
}/${filename}`,
);
},
);
});
}

async saveLocalFile(
destination: string,
filename: string,
file: any,
): Promise<string> {
const uploadsDir = path.join(process.cwd(), destination);
const localFilePath = path.join(uploadsDir, filename);
if (!fs.existsSync(uploadsDir)) {
try {
// Create the directory
fs.mkdirSync(uploadsDir, { recursive: true });
this.logger.log(`Directory created at ${uploadsDir}`);
} catch (err) {
this.logger.error(`Error creating directory: ${err.message}`);
}
} else {
this.logger.log(`Directory already exists at ${uploadsDir}`);
}
fs.writeFileSync(localFilePath, file.buffer);
return destination;
}

async upload(
file: any,
destination: string,
filename: string,
): Promise<string> {
try {
switch (process.env.STORAGE_MODE?.toLowerCase()) {
case STORAGE_MODE.MINIO:
this.logger.log('using minio');
return await this.uploadToMinio(filename, file);
default:
this.logger.log('writing to storage');
return await this.saveLocalFile(destination, filename, file);
}
} catch (error) {
this.logger.error(`Error uploading file: ${error}`);
throw new InternalServerErrorException('File upload failed');
}
}

async download(destination: string): Promise<any> {
try {
if (this.useMinio) {
const fileStream = await this.storage.getObject(
process.env.STORAGE_CONTAINER_NAME,
destination,
);
return fileStream;
} else {
const localFilePath = path.join(process.cwd(), 'uploads', destination); // don't use __dirname here that'll point to the dist folder and not the top level folder containing the project (and the uploads folder)
const fileStream = fs.createReadStream(localFilePath);
return fileStream;
}
} catch (error) {
this.logger.error(`Error downloading file: ${error.message}`);
throw new InternalServerErrorException('File download failed');
}
}
}
1 change: 1 addition & 0 deletions packages/common/src/services/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './temporal.service';
export * from './file-upload.service';
Loading

0 comments on commit dc460c2

Please sign in to comment.