Skip to content

Commit

Permalink
Merge pull request #279 from boostcampwm2023/server/feature/261
Browse files Browse the repository at this point in the history
이미지 업로드 로직에 CLOVA GreenEye API 추가
  • Loading branch information
sk000801 authored Dec 6, 2023
2 parents 8df8cd4 + 05e1d4c commit df08a83
Show file tree
Hide file tree
Showing 8 changed files with 223 additions and 43 deletions.
78 changes: 41 additions & 37 deletions .github/workflows/server-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ name: Server CD

on:
pull_request:
branches: [ "develop", "main" ]
branches: ["develop", "main"]
paths:
- "server/**"
types:
- closed

jobs:
deploy:
runs-on: ubuntu-20.04
Expand All @@ -19,40 +19,44 @@ jobs:
steps:
- name: Checkout repository
uses: actions/checkout@v3

- name: Set up Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'
cache-dependency-path: '**/package-lock.json'
cache: "npm"
cache-dependency-path: "**/package-lock.json"

- name: Install depenencies
run: |
cd server
npm install
cd server
npm install
- name: Create prod.env file
env:
DB_HOST_IP: ${{ secrets.SERVER_ENV_DB_HOST_IP }}
DB_PORT: ${{ secrets.SERVER_ENV_DB_PORT }}
DB_USER_NAME: ${{ secrets.SERVER_ENV_DB_USER_NAME }}
DB_PASSWORD: ${{ secrets.SERVER_ENV_DB_PASSWORD }}
DB_DATABASE_NAME: ${{ secrets.SERVER_ENV_DB_DATABASE_NAME }}
ACCESS_ID: ${{ secrets.SERVER_ENV_ACCESS_ID }}
SECRET_ACCESS_KEY: ${{ secrets.SERVER_ENV_SECRET_ACCESS_KEY }}
JWT_SECRET_KEY: ${{ secrets.SERVER_ENV_JWT_SECRET_KEY }}
DB_HOST_IP: ${{ secrets.SERVER_ENV_DB_HOST_IP }}
DB_PORT: ${{ secrets.SERVER_ENV_DB_PORT }}
DB_USER_NAME: ${{ secrets.SERVER_ENV_DB_USER_NAME }}
DB_PASSWORD: ${{ secrets.SERVER_ENV_DB_PASSWORD }}
DB_DATABASE_NAME: ${{ secrets.SERVER_ENV_DB_DATABASE_NAME }}
ACCESS_ID: ${{ secrets.SERVER_ENV_ACCESS_ID }}
SECRET_ACCESS_KEY: ${{ secrets.SERVER_ENV_SECRET_ACCESS_KEY }}
JWT_SECRET_KEY: ${{ secrets.SERVER_ENV_JWT_SECRET_KEY }}
GREEN_EYE_SECRET_KEY: ${{secrets.GREEN_EYE_SECRET_KEY}}
GREEN_EYE_REQUEST_URL: ${{secrets.GREEN_EYE_REQUEST_URL}}
run: |
cd server
touch prod.env
echo "DB_HOST_IP=$DB_HOST_IP" >> prod.env
echo "DB_PORT=$DB_PORT" >> prod.env
echo "DB_USER_NAME=$DB_USER_NAME" >> prod.env
echo "DB_PASSWORD=$DB_PASSWORD" >> prod.env
echo "DB_DATABASE_NAME=$DB_DATABASE_NAME" >> prod.env
echo "ACCESS_ID=$ACCESS_ID" >> prod.env
echo "SECRET_ACCESS_KEY=$SECRET_ACCESS_KEY" >> prod.env
echo "JWT_SECRET_KEY=$JWT_SECRET_KEY" >> prod.env
cd server
touch prod.env
echo "DB_HOST_IP=$DB_HOST_IP" >> prod.env
echo "DB_PORT=$DB_PORT" >> prod.env
echo "DB_USER_NAME=$DB_USER_NAME" >> prod.env
echo "DB_PASSWORD=$DB_PASSWORD" >> prod.env
echo "DB_DATABASE_NAME=$DB_DATABASE_NAME" >> prod.env
echo "ACCESS_ID=$ACCESS_ID" >> prod.env
echo "SECRET_ACCESS_KEY=$SECRET_ACCESS_KEY" >> prod.env
echo "JWT_SECRET_KEY=$JWT_SECRET_KEY" >> prod.env
echo "GREEN_EYE_SECRET_KEY=$GREEN_EYE_SECRET_KEY" >> prod.env
echo "GREEN_EYE_REQUEST_URL=$GREEN_EYE_REQUEST_URL" >> prod.env
- name: Build Docker image
run: docker build --platform linux/amd64 ./server -t ${{ secrets.NCP_REGISTRY }}/catchy-tape:latest
Expand All @@ -66,14 +70,14 @@ jobs:
- name: SSH into Server
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SERVER_SSH_HOST }}
username: ${{ secrets.SERVER_SSH_USER }}
password: ${{ secrets.SERVER_SSH_PASSWORD }}
port: ${{ secrets.SERVER_SSH_PORT }}
script: |
docker login ${{ secrets.NCP_REGISTRY }} -u ${{ secrets.NCP_DOCKER_ACCESS_KEY_ID }} -p ${{ secrets.NCP_DOCKER_SECRET_KEY }}
docker pull ${{ secrets.NCP_REGISTRY }}/catchy-tape:latest
docker stop catchy-tape-latest
docker rm catchy-tape-latest
docker run -d -p 3000:3000 --name catchy-tape-latest ${{ secrets.NCP_REGISTRY }}/catchy-tape:latest
curl -X POST -H 'Content-type: application/json' --data '{"text":"서버 배포 성공!"}' ${{ secrets.SLACK_WEBHOOK_URL }}
host: ${{ secrets.SERVER_SSH_HOST }}
username: ${{ secrets.SERVER_SSH_USER }}
password: ${{ secrets.SERVER_SSH_PASSWORD }}
port: ${{ secrets.SERVER_SSH_PORT }}
script: |
docker login ${{ secrets.NCP_REGISTRY }} -u ${{ secrets.NCP_DOCKER_ACCESS_KEY_ID }} -p ${{ secrets.NCP_DOCKER_SECRET_KEY }}
docker pull ${{ secrets.NCP_REGISTRY }}/catchy-tape:latest
docker stop catchy-tape-latest
docker rm catchy-tape-latest
docker run -d -p 3000:3000 --name catchy-tape-latest ${{ secrets.NCP_REGISTRY }}/catchy-tape:latest
curl -X POST -H 'Content-type: application/json' --data '{"text":"서버 배포 성공!"}' ${{ secrets.SLACK_WEBHOOK_URL }}
3 changes: 3 additions & 0 deletions server/src/config/errorCode.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ export enum ERROR_CODE {
'INVALID_INPUT_TYPE_VALUE' = 4008,
'NOT_EXIST_MUSIC_ID' = 4009,
'NOT_EXIST_TS_IN_BUCKET' = 4010,
'INVALID_GREEN_EYE_REQUEST' = 4011,
'FAIL_GREEN_EYE_IMAGE_RECOGNITION' = 4012,
'BAD_IMAGE' = 4013,
'WRONG_TOKEN' = 4100,
'EXPIRED_TOKEN' = 4101,
}
69 changes: 69 additions & 0 deletions server/src/config/greenEye.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { CatchyException } from 'src/config/catchyException';
import { ERROR_CODE } from 'src/config/errorCode.enum';
import { GreenEyeResponseDto } from 'src/dto/greenEye.response.dto';
import { HTTP_STATUS_CODE } from 'src/httpStatusCode.enum';
import { Logger } from '@nestjs/common';

