From 9cca0b410892d570a1c6a89bf375dd2a6c426b78 Mon Sep 17 00:00:00 2001 From: bjkim Date: Mon, 27 Oct 2025 19:26:24 +0900 Subject: [PATCH] =?UTF-8?q?[ADD]=20MlflowController=EC=97=90=20Artifact=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=A1=B0=ED=9A=8C=20API=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80,=20MinioAttachmentService=20=ED=8C=8C=EC=9D=BC=20?= =?UTF-8?q?=EA=B2=BD=EB=A1=9C=20=EC=B2=98=EB=A6=AC=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0=20=EB=B0=8F=20=EC=98=88=EC=99=B8=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DynamicMinioAttachmentController.java | 2 +- .../controllers/MlflowController.java | 41 +++++++++++++++++++ .../DynamicMinioAttachmentService.java | 36 ++++++++++------ 3 files changed, 66 insertions(+), 13 deletions(-) diff --git a/src/main/java/kr/re/etri/autoflow/controllers/DynamicMinioAttachmentController.java b/src/main/java/kr/re/etri/autoflow/controllers/DynamicMinioAttachmentController.java index d376dc2..0dc2af3 100644 --- a/src/main/java/kr/re/etri/autoflow/controllers/DynamicMinioAttachmentController.java +++ b/src/main/java/kr/re/etri/autoflow/controllers/DynamicMinioAttachmentController.java @@ -63,7 +63,7 @@ public class DynamicMinioAttachmentController { */ @GetMapping("/download") public ResponseEntity downloadFile( - @RequestParam String objectName, + @RequestParam(defaultValue = "4/9d08fa7973cf4c39a0979bb4d70c640b/artifacts/sklearn-model/model.pkl") String objectName, @RequestParam(defaultValue = "type1") String type ) { try { diff --git a/src/main/java/kr/re/etri/autoflow/controllers/MlflowController.java b/src/main/java/kr/re/etri/autoflow/controllers/MlflowController.java index 4e118f8..9466545 100644 --- a/src/main/java/kr/re/etri/autoflow/controllers/MlflowController.java +++ b/src/main/java/kr/re/etri/autoflow/controllers/MlflowController.java @@ -108,4 +108,45 @@ public class MlflowController { .map(ResponseEntity::ok) .onErrorResume(e -> Mono.just(ResponseEntity.internalServerError().body(e.getMessage()))); } + + @Operation( + summary = "Run의 Artifact 목록 조회", + description = """ + 주어진 Run ID의 Artifact 목록을 조회합니다. + MLflow API `/artifacts/list`를 호출하며, `path`를 지정하면 해당 하위 디렉터리의 Artifact만 반환합니다. + """, + responses = { + @ApiResponse(responseCode = "200", description = "Artifact 목록 조회 성공"), + @ApiResponse(responseCode = "404", description = "Run을 찾을 수 없음"), + @ApiResponse(responseCode = "500", description = "서버 오류 발생") + } + ) + @GetMapping(value = "/artifacts", produces = MediaType.APPLICATION_JSON_VALUE) + public Mono> listArtifacts( + @Parameter(description = "조회할 Run ID", required = true, example = "9d08fa7973cf4c39a0979bb4d70c640b") + @RequestParam String runId, + + @Parameter(description = "조회할 경로 (선택)", example = "models") + @RequestParam(required = false) String path, + + @Parameter(description = "페이징 토큰 (선택)", example = "MjAyNS0xMC0yN1QxMjo0NjozMlo=") + @RequestParam(required = false, name = "page_token") String pageToken + ) { + return webClient.get() + .uri(uriBuilder -> { + var builder = uriBuilder.path("/artifacts/list") + .queryParam("run_id", runId); + if (path != null && !path.isBlank()) { + builder.queryParam("path", path); + } + if (pageToken != null && !pageToken.isBlank()) { + builder.queryParam("page_token", pageToken); + } + return builder.build(); + }) + .retrieve() + .bodyToMono(String.class) + .map(ResponseEntity::ok) + .onErrorResume(e -> Mono.just(ResponseEntity.internalServerError().body(e.getMessage()))); + } } diff --git a/src/main/java/kr/re/etri/autoflow/service/DynamicMinioAttachmentService.java b/src/main/java/kr/re/etri/autoflow/service/DynamicMinioAttachmentService.java index 23be7ff..30978f5 100644 --- a/src/main/java/kr/re/etri/autoflow/service/DynamicMinioAttachmentService.java +++ b/src/main/java/kr/re/etri/autoflow/service/DynamicMinioAttachmentService.java @@ -107,25 +107,37 @@ public class DynamicMinioAttachmentService { MinioClient client = getClientByType(type); String bucketName = getBucketByType(type); - try (InputStream is = client.getObject( - GetObjectArgs.builder().bucket(bucketName).object(objectName).build() - )) { - return is.readAllBytes(); + try { + // 1. mlflow-artifacts:/ 접두어 제거 + String cleanObjectName = objectName.replaceFirst("^mlflow-artifacts:/", ""); + + // 2. 잘못된 슬래시 제거 + cleanObjectName = cleanObjectName.replaceAll("^/+", "").replaceAll("/+$", ""); + + // 3. 파일 확장자가 없는 경우 (디렉터리 요청으로 추정) + // → MLflow 구조상 실제 파일은 artifacts/ 하위에 있으므로 경로 자동 보정 + if (!cleanObjectName.matches(".*\\.[a-zA-Z0-9]+$")) { + throw new RuntimeException("요청된 객체가 파일이 아닙니다. 실제 파일 경로를 포함해야 합니다: " + cleanObjectName); + } + + try (InputStream is = client.getObject( + GetObjectArgs.builder() + .bucket(bucketName) + .object(cleanObjectName) + .build() + )) { + return is.readAllBytes(); + } + } catch (io.minio.errors.ErrorResponseException e) { throw new RuntimeException( "MinIO 서버가 요청을 거부했습니다: " + objectName + ", 코드=" + e.errorResponse().code() + + ", 버킷이름=" + bucketName + ", 메시지=" + e.errorResponse().message() + ", 요청ID=" + e.errorResponse().requestId() + ", 호스트ID=" + e.errorResponse().hostId(), e); - } catch (io.minio.errors.ServerException e) { - throw new RuntimeException("MinIO 서버 오류 발생: " + objectName, e); - } catch (io.minio.errors.InsufficientDataException e) { - throw new RuntimeException("MinIO 데이터 부족 오류: " + objectName, e); - } catch (io.minio.errors.InternalException e) { - throw new RuntimeException("MinIO 내부 오류: " + objectName, e); - } catch (io.minio.errors.InvalidResponseException e) { - throw new RuntimeException("MinIO 응답 파싱 실패: " + objectName, e); + } catch (Exception e) { throw new RuntimeException("MinIO 파일 다운로드 실패: " + objectName, e); }