Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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
49 changes: 49 additions & 0 deletions apps/backend/apps/admin/src/polygon/file/file.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Injectable } from '@nestjs/common'
import type { ToolType } from '@prisma/client'
import type { FileUpload } from 'graphql-upload/processRequest.mjs'
import { UnprocessableDataException } from '@libs/exception'
import { PrismaService } from '@libs/prisma'

const MAX_TOOL_FILE_SIZE = 10 * 1024 * 1024 // 10MB

@Injectable()
export class FileService {
constructor(private readonly prisma: PrismaService) {}

async uploadPolygonToolFile(
problemId: number,
toolType: ToolType,
file: FileUpload
) {
const { filename, createReadStream } = file

//ReadStream → [chunk1, chunk2, chunk3, ...] → Buffer.concat
//→ 최종 Buffer로 변환해 → DB(PostgreSQL)에 저장
const chunks: Buffer[] = []
let total = 0
for await (const chunk of createReadStream()) {
total += chunk.length
if (total > MAX_TOOL_FILE_SIZE) {
throw new UnprocessableDataException('File size exceeds maximum limit')
}
chunks.push(chunk)
}
const fileContent = Buffer.concat(chunks).toString('utf-8')

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

파일 내용을 utf-8 문자열로 변환하여 DB에 저장하고 있습니다. PR 설명에 따르면 .cpp 소스코드를 상정하고 있으나, 사용자가 바이너리 파일 등 의도하지 않은 형식의 파일을 업로드할 경우 데이터가 손상될 수 있습니다. 업로드된 파일의 mimetype이나 확장자를 검증하는 로직을 추가하여 의도한 형식의 파일만 처리하도록 하는 것이 안전합니다.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

확장자 검증 추가하겠습니다..!


// (problemId, toolType) unique — 재업로드 시 갱신
const tool = await this.prisma.polygonTool.upsert({
// eslint-disable-next-line @typescript-eslint/naming-convention
where: { problemId_toolType: { problemId, toolType } },
update: { fileName: filename, fileContent },
create: { problemId, toolType, fileName: filename, fileContent }
})
Comment on lines +25 to +42
return tool
}

async deletePolygonFile(problemId: number, toolType: ToolType) {
return await this.prisma.polygonTool.delete({

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

prisma.polygonTool.delete는 삭제하려는 레코드가 존재하지 않을 경우 에러를 발생시킵니다. 해당 도구가 이미 삭제되었거나 존재하지 않는 경우를 대비하여 예외 처리를 추가하거나, 존재 여부를 먼저 확인하는 것이 좋습니다. 단순히 삭제 여부만 중요하다면 deleteMany를 사용하는 것도 방법이지만, 현재 리턴 타입이 PolygonTool이므로 에러 발생 시 적절한 GraphQL 에러(예: NotFoundException)를 던지도록 처리하는 것이 더 명확합니다.

// eslint-disable-next-line @typescript-eslint/naming-convention
where: { problemId_toolType: { problemId, toolType } }
})
}
}
6 changes: 4 additions & 2 deletions apps/backend/apps/admin/src/polygon/polygon.module.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { Module } from '@nestjs/common'
import { AMQPModule } from '@libs/amqp'
import { RolesModule } from '@libs/auth'
import { FileService } from './file/file.service'
import { PolygonResolver } from './polygon.resolver'
import { PolygonService } from './polygon.service'

@Module({
imports: [RolesModule],
providers: [PolygonResolver, PolygonService]
imports: [RolesModule, AMQPModule],
providers: [PolygonResolver, PolygonService, FileService]
})
export class PolygonModule {}
28 changes: 26 additions & 2 deletions apps/backend/apps/admin/src/polygon/polygon.resolver.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,34 @@
import { Resolver } from '@nestjs/graphql'
import { Args, Int, Mutation, Resolver } from '@nestjs/graphql'
import { ToolType } from '@prisma/client'
import GraphQLUpload from 'graphql-upload/GraphQLUpload.mjs'
import type { FileUpload } from 'graphql-upload/processRequest.mjs'
import { UseDisableAdminGuard } from '@libs/auth'
import { PolygonProblem } from '@admin/@generated'
import { PolygonProblem, PolygonTool } from '@admin/@generated'
import { PolygonService } from './polygon.service'

