From 339ab3d604e59cb55606342d9c990bb40e1f30cf Mon Sep 17 00:00:00 2001 From: TraceofLight Date: Sat, 22 Mar 2025 02:48:42 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=ED=8A=B9=EC=A0=95=20repo=EC=97=90=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EC=97=85=EB=A1=9C=EB=93=9C=20=EA=B8=B0?= =?UTF-8?q?=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 사용자가 작성한 내용을 파일로 받아 이를 github에 commit의 형태로 전달하는 기능 구현 - member controller 쪽 api 호출부 구현 - base64 활용한 인코딩 처리 오브젝트 구현 - github auth 정보와 조합하여 파일 업로드하는 기능 추가 --- .../gitmon/client/github/GithubApiService.kt | 30 ++++++++++++-- .../client/github/GithubResourceApiClient.kt | 18 ++++++++ .../github/request/GithubUpsertFileRequest.kt | 7 ++++ .../response/GithubUpsertFileResponse.kt | 5 +++ .../ixfp/gitmon/common/util/Base64Encoder.kt | 10 +++++ .../gitmon/controller/MemberController.kt | 41 +++++++++++++++++++ 6 files changed, 108 insertions(+), 3 deletions(-) create mode 100644 src/main/kotlin/com/ixfp/gitmon/client/github/request/GithubUpsertFileRequest.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/client/github/response/GithubUpsertFileResponse.kt create mode 100644 src/main/kotlin/com/ixfp/gitmon/common/util/Base64Encoder.kt diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt index 458a9b6..fdf1473 100644 --- a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubApiService.kt @@ -2,10 +2,13 @@ package com.ixfp.gitmon.client.github import com.ixfp.gitmon.client.github.request.GithubAccessTokenRequest import com.ixfp.gitmon.client.github.request.GithubCreateRepositoryRequest +import com.ixfp.gitmon.client.github.request.GithubUpsertFileRequest import com.ixfp.gitmon.client.github.response.GithubUserResponse +import com.ixfp.gitmon.common.util.Base64Encoder import com.ixfp.gitmon.common.util.BearerToken import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Component +import org.springframework.web.multipart.MultipartFile @Component class GithubApiService( @@ -28,9 +31,30 @@ class GithubApiService( return githubOauth2ApiClient.fetchAccessToken(request).accessToken } - // TODO(KHJ): dummy function, 관련 api 나오면 정리할 것 - fun hasRepository(githubAccessToken: String): Boolean { - return false + fun upsertFile( + githubAccessToken: String, + content: MultipartFile, + repo: String, + path: String, + commitMessage: String = "Upsert File by API", + ): String { + val request = + GithubUpsertFileRequest( + message = "Add New File", + content = Base64Encoder.encodeBase64(content), + sha = "", + ) + val owner = "traceoflight" // TODO(KHJ): db에서 owner name 가져오는 부분 대응할 것 + val response = + githubResourceApiClient.upsertFile( + bearerToken = BearerToken(githubAccessToken).format(), + owner = owner, + repo = repo, + path = path, + request = request, + ) + + return response.sha } private fun createRepository( diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubResourceApiClient.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubResourceApiClient.kt index ed2c596..63d532b 100644 --- a/src/main/kotlin/com/ixfp/gitmon/client/github/GithubResourceApiClient.kt +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/GithubResourceApiClient.kt @@ -1,14 +1,17 @@ package com.ixfp.gitmon.client.github import com.ixfp.gitmon.client.github.request.GithubCreateRepositoryRequest +import com.ixfp.gitmon.client.github.request.GithubUpsertFileRequest import com.ixfp.gitmon.client.github.response.GithubCreateRepositoryResponse import com.ixfp.gitmon.client.github.response.GithubFetchRepositoryResponse +import com.ixfp.gitmon.client.github.response.GithubUpsertFileResponse import com.ixfp.gitmon.client.github.response.GithubUserResponse import org.springframework.cloud.openfeign.FeignClient import org.springframework.http.MediaType import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestHeader @@ -47,4 +50,19 @@ interface GithubResourceApiClient { @RequestHeader("Accept") accept: String = "application/vnd.github+json", @RequestHeader("X-GitHub-Api-Version") apiVersion: String = "2022-11-28", ): GithubCreateRepositoryResponse + + @PutMapping( + "/repos/{owner}/{repo}/contents/{path}", + consumes = [MediaType.APPLICATION_JSON_VALUE], + produces = [MediaType.APPLICATION_JSON_VALUE], + ) + fun upsertFile( + @RequestHeader("Authorization") bearerToken: String, + @RequestHeader("Accept") accept: String = "application/vnd.github+json", + @RequestHeader("X-GitHub-Api-Version") apiVersion: String = "2022-11-28", + @PathVariable("owner") owner: String, + @PathVariable("repo") repo: String, + @PathVariable("path") path: String, + @RequestBody request: GithubUpsertFileRequest, + ): GithubUpsertFileResponse } diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/request/GithubUpsertFileRequest.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/request/GithubUpsertFileRequest.kt new file mode 100644 index 0000000..99d947f --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/request/GithubUpsertFileRequest.kt @@ -0,0 +1,7 @@ +package com.ixfp.gitmon.client.github.request + +data class GithubUpsertFileRequest( + val message: String, + val content: String, + val sha: String, +) diff --git a/src/main/kotlin/com/ixfp/gitmon/client/github/response/GithubUpsertFileResponse.kt b/src/main/kotlin/com/ixfp/gitmon/client/github/response/GithubUpsertFileResponse.kt new file mode 100644 index 0000000..8e33bf3 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/client/github/response/GithubUpsertFileResponse.kt @@ -0,0 +1,5 @@ +package com.ixfp.gitmon.client.github.response + +data class GithubUpsertFileResponse( + val sha: String, +) diff --git a/src/main/kotlin/com/ixfp/gitmon/common/util/Base64Encoder.kt b/src/main/kotlin/com/ixfp/gitmon/common/util/Base64Encoder.kt new file mode 100644 index 0000000..7273923 --- /dev/null +++ b/src/main/kotlin/com/ixfp/gitmon/common/util/Base64Encoder.kt @@ -0,0 +1,10 @@ +package com.ixfp.gitmon.common.util + +import org.springframework.web.multipart.MultipartFile +import java.util.Base64 + +object Base64Encoder { + fun encodeBase64(file: MultipartFile): String { + return Base64.getUrlEncoder().encodeToString(file.bytes) + } +} diff --git a/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt b/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt index 8f25062..060f427 100644 --- a/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt +++ b/src/main/kotlin/com/ixfp/gitmon/controller/MemberController.kt @@ -1,5 +1,6 @@ package com.ixfp.gitmon.controller +import com.ixfp.gitmon.client.github.GithubApiService import com.ixfp.gitmon.common.util.JwtUtil import com.ixfp.gitmon.controller.request.CreateRepoRequest import com.ixfp.gitmon.domain.auth.AuthService @@ -13,11 +14,14 @@ import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PostMapping +import org.springframework.web.bind.annotation.PutMapping import org.springframework.web.bind.annotation.RequestBody import org.springframework.web.bind.annotation.RequestHeader import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RequestPart import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile import javax.naming.AuthenticationException @RequestMapping("/api/v1/member") @@ -27,6 +31,7 @@ class MemberController( private val jwtUtil: JwtUtil, private val authService: AuthService, private val memberService: MemberService, + private val githubApiService: GithubApiService, ) { @Operation( summary = "레포지토리 생성/갱신", @@ -137,6 +142,42 @@ class MemberController( } } + @Operation( + summary = "레포지토리 파일 추가", + description = "특정 파일의 base64 인코딩된 내용을 기반으로 파일을 repo에 추가합니다.", + ) + @ApiResponses( + value = [ + ApiResponse(responseCode = "200", description = "파일 업로드 성공"), + ApiResponse(responseCode = "400", description = "잘못된 요청"), + ApiResponse(responseCode = "401", description = "인증 실패 (엑세스 토큰 문제)"), + ApiResponse(responseCode = "500", description = "서버 내부 오류"), + ], + ) + @PutMapping("/repo/upsert_file") + fun upsertFile( + @RequestPart file: MultipartFile, + @RequestParam repo: String, + @RequestParam path: String, + @RequestHeader("Authorization") authorizationHeader: String, + ): ResponseEntity { + return try { + val accessToken = authorizationHeader.removePrefix("Bearer ") + // TODO(KHJ): 여기 에러 핸들링할 것 + githubApiService.upsertFile(accessToken, file, repo, path) + ResponseEntity.ok().build() + } catch (e: Exception) { + log.error(e) { "Failed to upsert file: ${e.message}" } + val status = + when (e) { + is IllegalArgumentException -> HttpStatus.BAD_REQUEST + is AuthenticationException -> HttpStatus.UNAUTHORIZED + else -> HttpStatus.INTERNAL_SERVER_ERROR + } + ResponseEntity.status(status).build() + } + } + companion object { private val log = logger {} }