Skip to content
This repository has been archived by the owner on Dec 16, 2024. It is now read-only.

Commit

Permalink
feat: implements object-storage module
Browse files Browse the repository at this point in the history
  • Loading branch information
jspark2000 committed Mar 1, 2024
1 parent 5f016fe commit 26955c6
Show file tree
Hide file tree
Showing 15 changed files with 1,169 additions and 120 deletions.
3 changes: 2 additions & 1 deletion backend/.swcrc
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@
"@libs/exception": ["./libs/exception/src/index.ts"],
"@libs/decorator": ["./libs/decorator/src/index.ts"],
"@libs/constants": ["./libs/constants/src/index.ts"],
"@libs/cache": ["./libs/cache/src/index.ts"]
"@libs/cache": ["./libs/cache/src/index.ts"],
"@libs/storage": ["./libs/storage/src/index.ts"]
}
}
}
4 changes: 3 additions & 1 deletion backend/app/src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { JwtAuthGuard, JwtAuthModule, RolesModule } from '@libs/auth'
import { CacheConfigService } from '@libs/cache'
import { ExceptionsFilter } from '@libs/exception'
import { PrismaModule } from '@libs/prisma'
import { StorageModule } from '@libs/storage'
import { AppController } from './app.controller'
import { AppService } from './app.service'
import { AuthModule } from './auth/auth.module'
Expand All @@ -22,7 +23,8 @@ import { UserModule } from './user/user.module'
PrismaModule,
JwtAuthModule,
UserModule,
RolesModule
RolesModule,
StorageModule
],
controllers: [AppController],
providers: [
Expand Down
1 change: 1 addition & 0 deletions backend/libs/constants/src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export * from './time.constants'
export * from './storage.constants'
1 change: 1 addition & 0 deletions backend/libs/constants/src/storage.constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export const IMAGE_SIZE_LIMIT = 10 * 1024 * 1024 // 10mb
126 changes: 126 additions & 0 deletions backend/libs/storage/src/image-storage.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { ConfigService } from '@nestjs/config'
import {
DeleteObjectCommand,
PutObjectCommand,
S3Client
} from '@aws-sdk/client-s3'
import { Service } from '@libs/decorator'
import {
ParameterValidationException,
UnexpectedException
} from '@libs/exception'
import mime from 'mime-types'
import { v4 as uuidv4 } from 'uuid'
import type { ImageStorageService } from './storage.interface'

@Service()
export class ImageStorageServiceImpl implements ImageStorageService {
private readonly s3: S3Client

constructor(private readonly configService: ConfigService) {
if (this.configService.get('NODE_ENV') === 'production') {
this.s3 = new S3Client()
} else {
this.s3 = new S3Client({
region: this.configService.get('AWS_CDN_BUCKET_REGION'),
endpoint: this.configService.get('AWS_CDN_BUCKET_URL'),
forcePathStyle: true,
credentials: {
accessKeyId: this.configService.get('AWS_CDN_ACCESS_KEY') || '',
secretAccessKey: this.configService.get('AWS_CDN_SECRET_KEY') || ''
}
})
}
}

async uploadObject(
file: Express.Multer.File,
src: string
): Promise<{ src: string }> {
try {
const extension = this.getFileExtension(file.originalname)
const keyWithoutExtenstion = `${src}/${this.generateUniqueImageName()}`
const key = keyWithoutExtenstion + `${extension}`
const fileType = this.extractContentType(file)

await this.s3.send(
new PutObjectCommand({
Bucket: this.configService.get('AWS_CDN_BUCKET_NAME'),
Key: key,
Body: file.buffer,
ContentType: fileType
})
)

if (this.configService.get('NODE_ENV') === 'production') {
return {
src: keyWithoutExtenstion
}
} else {
return {
src: key
}
}
} catch (error) {
throw new UnexpectedException(error)
}
}

async deleteObject(src: string): Promise<{ result: string }> {
try {
await this.s3.send(
new DeleteObjectCommand({
Bucket: this.configService.get('AWS_CDN_ORIGIN_BUCKET_NAME'),
Key: src
})
)

return { result: 'ok' }
} catch (error) {
throw new UnexpectedException(error)
}
}

/**
* 랜덤한 uuid를 생성하여 리턴합니다.
*
* @returns {string}
*/
private generateUniqueImageName(): string {
const uniqueId = uuidv4()

return uniqueId
}

/**
* 파일이름으로 부터 MimeType을 추출하여 리턴합니다.
* @param {Express.Multer.File} file - MimeType을 추출할 파일
* @returns {string} 추출한 MimeType
*/
private extractContentType(file: Express.Multer.File): string {
if (file.mimetype) {
return file.mimetype.toString()
}

return file.originalname
? mime.lookup(file.originalname) || 'application/octet-stream'
: 'application/octet-stream'
}

/**
* 파일에서 확장자를 추출합니다.
*
* @param {string} filename - 파일 원본명
* @returns {string} 확장자
* @throws {ParameterValidationException} 전달받은 파일에 확장자가 없는 경우 발생
*/
private getFileExtension(filename: string): string {
const match = filename.match(/\.[^.]+$/)

if (match) {
return match[0]
}

throw new ParameterValidationException('Unsupported file extension')
}
}
3 changes: 3 additions & 0 deletions backend/libs/storage/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export * from './options/image-options'
export * from './storage.module'
export * from './image-storage.service'
21 changes: 21 additions & 0 deletions backend/libs/storage/src/options/image-options.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { MulterOptions } from '@nestjs/platform-express/multer/interfaces/multer-options.interface'
import { IMAGE_SIZE_LIMIT } from '@libs/constants'
import type { FileFilterCallback } from 'multer'

export const IMAGE_OPTIONS: MulterOptions = {
limits: {
fieldSize: IMAGE_SIZE_LIMIT
},
fileFilter: (
req: Request,
file: Express.Multer.File,
cb: FileFilterCallback
) => {
const fileExts = ['png', 'jpg', 'jpeg', 'webp', 'heif', 'heic']
const ext = file.originalname.split('.').pop().toLocaleLowerCase()
if (!fileExts.includes(ext)) {
return cb(null, false)
}
cb(null, true)
}
}
20 changes: 20 additions & 0 deletions backend/libs/storage/src/storage.interface.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
export interface StorageService {
/**
* 파일을 지정한 경로에 저장하고 저장된 파일이름을 포함한 전체 경로를 반환합니다.
*
* @param {Express.Multer.File} file - 저장할 파일
* @param {string} src - 파일을 저장할 경로
* @returns {Promise<{src: string}>} 파일이 저장된 경로
*/
uploadObject(file: Express.Multer.File, src: string): Promise<{ src: string }>

/**
* 해당 URI에 있는 파일을 삭제하고 삭제 결과를 반환합니다.
*
* @param {string} src - 삭제할 파일이 위치한 경로
* @returns {Promise<{result: string}>} 파일 삭제 결과
*/
deleteObject(src: string): Promise<{ result: string }>
}

export interface ImageStorageService extends StorageService {}
19 changes: 19 additions & 0 deletions backend/libs/storage/src/storage.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { Global, Module } from '@nestjs/common'
import { ImageStorageServiceImpl } from './image-storage.service'

@Global()
@Module({
providers: [
{
provide: 'ImageStorageService',
useClass: ImageStorageServiceImpl
}
],
exports: [
{
provide: 'ImageStorageService',
useClass: ImageStorageServiceImpl
}
]
})
export class StorageModule {}
9 changes: 9 additions & 0 deletions backend/libs/storage/tsconfig.lib.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"declaration": true,
"outDir": "../../dist/libs/storage"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "test", "**/*spec.ts"]
}
9 changes: 9 additions & 0 deletions backend/nest-cli.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,15 @@
"compilerOptions": {
"tsConfigPath": "libs/cache/tsconfig.lib.json"
}
},
"storage": {
"type": "library",
"root": "libs/storage",
"entryFile": "index",
"sourceRoot": "libs/storage/src",
"compilerOptions": {
"tsConfigPath": "libs/storage/tsconfig.lib.json"
}
}
}
}
8 changes: 6 additions & 2 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
"test:cov": "vitest run --coverage"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.525.0",
"@nestjs/cache-manager": "^2.2.1",
"@nestjs/common": "^10.3.0",
"@nestjs/config": "^3.1.1",
Expand All @@ -28,9 +29,11 @@
"cache-manager-redis-yet": "^4.1.2",
"cookie-parser": "^1.4.6",
"express": "^4.18.2",
"mime-types": "^2.1.35",
"passport-jwt": "^4.0.1",
"reflect-metadata": "^0.1.12",
"rxjs": "^7.8.1"
"rxjs": "^7.8.1",
"uuid": "^9.0.1"
},
"devDependencies": {
"@nestjs/cli": "^10.3.0",
Expand All @@ -40,7 +43,8 @@
"@swc/core": "^1.3.107",
"@swc/register": "^0.1.10",
"@types/express": "^4.17.17",
"@types/jest": "^29.5.2",
"@types/mime-types": "^2.1.4",
"@types/multer": "^1.4.11",
"@types/node": "^20.3.1",
"@types/passport-jwt": "^4.0.1",
"@types/supertest": "^2.0.12",
Expand Down
3 changes: 2 additions & 1 deletion backend/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"@libs/exception": ["./libs/exception/src/index.ts"],
"@libs/decorator": ["./libs/decorator/src/index.ts"],
"@libs/constants": ["./libs/constants/src/index.ts"],
"@libs/cache": ["./libs/cache/src/index.ts"]
"@libs/cache": ["./libs/cache/src/index.ts"],
"@libs/storage": ["./libs/storage/src/index.ts"]
}
}
}
3 changes: 2 additions & 1 deletion backend/vitest.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ export default defineConfig({
'@libs/exception': './libs/exception/src/index.ts',
'@libs/decorator': './libs/decorator/src/index.ts',
'@libs/constants': './libs/constants/src/index.ts',
'@libs/cache': './libs/cache/src/index.ts'
'@libs/cache': './libs/cache/src/index.ts',
'@libs/storage': './libs/storage/src/index.ts'
}
},
plugins: [swc.vite()]
Expand Down
Loading

0 comments on commit 26955c6

Please sign in to comment.