@Resolver(() => PolygonProblem)
@UseDisableAdminGuard()
export class PolygonResolver {
constructor(private readonly polygonService: PolygonService) {}

@Mutation(() => PolygonTool)
async uploadPolygonTool(
@Args('problemId', { type: () => Int }) problemId: number,
@Args('toolType', { type: () => ToolType }) toolType: ToolType,
@Args('file', { type: () => GraphQLUpload }) file: Promise<FileUpload>
) {
return this.polygonService.uploadPolygonTool(
problemId,
toolType,
await file
)
}

@Mutation(() => PolygonTool)
async deletePolygonTool(
@Args('problemId', { type: () => Int }) problemId: number,
@Args('toolType', { type: () => ToolType }) toolType: ToolType
) {
return this.polygonService.deletePolygonTool(problemId, toolType)
}
}
35 changes: 34 additions & 1 deletion apps/backend/apps/admin/src/polygon/polygon.service.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,40 @@
import { Injectable } from '@nestjs/common'
import { ToolType } from '@prisma/client'
import type { FileUpload } from 'graphql-upload/processRequest.mjs'
import { PolygonAMQPService } from '@libs/amqp'
import { PrismaService } from '@libs/prisma'
import { FileService } from './file/file.service'

@Injectable()
export class PolygonService {
constructor(private readonly prisma: PrismaService) {}
constructor(
private readonly prisma: PrismaService,
private readonly fileService: FileService,
private readonly polygonAMQPService: PolygonAMQPService
) {}

async uploadPolygonTool(
problemId: number,
toolType: ToolType,
file: FileUpload
) {
//DB에 파일 저장
const uploadedTool = await this.fileService.uploadPolygonToolFile(
problemId,
toolType,
file
)

//RabbitMQ 메시지 발행
await this.polygonAMQPService.publishPolygonToolUploadMessage(
uploadedTool.problemId,
uploadedTool.toolType
)

return uploadedTool
}

async deletePolygonTool(problemId: number, toolType: ToolType) {
return this.fileService.deletePolygonFile(problemId, toolType)
}
}
10 changes: 7 additions & 3 deletions apps/backend/libs/amqp/src/amqp.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import { Module } from '@nestjs/common'
import { ConfigModule, ConfigService } from '@nestjs/config'
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq'
import { CONSUME_CHANNEL, PUBLISH_CHANNEL } from '@libs/constants'
import { CheckAMQPService, JudgeAMQPService } from './amqp.service'
import {
CheckAMQPService,
JudgeAMQPService,
PolygonAMQPService
Comment thread
zero1177 marked this conversation as resolved.
} from './amqp.service'

@Module({
imports: [
Expand Down Expand Up @@ -41,7 +45,7 @@ import { CheckAMQPService, JudgeAMQPService } from './amqp.service'
inject: [ConfigService]
})
],
providers: [JudgeAMQPService, CheckAMQPService],
exports: [JudgeAMQPService, CheckAMQPService]
providers: [JudgeAMQPService, CheckAMQPService, PolygonAMQPService],
exports: [JudgeAMQPService, CheckAMQPService, PolygonAMQPService]
})
export class AMQPModule {}
10 changes: 10 additions & 0 deletions apps/backend/libs/constants/src/rabbitmq.constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,13 @@ export const CHECK_RESULT_KEY = 'check.result'
export const CHECK_RESULT_QUEUE = 'plag.q.check.result'

export const CHECK_MESSAGE_TYPE = 'check'

/**
* Polygon Tool 업로드
*/

export const POLYGON_EXCHANGE = 'iris.e.direct.polygon'

export const POLYGON_TOOL_KEY = 'polygon.tool'

export const POLYGON_TOOL_MESSAGE_TYPE = 'polygonTool'
6 changes: 3 additions & 3 deletions apps/backend/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -1136,9 +1136,9 @@ model PolygonTool {
problem PolygonProblem @relation(fields: [problemId], references: [id], onDelete: Cascade)
problemId Int @map("problem_id")

toolType ToolType @map("tool_type") // 'generator' | 'validator' | 'checker'
fileName String @map("file_name")
filePath String @map("file_path") // S3 key
toolType ToolType @map("tool_type") // 'generator' | 'validator' | 'checker'
fileName String @map("file_name")
fileContent String @map("file_content")

createTime DateTime @default(now()) @map("create_time")
updateTime DateTime @updatedAt @map("update_time")
Expand Down
Loading