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

Feat/s3 integration #26

Merged
merged 5 commits into from
Jan 17, 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 .platform/hooks/predeploy/01_databse_migration.sh
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
#!/usr/bin/env bash

node node_modules/prisma/build/index.js migrate resolve --applied 20240117154221_user_profile_image
5 changes: 5 additions & 0 deletions domain/user.domain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ export class UserDomain implements User {
})
message: string;

@ApiProperty({
required: false,
})
profileImage: string;

@ApiProperty({
required: false,
})
Expand Down
13 changes: 10 additions & 3 deletions libs/aws-s3/src/aws-s3.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import { v4 } from 'uuid';
* getSignedURL: https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/modules/_aws_sdk_s3_request_presigner.html
* PutObjectCommand: https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-s3/classes/putobjectcommand.html
*
*
* 2024 Hoplin
*/

@Injectable()
Expand All @@ -43,20 +45,25 @@ export class AwsS3Service {
}

/** Upload file */
public async uploadFile(file: Express.Multer.File, directory: string) {
public async uploadFile(
file: Express.Multer.File,
directory: string,
tag?: { [k: string]: string },
) {
/** Generate file salt */
const fileSalt = v4();
/** File name with salt */
const fileKey = `${fileSalt}_${file.originalname}`;
/** S3 upload location */
const s3SavedIn = this.directoryBuilder(fileKey, directory);
const tagQuery = tag ? new URLSearchParams(tag).toString() : '';
/** S3 Object put command */
const command = new PutObjectCommand({
Bucket: this.s3Bucket,
Key: s3SavedIn,
ContentType: file.mimetype,
Body: file.buffer,
ACL: 'public-read',
Tagging: tagQuery,
});

await this.s3Client.send(command);
Expand All @@ -70,7 +77,7 @@ export class AwsS3Service {
return null;
}
return `https://${process.env.AWS_S3_BUCKET}.s3.${
process.env.AWS_S3_Region
process.env.AWS_REGION
}.amazonaws.com/${this.directoryBuilder(fileKey, directory)}`;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE `user` ADD COLUMN `profileImage` VARCHAR(191) NULL;
47 changes: 24 additions & 23 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -12,19 +12,20 @@ datasource db {
}

model User {
id String @id @default(uuid())
nickname String @unique
password String
email String @unique
message String?
github String?
blog String?
type UserType? @default(User)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
problems Problem[]
Submission Submission[]
Issues ProblemIssue[]
id String @id @default(uuid())
nickname String @unique
password String
email String @unique
message String?
github String?
blog String?
profileImage String?
type UserType? @default(User)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
problems Problem[]
Submission Submission[]
Issues ProblemIssue[]
IssueComments ProblemIssueComment[]

@@map("user")
Expand Down Expand Up @@ -57,7 +58,7 @@ model Problem {
isArchived Boolean @default(false)
deletedAt DateTime?

issues ProblemIssue[]
issues ProblemIssue[]
comments ProblemIssueComment[]

createdAt DateTime @default(now())
Expand Down Expand Up @@ -121,33 +122,33 @@ model Submission {
}

model ProblemIssue {
id Int @id @default(autoincrement())
title String @db.VarChar(100) @default("Title Here")
content String @db.Text
id Int @id @default(autoincrement())
title String @default("Title Here") @db.VarChar(100)
content String @db.Text
// 1:N relation with Problem
problemId Int
problem Problem @relation(references: [id], fields: [problemId], onDelete: Cascade)
// 1:N relation with User
issuerId String
issuer User @relation(references: [id], fields: [issuerId], onDelete: Cascade)
issuerId String
issuer User @relation(references: [id], fields: [issuerId], onDelete: Cascade)

comments ProblemIssueComment[]

@@map("problem_issue")
}

model ProblemIssueComment {
id Int @id @default(autoincrement())
id Int @id @default(autoincrement())
content String @db.Text

issueId Int
issue ProblemIssue @relation(fields: [issueId], references: [id], onDelete: Cascade, onUpdate: Cascade)

userId String
user User @relation(fields: [userId],references: [id],onDelete: Cascade, onUpdate: Cascade)
userId String
user User @relation(fields: [userId], references: [id], onDelete: Cascade, onUpdate: Cascade)

problemId Int
problem Problem @relation(references: [id],fields: [problemId])
problem Problem @relation(references: [id], fields: [problemId])

@@map("problem_issue_comment")
}
2 changes: 2 additions & 0 deletions src/app.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { ConfigModule } from '@nestjs/config';
import { JudgeModule } from './judge/judge.module';
import { SystemLoggerModule } from './system-logger/system-logger.module';
import { WorkerModule } from './worker/worker.module';
import { ArtifactModule } from './artifact/artifact.module';

@Module({
imports: [
Expand All @@ -18,6 +19,7 @@ import { WorkerModule } from './worker/worker.module';
UserModule,
JudgeModule,
WorkerModule,
ArtifactModule,
],
controllers: [AppController],
providers: [AppService],
Expand Down
31 changes: 31 additions & 0 deletions src/artifact/artifact.controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import {
Controller,
Post,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { FileInterceptor } from '@nestjs/platform-express';
import {
FileNameTransformPipe,
FileOptionFactory,
StaticArtifactConfig,
} from 'app/config/artifact.config';
import { ArtifactDocs } from './artifact.docs';
import { ArtifactService } from './artifact.service';

@Controller('artifact')
@ArtifactDocs.Controller()
export class ArtifactController {
constructor(private artifactService: ArtifactService) {}

@Post('static')
@UseInterceptors(
FileInterceptor('artifact', FileOptionFactory(StaticArtifactConfig)),
)
@ArtifactDocs.uploadStaticArtifact()
uploadStaticArtifact(
@UploadedFile(FileNameTransformPipe) file: Express.Multer.File,
) {
return this.artifactService.uploadStaticArtifact(file);
}
}
30 changes: 30 additions & 0 deletions src/artifact/artifact.docs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { applyDecorators } from '@nestjs/common';
import {
ApiBody,
ApiConsumes,
ApiCreatedResponse,
ApiOperation,
ApiTags,
} from '@nestjs/swagger';
import { UploadStaticArtifactDto } from './dto';
import { UploadStaticArtifactResponse } from './response';

export class ArtifactDocs {
public static Controller() {
return applyDecorators(ApiTags('Artifact'));
}
public static uploadStaticArtifact() {
return applyDecorators(
ApiOperation({
summary: 'Static File Upload. Public Artifact 업로드',
}),
ApiConsumes('multipart/form-data'),
ApiBody({
type: UploadStaticArtifactDto,
}),
ApiCreatedResponse({
type: UploadStaticArtifactResponse,
}),
);
}
}
11 changes: 11 additions & 0 deletions src/artifact/artifact.module.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { Module } from '@nestjs/common';
import { ArtifactController } from './artifact.controller';
import { ArtifactService } from './artifact.service';
import { AwsS3Module } from 's3/aws-s3';

@Module({
imports: [AwsS3Module],
controllers: [ArtifactController],
providers: [ArtifactService],
})
export class ArtifactModule {}
38 changes: 38 additions & 0 deletions src/artifact/artifact.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Injectable } from '@nestjs/common';
import { StaticArtifactDir } from 'app/config';
import { AwsS3Service } from 's3/aws-s3';
import { UploadStaticArtifactResponse } from './response';

@Injectable()
export class ArtifactService {
constructor(private s3: AwsS3Service) {}

async uploadStaticArtifact(
file: Express.Multer.File,
): Promise<UploadStaticArtifactResponse> {
try {
const fileKey = await this.s3.uploadFile(file, StaticArtifactDir, {
public: 'true',
});
const url = this.s3.getStaticURL(fileKey, StaticArtifactDir);
return {
success: true,
url,
};
} catch (err) {
console.log(err);
return {
success: false,
url: '',
};
}
}
}

/**
* Bucket Policy
*
* Set tag "public=true" to set artifact public access
*
* https://repost.aws/knowledge-center/read-access-objects-s3-bucket
*/
1 change: 1 addition & 0 deletions src/artifact/dto/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './upload-static-artifact.dto';
11 changes: 11 additions & 0 deletions src/artifact/dto/upload-static-artifact.dto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { ApiProperty } from '@nestjs/swagger';
import { IsOptional } from 'class-validator';

export class UploadStaticArtifactDto {
@ApiProperty({
type: 'string',
format: 'binary',
})
@IsOptional()
artifact: Express.Multer.File;
}
1 change: 1 addition & 0 deletions src/artifact/response/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './upload-static-artifact.response';
9 changes: 9 additions & 0 deletions src/artifact/response/upload-static-artifact.response.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { ApiProperty } from '@nestjs/swagger';

export class UploadStaticArtifactResponse {
@ApiProperty()
success: boolean;

@ApiProperty()
url: string;
}
2 changes: 2 additions & 0 deletions src/auth/dto/signin.dto.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import { PickType } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
import { UserDomain } from 'domains';

export class SigninDto extends PickType(UserDomain, ['email', 'password']) {
@Transform(({ value }) => value.trim().toLowerCase())
@IsEmail()
@IsNotEmpty()
email: string;
Expand Down
2 changes: 2 additions & 0 deletions src/auth/dto/signup.dto.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PickType } from '@nestjs/swagger';
import { Transform } from 'class-transformer';
import { IsEmail, IsNotEmpty, IsString } from 'class-validator';
import { UserDomain } from 'domains';

Expand All @@ -7,6 +8,7 @@ export class SingupDto extends PickType(UserDomain, [
'password',
'email',
]) {
@Transform(({ value }) => value.trim().toLowerCase())
@IsString()
@IsNotEmpty()
nickname: string;
Expand Down
10 changes: 10 additions & 0 deletions src/config/artifact-directory.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* S3 Bucket Destination
*
*
* Follow this format: (root)/~~/(dir)
*/

export const UserProfileImageDir = 'user/profile';

export const StaticArtifactDir = 'statics';
Loading