@Injectable()
export class GreenEyeService {
private readonly logger = new Logger('GreenEyeService');
greenEyeSecretKey: string;
greenEyeRequestUrl: string;

contentType: string = 'application/json';

constructor(private readonly configService: ConfigService) {
this.greenEyeSecretKey = configService.get<string>('GREEN_EYE_SECRET_KEY');
this.greenEyeRequestUrl = configService.get<string>(
'GREEN_EYE_REQUEST_URL',
);
}

private getTimeStamp(): number {
return new Date().getTime();
}

private getRequestInit(imageUrl: string): RequestInit {
return {
method: 'POST',
headers: {
'X-GREEN-EYE-SECRET': this.greenEyeSecretKey,
'Content-Type': this.contentType,
},
body: JSON.stringify({
version: 'V1',
requestId: 'requestId',
timestamp: this.getTimeStamp(),
images: [
{
name: 'image',
url: imageUrl,
},
],
}),
};
}

async getResultOfNormalImage(imageUrl: string): Promise<GreenEyeResponseDto> {
return await fetch(this.greenEyeRequestUrl, this.getRequestInit(imageUrl))
.then((res) => {
if (res.ok) {
return res.json();
}

throw new Error();
})
.catch((err) => {
this.logger.error(
`greenEye.service - getResultOfNormalImage : INVALID_GREEN_EYE_REQUEST`,
);
throw new CatchyException(
'INVALID_GREEN_EYE_REQUEST',
HTTP_STATUS_CODE.BAD_REQUEST,
ERROR_CODE.INVALID_GREEN_EYE_REQUEST,
);
});
}
}
39 changes: 39 additions & 0 deletions server/src/dto/greenEye.response.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { IsNotEmpty, IsString } from 'class-validator';

