feat(s3): implement non-ephemeral storage through S3/DO Spaces

This commit is contained in:
Amruth Pillai
2022-04-09 09:28:08 +02:00
parent d0863d68c6
commit feb911aea0
13 changed files with 973 additions and 38 deletions

View File

@ -10,6 +10,7 @@
"lint": "eslint --fix --ext .ts ./src"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.67.0",
"@nestjs/axios": "^0.0.7",
"@nestjs/common": "^8.4.4",
"@nestjs/config": "^2.0.0",

View File

@ -7,6 +7,7 @@ import authConfig from './auth.config';
import databaseConfig from './database.config';
import googleConfig from './google.config';
import sendgridConfig from './sendgrid.config';
import storageConfig from './storage.config';
const validationSchema = Joi.object({
// App
@ -40,12 +41,20 @@ const validationSchema = Joi.object({
SENDGRID_FORGOT_PASSWORD_TEMPLATE_ID: Joi.string().allow(''),
SENDGRID_FROM_NAME: Joi.string().allow(''),
SENDGRID_FROM_EMAIL: Joi.string().allow(''),
// Storage
STORAGE_BUCKET: Joi.string().allow(''),
STORAGE_REGION: Joi.string().allow(''),
STORAGE_ENDPOINT: Joi.string().allow(''),
STORAGE_URL_PREFIX: Joi.string().allow(''),
STORAGE_ACCESS_KEY: Joi.string().allow(''),
STORAGE_SECRET_KEY: Joi.string().allow(''),
});
@Module({
imports: [
NestConfigModule.forRoot({
load: [appConfig, authConfig, databaseConfig, googleConfig, sendgridConfig],
load: [appConfig, authConfig, databaseConfig, googleConfig, sendgridConfig, storageConfig],
validationSchema: validationSchema,
}),
],

View File

@ -0,0 +1,10 @@
import { registerAs } from '@nestjs/config';
export default registerAs('storage', () => ({
bucket: process.env.STORAGE_BUCKET,
region: process.env.STORAGE_REGION,
endpoint: process.env.STORAGE_ENDPOINT,
urlPrefix: process.env.STORAGE_URL_PREFIX,
accessKey: process.env.STORAGE_ACCESS_KEY,
secretKey: process.env.STORAGE_SECRET_KEY,
}));

View File

@ -99,7 +99,7 @@ export class ResumeController {
@Put(':id/photo')
@UseInterceptors(FileInterceptor('file'))
async uploadPhoto(@Param('id') id: string, @User('id') userId: number, @UploadedFile() file: Express.Multer.File) {
return this.resumeService.uploadPhoto(+id, userId, file.filename);
return this.resumeService.uploadPhoto(+id, userId, file);
}
@UseGuards(JwtAuthGuard)

View File

@ -2,12 +2,9 @@ import { Module } from '@nestjs/common';
import { ConfigModule } from '@nestjs/config';
import { MulterModule } from '@nestjs/platform-express';
import { TypeOrmModule } from '@nestjs/typeorm';
import { mkdir } from 'fs/promises';
import { diskStorage } from 'multer';
import { extname, join } from 'path';
import { memoryStorage } from 'multer';
import { AuthModule } from '@/auth/auth.module';
import { User } from '@/users/entities/user.entity';
import { UsersModule } from '@/users/users.module';
import { Resume } from './entities/resume.entity';
@ -18,24 +15,7 @@ import { ResumeService } from './resume.service';
imports: [
ConfigModule,
TypeOrmModule.forFeature([Resume]),
MulterModule.register({
storage: diskStorage({
destination: async (req, _, cb) => {
const userId = (req.user as User).id;
const resumeId = +req.params.id;
const destination = join(__dirname, '..', `assets/uploads/${userId}/${resumeId}`);
await mkdir(destination, { recursive: true });
cb(null, destination);
},
filename: (_, file, cb) => {
const filename = new Date().getTime() + extname(file.originalname);
cb(null, filename);
},
}),
}),
MulterModule.register({ storage: memoryStorage() }),
AuthModule,
UsersModule,
],

View File

@ -1,11 +1,11 @@
import { DeleteObjectCommand, PutObjectCommand, S3, S3Client } from '@aws-sdk/client-s3';
import { HttpException, HttpStatus, Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { InjectRepository } from '@nestjs/typeorm';
import { Resume as ResumeSchema } from '@reactive-resume/schema';
import { unlink } from 'fs/promises';
import { pick, sample, set } from 'lodash';
import { nanoid } from 'nanoid';
import { join } from 'path';
import { extname } from 'path';
import { Repository } from 'typeorm';
import { PostgresErrorCode } from '@/database/errorCodes.enum';
@ -22,11 +22,22 @@ export const SHORT_ID_LENGTH = 8;
@Injectable()
export class ResumeService {
private s3Client: S3Client;
constructor(
@InjectRepository(Resume) private resumeRepository: Repository<Resume>,
private configService: ConfigService,
private usersService: UsersService
) {}
) {
this.s3Client = new S3({
endpoint: configService.get<string>('storage.endpoint'),
region: configService.get<string>('storage.region'),
credentials: {
accessKeyId: configService.get<string>('storage.accessKey'),
secretAccessKey: configService.get<string>('storage.secretKey'),
},
});
}
async create(createResumeDto: CreateResumeDto, userId: number) {
try {
@ -216,22 +227,44 @@ export class ResumeService {
return this.resumeRepository.update(id, nextResume);
}
async uploadPhoto(id: number, userId: number, filename: string) {
async uploadPhoto(id: number, userId: number, file: Express.Multer.File) {
const resume = await this.findOne(id, userId);
const url = `/api/assets/uploads/${userId}/${id}/${filename}`;
const updatedResume = set(resume, 'basics.photo.url', url);
const urlPrefix = this.configService.get<string>('storage.urlPrefix');
const filename = new Date().getTime() + extname(file.originalname);
const key = `uploads/${userId}/${id}/${filename}`;
await this.s3Client.send(
new PutObjectCommand({
Bucket: this.configService.get<string>('storage.bucket'),
Key: key,
Body: file.buffer,
ACL: 'public-read',
})
);
const publicUrl = urlPrefix + key;
const updatedResume = set(resume, 'basics.photo.url', publicUrl);
return this.resumeRepository.save<Resume>(updatedResume);
}
async deletePhoto(id: number, userId: number) {
const resume = await this.findOne(id, userId);
const filepath = new URL(resume.basics.photo.url).pathname;
const photoPath = join(__dirname, '..', `assets/${filepath}`);
const updatedResume = set(resume, 'basics.photo.url', '');
await unlink(photoPath);
const urlPrefix = this.configService.get<string>('storage.urlPrefix');
const publicUrl = resume.basics.photo.url;
const key = publicUrl.replace(urlPrefix, '');
await this.s3Client.send(
new DeleteObjectCommand({
Bucket: this.configService.get<string>('storage.bucket'),
Key: key,
})
);
const updatedResume = set(resume, 'basics.photo.url', '');
return this.resumeRepository.save<Resume>(updatedResume);
}