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); }