export class GreenEyeResponseDto {
@IsString()
@IsNotEmpty()
version: string;

@IsString()
@IsNotEmpty()
requestId: string;

@IsString()
@IsNotEmpty()
timestamp: number;

@IsNotEmpty()
images: [
{
result: {
adult: {
confidence: number;
};
normal: {
confidence: number;
};
porn: {
confidence: number;
};
sexy: {
confidence: number;
};
};
latency: number;
confidence: number;
message: string;
name: string;
},
];
}
1 change: 1 addition & 0 deletions server/src/entity/music.entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ export class Music extends BaseEntity {
title: true,
cover: true,
music_file: true,
genre: true,
user: {
user_id: true,
nickname: true,
Expand Down
9 changes: 8 additions & 1 deletion server/src/music/music.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,17 @@ import { AuthModule } from 'src/auth/auth.module';
import { UploadService } from 'src/upload/upload.service';
import { NcloudConfigService } from 'src/config/ncloud.config';
import { Logger } from 'winston';
import { GreenEyeService } from 'src/config/greenEye.service';

@Module({
imports: [TypeOrmModule.forFeature([Music]), AuthModule],
controllers: [MusicController],
providers: [MusicService, UploadService, NcloudConfigService, Logger],
providers: [
MusicService,
UploadService,
NcloudConfigService,
Logger,
GreenEyeService,
],
})
export class MusicModule {}
3 changes: 2 additions & 1 deletion server/src/upload/upload.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ import { UploadService } from './upload.service';
import { NcloudConfigService } from 'src/config/ncloud.config';
import { AuthModule } from 'src/auth/auth.module';
import { Logger } from 'winston';
import { GreenEyeService } from 'src/config/greenEye.service';

@Module({
imports: [AuthModule],
controllers: [UploadController],
providers: [UploadService, NcloudConfigService, Logger],
providers: [UploadService, NcloudConfigService, Logger, GreenEyeService],
})
export class UploadModule {}
64 changes: 60 additions & 4 deletions server/src/upload/upload.service.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Inject, Injectable, Logger, LoggerService } from '@nestjs/common';
import { Injectable, Logger } from '@nestjs/common';
import { HTTP_STATUS_CODE } from 'src/httpStatusCode.enum';
import { NcloudConfigService } from './../config/ncloud.config';
import { S3 } from 'aws-sdk';
Expand All @@ -7,13 +7,16 @@ import { CatchyException } from 'src/config/catchyException';
import { ERROR_CODE } from 'src/config/errorCode.enum';
import * as fs from 'fs';
import { Readable } from 'stream';
import { GreenEyeService } from '../config/greenEye.service';
import { DeleteObjectOutput } from 'aws-sdk/clients/s3';

@Injectable()
export class UploadService {
private readonly logger = new Logger('UploadService');
private objectStorage: S3;
constructor(
private readonly nCloudConfigService: NcloudConfigService,
private readonly greenEyeService: GreenEyeService,
) {
this.objectStorage = nCloudConfigService.createObjectStorageOption();
}
Expand All @@ -33,6 +36,47 @@ export class UploadService {
return false;
}

private async deleteObjectStorageImage(
path: string,
): Promise<DeleteObjectOutput> {
return await this.objectStorage
.deleteObject({
Bucket: 'catchy-tape-bucket2',
Key: path,
})
.promise();
}

async checkImageNormal(
message: string,
confidence: number,
keyPath: string,
): Promise<void> {
if (message !== 'SUCCESS') {
await this.deleteObjectStorageImage(keyPath);

this.logger.error(
`upload.service - checkImageNormal : FAIL_GREEN_EYE_IMAGE_RECOGNITION`,
);
throw new CatchyException(
'FAIL_GREEN_EYE_IMAGE_RECOGNITION',
HTTP_STATUS_CODE.BAD_REQUEST,
ERROR_CODE.FAIL_GREEN_EYE_IMAGE_RECOGNITION,
);
}

if (confidence < 0.9) {
await this.deleteObjectStorageImage(keyPath);

this.logger.error(`upload.service - checkImageNormal : BAD_IMAGE`);
throw new CatchyException(
'BAD_IMAGE',
HTTP_STATUS_CODE.BAD_REQUEST,
ERROR_CODE.BAD_IMAGE,
);
}
}

async uploadMusic(
file: Express.Multer.File,
musicId: string,
Expand Down Expand Up @@ -98,8 +142,6 @@ export class UploadService {
);
}

const encodedFileName = encodeURIComponent(file.originalname);

const keyPath =
type === 'user'
? `image/user/${id}/image.png`
Expand All @@ -115,8 +157,22 @@ export class UploadService {
})
.promise();

const { images } = await this.greenEyeService.getResultOfNormalImage(
uploadResult.Location,
);

await this.checkImageNormal(
images[0].message,
images[0].confidence,
keyPath,
);

return { url: uploadResult.Location };
} catch {
} catch (err) {
if (err instanceof CatchyException) {
throw err;
}

this.logger.error(`upload.service - uploadImage : SERVICE_ERROR`);
throw new CatchyException(
'SERVER ERROR',
Expand Down

0 comments on commit df08a83

Please sign in to comment.