diff --git a/README.md b/README.md index 89d8c0c..146c9f0 100644 --- a/README.md +++ b/README.md @@ -23,9 +23,9 @@ - 워크플로우 생성 및 실행 관리 ### 4. 파일 관리 (File Management) -- **MinIO** 및 **AWS S3** 연동을 통한 대용량 파일 저장 및 관리 +- **AWS S3** 연동을 통한 대용량 파일 저장 및 관리 - 멀티파트(Multipart) 파일 업로드 지원 (최대 500MB) -- 동적 MinIO 첨부 파일 관리 시스템 +- 동적 AWS S3 첨부 파일 관리 시스템 ### 5. 외부 시스템 연동 (External Integrations) - **OTA (Over-The-Air)** 연동: 외부 인증 및 패키지 검색 API 연동 지원 @@ -40,7 +40,7 @@ - **Build Tool:** Gradle - **Database:** MariaDB (JPA / Hibernate) - **Security:** Spring Security, JWT (jjwt 0.11.5) -- **Storage:** MinIO, AWS S3 +- **Storage:** AWS S3 - **Batch Processing:** Spring Batch - **API Documentation:** Springdoc OpenAPI (Swagger UI) - **Etc:** Lombok, Jsoup, Caffeine Cache, WebFlux @@ -52,7 +52,7 @@ ### 사전 요구 사항 - Java 17 이상 설치 - MariaDB 설치 및 데이터베이스 생성 (`autoflow`) -- MinIO 또는 S3 접근 권한 필요 +- AWS S3 접근 권한 필요 ### 환경 설정 (`application.properties`) `src/main/resources/application.properties` 파일에서 다음 항목들을 설정해야 합니다: @@ -66,10 +66,11 @@ spring.datasource.password={PASSWORD} # JWT 보안 설정 cuuva.app.jwtSecret={YOUR_SECRET_KEY} -# MinIO 설정 -minio.endpoint={MINIO_ENDPOINT} -minio.access-key={ACCESS_KEY} -minio.secret-key={SECRET_KEY} +# AWS S3 설정 +cloud.aws.s3.endpoint={AWS_S3_ENDPOINT} +cloud.aws.credentials.access-key={AWS_ACCESS_KEY} +cloud.aws.credentials.secret-key={AWS_SECRET_KEY} +cloud.aws.region.static={AWS_REGION} # 외부 서비스 연동 kubeflow.url={KUBEFLOW_URL} diff --git a/src/main/java/kr/re/etri/autoflow/common/MinIOConfig.java b/src/main/java/kr/re/etri/autoflow/common/StorageConfig.java similarity index 75% rename from src/main/java/kr/re/etri/autoflow/common/MinIOConfig.java rename to src/main/java/kr/re/etri/autoflow/common/StorageConfig.java index 3c01687..20f9b41 100644 --- a/src/main/java/kr/re/etri/autoflow/common/MinIOConfig.java +++ b/src/main/java/kr/re/etri/autoflow/common/StorageConfig.java @@ -2,11 +2,13 @@ package kr.re.etri.autoflow.common; import io.minio.MinioClient; import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration -public class MinIOConfig { +public class StorageConfig { + @Value("${minio.endpoint}") private String endpoint; @@ -17,11 +19,11 @@ public class MinIOConfig { private String secretKey; @Bean + @ConditionalOnProperty(name = "storage.provider", havingValue = "minio", matchIfMissing = true) public MinioClient minioClient() { return MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); } - } diff --git a/src/main/java/kr/re/etri/autoflow/controllers/DynamicMinioAttachmentController.java b/src/main/java/kr/re/etri/autoflow/controllers/DynamicStorageAttachmentController.java similarity index 90% rename from src/main/java/kr/re/etri/autoflow/controllers/DynamicMinioAttachmentController.java rename to src/main/java/kr/re/etri/autoflow/controllers/DynamicStorageAttachmentController.java index 031c011..e38cf5b 100644 --- a/src/main/java/kr/re/etri/autoflow/controllers/DynamicMinioAttachmentController.java +++ b/src/main/java/kr/re/etri/autoflow/controllers/DynamicStorageAttachmentController.java @@ -3,9 +3,9 @@ package kr.re.etri.autoflow.controllers; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; -import kr.re.etri.autoflow.entity.MinioAttachmentEntity; +import kr.re.etri.autoflow.entity.StorageAttachmentEntity; import kr.re.etri.autoflow.payload.request.ProjectBaseSearchRequest; -import kr.re.etri.autoflow.service.DynamicMinioAttachmentService; +import kr.re.etri.autoflow.service.DynamicStorageAttachmentService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.domain.Page; @@ -27,9 +27,9 @@ import java.util.Map; @RestController @RequestMapping("/api/minio") @RequiredArgsConstructor -public class DynamicMinioAttachmentController { +public class DynamicStorageAttachmentController { - private final DynamicMinioAttachmentService minioService; + private final DynamicStorageAttachmentService minioService; /** * 파일 업로드 @@ -48,7 +48,7 @@ public class DynamicMinioAttachmentController { @RequestParam(defaultValue = "type1") String type ) { try { - MinioAttachmentEntity attachment = minioService.uploadFile( + StorageAttachmentEntity attachment = minioService.uploadFile( file, path, refId, refType, title, description, version, regUserId, projectId, type ); @@ -149,8 +149,8 @@ public class DynamicMinioAttachmentController { * 전체 조회 */ @GetMapping("/list") - public ResponseEntity> listAll() { - List list = minioService.findAll(); + public ResponseEntity> listAll() { + List list = minioService.findAll(); return ResponseEntity.ok(list); } @@ -158,12 +158,12 @@ public class DynamicMinioAttachmentController { * 검색 + 페이지네이션 */ @PostMapping("/search") - public ResponseEntity> search( + public ResponseEntity> search( @RequestBody ProjectBaseSearchRequest request, @RequestParam String refType, @RequestParam Integer refId ) { - Page page = minioService.search(request, refType, refId); + Page page = minioService.search(request, refType, refId); return ResponseEntity.ok(page); } } diff --git a/src/main/java/kr/re/etri/autoflow/controllers/ExternalAuthController.java b/src/main/java/kr/re/etri/autoflow/controllers/ExternalAuthController.java index a9ad2bc..244e423 100644 --- a/src/main/java/kr/re/etri/autoflow/controllers/ExternalAuthController.java +++ b/src/main/java/kr/re/etri/autoflow/controllers/ExternalAuthController.java @@ -9,7 +9,7 @@ import io.swagger.v3.oas.annotations.security.SecurityRequirement; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.PostConstruct; import kr.re.etri.autoflow.payload.request.EdgeSWVO; -import kr.re.etri.autoflow.service.DynamicMinioAttachmentService; +import kr.re.etri.autoflow.service.DynamicStorageAttachmentService; import kr.re.etri.autoflow.service.EdgeSWUploadService; import kr.re.etri.autoflow.service.ExternalAuthService; import lombok.RequiredArgsConstructor; @@ -45,7 +45,7 @@ public class ExternalAuthController { private final ExternalAuthService externalAuthService; private final EdgeSWUploadService edgeSWUploadService; - private final DynamicMinioAttachmentService minioService; + private final DynamicStorageAttachmentService minioService; private RestTemplate restTemplate; diff --git a/src/main/java/kr/re/etri/autoflow/controllers/ExternalDataSetController.java b/src/main/java/kr/re/etri/autoflow/controllers/ExternalDataSetController.java index fd880e4..fd7f794 100644 --- a/src/main/java/kr/re/etri/autoflow/controllers/ExternalDataSetController.java +++ b/src/main/java/kr/re/etri/autoflow/controllers/ExternalDataSetController.java @@ -7,7 +7,7 @@ import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; -import kr.re.etri.autoflow.entity.MinioAttachmentEntity; +import kr.re.etri.autoflow.entity.StorageAttachmentEntity; import kr.re.etri.autoflow.service.DatasetService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -26,7 +26,7 @@ import java.util.Map; public class ExternalDataSetController { private final DatasetService datasetService; - private final kr.re.etri.autoflow.service.MinioAttachmentService minioAttachmentService; + private final kr.re.etri.autoflow.service.StorageAttachmentService storageAttachmentService; @Operation( summary = "데이터셋 목록 조회", @@ -64,13 +64,13 @@ public class ExternalDataSetController { @RequestParam Long projectId ) { try { - MinioAttachmentEntity saved = datasetService.downloadDataset( + StorageAttachmentEntity saved = datasetService.downloadDataset( datasetName, path, refId, refType, title, description, version, regUserId, projectId ); Map response = new HashMap<>(); response.put("attachment", saved); - response.put("minioUrl", minioAttachmentService.getFileUrl(saved.getStoragePath())); + response.put("minioUrl", storageAttachmentService.getFileUrl(saved.getStoragePath())); return ResponseEntity.ok(response); } catch (Exception e) { diff --git a/src/main/java/kr/re/etri/autoflow/controllers/PipelineUploadController.java b/src/main/java/kr/re/etri/autoflow/controllers/PipelineUploadController.java index 1fdef87..d63f289 100644 --- a/src/main/java/kr/re/etri/autoflow/controllers/PipelineUploadController.java +++ b/src/main/java/kr/re/etri/autoflow/controllers/PipelineUploadController.java @@ -6,10 +6,10 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.media.Content; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; -import kr.re.etri.autoflow.entity.MinioAttachmentEntity; +import kr.re.etri.autoflow.entity.StorageAttachmentEntity; import kr.re.etri.autoflow.entity.WorkflowEntity; import kr.re.etri.autoflow.payload.request.CreateRunRequest; -import kr.re.etri.autoflow.service.MinioAttachmentService; +import kr.re.etri.autoflow.service.StorageAttachmentService; import kr.re.etri.autoflow.service.PipelineUploadService; import kr.re.etri.autoflow.service.WorkFlowService; import lombok.RequiredArgsConstructor; @@ -36,7 +36,7 @@ public class PipelineUploadController { private final PipelineUploadService pipelineUploadService; private final WorkFlowService workFlowService; - private final MinioAttachmentService minioAttachmentService; + private final StorageAttachmentService storageAttachmentService; @PostMapping(value = "/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE) public ResponseEntity> uploadPipeline( @@ -73,7 +73,7 @@ public class PipelineUploadController { workFlowService.save(workflow); // 2. MinIO 업로드 - MinioAttachmentEntity attachment = minioAttachmentService.uploadFile( + StorageAttachmentEntity attachment = storageAttachmentService.uploadFile( file, "workflows/" + projectId, workflow.getId(), @@ -85,7 +85,7 @@ public class PipelineUploadController { projectId ); - String minioUrl = minioAttachmentService.getFileUrl(attachment.getStoragePath()); + String minioUrl = storageAttachmentService.getFileUrl(attachment.getStoragePath()); // 3. 최종 응답 Map response = new HashMap<>(); diff --git a/src/main/java/kr/re/etri/autoflow/controllers/MinioAttachmentController.java b/src/main/java/kr/re/etri/autoflow/controllers/StorageAttachmentController.java similarity index 87% rename from src/main/java/kr/re/etri/autoflow/controllers/MinioAttachmentController.java rename to src/main/java/kr/re/etri/autoflow/controllers/StorageAttachmentController.java index 6921b78..66576e6 100644 --- a/src/main/java/kr/re/etri/autoflow/controllers/MinioAttachmentController.java +++ b/src/main/java/kr/re/etri/autoflow/controllers/StorageAttachmentController.java @@ -8,10 +8,10 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.servlet.ServletOutputStream; import jakarta.servlet.http.HttpServletResponse; -import kr.re.etri.autoflow.entity.MinioAttachmentEntity; +import kr.re.etri.autoflow.entity.StorageAttachmentEntity; import kr.re.etri.autoflow.payload.request.ProjectBaseSearchRequest; -import kr.re.etri.autoflow.service.MinioAttachmentService; +import kr.re.etri.autoflow.service.StorageAttachmentService; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springdoc.core.annotations.ParameterObject; @@ -39,31 +39,31 @@ import java.util.*; @RequiredArgsConstructor @Slf4j @Tag(name = "첨부파일", description = "MinIO 첨부파일 관리 API") -public class MinioAttachmentController { +public class StorageAttachmentController { - private final MinioAttachmentService minioAttachmentService; + private final StorageAttachmentService storageAttachmentService; private final MinioClient minioClient; @Operation(summary = "첨부파일 전체 조회") @GetMapping - public ResponseEntity> getAll() { - return ResponseEntity.ok(minioAttachmentService.findAll()); + public ResponseEntity> getAll() { + return ResponseEntity.ok(storageAttachmentService.findAll()); } @Operation(summary = "ID로 첨부파일 조회") @GetMapping("/{id}") - public ResponseEntity getById( + public ResponseEntity getById( @Parameter(description = "첨부파일 ID", required = true) @PathVariable("id") Long id) { - return minioAttachmentService.findById(id) + return storageAttachmentService.findById(id) .map(ResponseEntity::ok) .orElse(ResponseEntity.notFound().build()); } @Operation(summary = "검색 및 페이지네이션 첨부파일 목록 조회") @GetMapping("/search") - public ResponseEntity> search( + public ResponseEntity> search( @ParameterObject @ModelAttribute ProjectBaseSearchRequest request, @Parameter( description = "첨부파일 구분자. 예: WORKFLOW_STEP, DATASET, TRAINING_SCRIPT", @@ -72,7 +72,7 @@ public class MinioAttachmentController { @RequestParam(value = "refType", required = false) String refType, @RequestParam(value = "refId", required = false, defaultValue = "0") Integer refId ) { - Page page = minioAttachmentService.search(request, refType, refId); + Page page = storageAttachmentService.search(request, refType, refId); return ResponseEntity.ok(page); } @@ -81,7 +81,7 @@ public class MinioAttachmentController { public ResponseEntity delete( @Parameter(description = "첨부파일 ID", required = true) @PathVariable("id") Long id) { - if (minioAttachmentService.delete(id)) { + if (storageAttachmentService.delete(id)) { return ResponseEntity.noContent().build(); } return ResponseEntity.notFound().build(); @@ -91,7 +91,7 @@ public class MinioAttachmentController { @GetMapping("/download") public ResponseEntity downloadFile(@RequestParam String objectName) { try { - byte[] bytes = minioAttachmentService.downloadFile("mlpipeline", objectName); + byte[] bytes = storageAttachmentService.downloadFile("mlpipeline", objectName); String encodedFileName = URLEncoder.encode(objectName, StandardCharsets.UTF_8) .replaceAll("\\+", "%20"); // 공백 처리 @@ -139,7 +139,7 @@ public class MinioAttachmentController { @GetMapping(value = "/readYamlText", produces = MediaType.TEXT_PLAIN_VALUE) public ResponseEntity readYamlTextFromMinio(@RequestParam String objectName) { try { - String content = minioAttachmentService.readYamlText("mlpipeline", objectName); + String content = storageAttachmentService.readYamlText("mlpipeline", objectName); return ResponseEntity.ok(content); } catch (Exception e) { log.error("MinIO 파일 읽기 실패: {}", objectName, e); @@ -163,13 +163,13 @@ public class MinioAttachmentController { ) { try { - MinioAttachmentEntity saved = minioAttachmentService.uploadFile( + StorageAttachmentEntity saved = storageAttachmentService.uploadFile( file, path, refId, refType, title, description, version, regUserId, projectId ); Map response = new HashMap<>(); response.put("attachment", saved); - response.put("minioUrl", minioAttachmentService.getFileUrl(saved.getStoragePath())); + response.put("minioUrl", storageAttachmentService.getFileUrl(saved.getStoragePath())); return ResponseEntity.ok(response); @@ -193,13 +193,13 @@ public class MinioAttachmentController { @RequestParam(value = "regUserId") String regUserId ) { try { - MinioAttachmentEntity updated = minioAttachmentService.updateFile( + StorageAttachmentEntity updated = storageAttachmentService.updateFile( id, projectId, file, path, title, description, regUserId ); Map response = new HashMap<>(); response.put("attachment", updated); - response.put("minioUrl", minioAttachmentService.getFileUrl(updated.getStoragePath())); + response.put("minioUrl", storageAttachmentService.getFileUrl(updated.getStoragePath())); return ResponseEntity.ok(response); diff --git a/src/main/java/kr/re/etri/autoflow/entity/MinioAttachmentEntity.java b/src/main/java/kr/re/etri/autoflow/entity/StorageAttachmentEntity.java similarity index 98% rename from src/main/java/kr/re/etri/autoflow/entity/MinioAttachmentEntity.java rename to src/main/java/kr/re/etri/autoflow/entity/StorageAttachmentEntity.java index b043c61..a05299e 100644 --- a/src/main/java/kr/re/etri/autoflow/entity/MinioAttachmentEntity.java +++ b/src/main/java/kr/re/etri/autoflow/entity/StorageAttachmentEntity.java @@ -19,7 +19,7 @@ import java.time.LocalDateTime; @NoArgsConstructor @AllArgsConstructor @Builder -public class MinioAttachmentEntity { +public class StorageAttachmentEntity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) diff --git a/src/main/java/kr/re/etri/autoflow/repository/MinioAttachmentRepository.java b/src/main/java/kr/re/etri/autoflow/repository/MinioAttachmentRepository.java deleted file mode 100644 index 734f328..0000000 --- a/src/main/java/kr/re/etri/autoflow/repository/MinioAttachmentRepository.java +++ /dev/null @@ -1,16 +0,0 @@ -package kr.re.etri.autoflow.repository; - -import kr.re.etri.autoflow.entity.MinioAttachmentEntity; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.JpaSpecificationExecutor; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -@Repository -public interface MinioAttachmentRepository extends JpaRepository, JpaSpecificationExecutor { - //최신버전 파일 가져오기 - Optional findTopByRefIdAndRefTypeOrderByVersionDesc(Long refId, String refType); - List findAllByRefId(Long refId); -} diff --git a/src/main/java/kr/re/etri/autoflow/repository/StorageAttachmentRepository.java b/src/main/java/kr/re/etri/autoflow/repository/StorageAttachmentRepository.java new file mode 100644 index 0000000..547bb29 --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/repository/StorageAttachmentRepository.java @@ -0,0 +1,16 @@ +package kr.re.etri.autoflow.repository; + +import kr.re.etri.autoflow.entity.StorageAttachmentEntity; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Optional; + +@Repository +public interface StorageAttachmentRepository extends JpaRepository, JpaSpecificationExecutor { + //최신버전 파일 가져오기 + Optional findTopByRefIdAndRefTypeOrderByVersionDesc(Long refId, String refType); + List findAllByRefId(Long refId); +} diff --git a/src/main/java/kr/re/etri/autoflow/service/DataGroupService.java b/src/main/java/kr/re/etri/autoflow/service/DataGroupService.java index ae2c53d..3a0e2c7 100644 --- a/src/main/java/kr/re/etri/autoflow/service/DataGroupService.java +++ b/src/main/java/kr/re/etri/autoflow/service/DataGroupService.java @@ -3,12 +3,12 @@ package kr.re.etri.autoflow.service; import io.minio.MinioClient; import io.minio.RemoveObjectArgs; import kr.re.etri.autoflow.entity.DataGroupEntity; -import kr.re.etri.autoflow.entity.MinioAttachmentEntity; +import kr.re.etri.autoflow.entity.StorageAttachmentEntity; import kr.re.etri.autoflow.payload.request.ProjectBaseAndRefTypeRequest; import kr.re.etri.autoflow.payload.request.ProjectBaseSearchRequest; import kr.re.etri.autoflow.payload.request.ProjectRequest; import kr.re.etri.autoflow.repository.DataGroupRepository; -import kr.re.etri.autoflow.repository.MinioAttachmentRepository; +import kr.re.etri.autoflow.repository.StorageAttachmentRepository; import kr.re.etri.autoflow.specification.DataGroupSpecification; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -36,7 +36,7 @@ public class DataGroupService { private final DataGroupRepository dataGroupRepository; - private final MinioAttachmentRepository minioAttachmentRepository; + private final StorageAttachmentRepository storageAttachmentRepository; private final DataGroupSpecification dataGroupSpecification; @@ -133,11 +133,11 @@ public class DataGroupService { } // 2. refId 기준으로 MinIO 첨부파일 조회 - List attachments = - minioAttachmentRepository.findAllByRefId(dataGroupId); + List attachments = + storageAttachmentRepository.findAllByRefId(dataGroupId); // 3. MinIO에서 파일 삭제 - for (MinioAttachmentEntity attachment : attachments) { + for (StorageAttachmentEntity attachment : attachments) { try { minioClient.removeObject( RemoveObjectArgs.builder() @@ -146,7 +146,7 @@ public class DataGroupService { .build() ); // DB에서도 첨부파일 삭제 - minioAttachmentRepository.delete(attachment); + storageAttachmentRepository.delete(attachment); } catch (Exception e) { log.error("MinIO 파일 삭제 실패: {}", attachment.getStoragePath(), e); } diff --git a/src/main/java/kr/re/etri/autoflow/service/DatasetService.java b/src/main/java/kr/re/etri/autoflow/service/DatasetService.java index af1a52b..f98bc3f 100644 --- a/src/main/java/kr/re/etri/autoflow/service/DatasetService.java +++ b/src/main/java/kr/re/etri/autoflow/service/DatasetService.java @@ -2,8 +2,8 @@ package kr.re.etri.autoflow.service; import io.minio.MinioClient; import io.minio.PutObjectArgs; -import kr.re.etri.autoflow.entity.MinioAttachmentEntity; -import kr.re.etri.autoflow.repository.MinioAttachmentRepository; +import kr.re.etri.autoflow.entity.StorageAttachmentEntity; +import kr.re.etri.autoflow.repository.StorageAttachmentRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; @@ -33,7 +33,7 @@ public class DatasetService { private final RestTemplate restTemplate; - private final MinioAttachmentRepository minioAttachmentRepository; + private final StorageAttachmentRepository storageAttachmentRepository; private static final String BASE_URL = "http://52.14.11.43:18010"; @@ -111,7 +111,7 @@ public class DatasetService { .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE) .build(); - public MinioAttachmentEntity downloadDataset( + public StorageAttachmentEntity downloadDataset( String datasetName, String path, Long refId, @@ -195,7 +195,7 @@ public class DatasetService { latch.await(); // DB 저장 시 size 컬럼 필수 - MinioAttachmentEntity attachment = MinioAttachmentEntity.builder() + StorageAttachmentEntity attachment = StorageAttachmentEntity.builder() .refId(refId) .refType(refType) .originalName(datasetName + ".zip") @@ -210,7 +210,7 @@ public class DatasetService { .size(totalBytes[0]) .build(); - return minioAttachmentRepository.save(attachment); + return storageAttachmentRepository.save(attachment); } catch (Exception e) { log.error("외부 API 다운로드 및 MinIO 업로드 실패", e); diff --git a/src/main/java/kr/re/etri/autoflow/service/DynamicMinioAttachmentService.java b/src/main/java/kr/re/etri/autoflow/service/DynamicMinioAttachmentService.java deleted file mode 100644 index c6af625..0000000 --- a/src/main/java/kr/re/etri/autoflow/service/DynamicMinioAttachmentService.java +++ /dev/null @@ -1,285 +0,0 @@ -package kr.re.etri.autoflow.service; - -import io.minio.GetObjectArgs; -import io.minio.MinioClient; -import io.minio.PutObjectArgs; -import io.minio.RemoveObjectArgs; -import jakarta.transaction.Transactional; -import kr.re.etri.autoflow.common.MinioType; -import kr.re.etri.autoflow.entity.MinioAttachmentEntity; -import kr.re.etri.autoflow.payload.request.ProjectBaseSearchRequest; -import kr.re.etri.autoflow.repository.MinioAttachmentRepository; -import kr.re.etri.autoflow.specification.MinioAttachmentSpecification; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.BeanUtils; -import org.springframework.data.domain.*; -import org.springframework.data.jpa.domain.Specification; -import org.springframework.stereotype.Service; -import org.springframework.web.multipart.MultipartFile; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.InputStream; -import java.nio.charset.StandardCharsets; -import java.nio.file.Paths; -import java.time.LocalDate; -import java.time.format.DateTimeFormatter; -import java.time.format.DateTimeParseException; -import java.util.List; -import java.util.Optional; -import java.util.UUID; - -@Slf4j -@Service -@RequiredArgsConstructor -@Transactional -public class DynamicMinioAttachmentService { - - private final MinioAttachmentRepository minioAttachmentRepository; - private final MinioAttachmentSpecification minioAttachmentSpecification; - private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); - - /** MinioClient 생성 (type 기반) */ - private MinioClient getClientByType(String type) { - MinioType config = MinioType.of(type); - return MinioClient.builder() - .endpoint(config.getEndpoint()) - .credentials(config.getAccessKey(), config.getSecretKey()) - .build(); - } - - /** Bucket 조회 (type 기반) */ - private String getBucketByType(String type) { - return MinioType.of(type).getBucket(); - } - - /** 파일 업로드 & DB 저장 */ - public MinioAttachmentEntity uploadFile(MultipartFile file, - String path, - Long refId, - String refType, - String title, - String description, - Integer version, - String regUserId, - Long projectId, - String type - ) throws Exception { - MinioClient client = getClientByType(type); - String bucketName = getBucketByType(type); - - try (InputStream is = file.getInputStream()) { - String storedName = UUID.randomUUID() + "-" + file.getOriginalFilename(); - String objectName = (path == null || path.isEmpty()) - ? storedName - : path + "/" + storedName; - - // MinIO 업로드 - client.putObject( - PutObjectArgs.builder() - .bucket(bucketName) - .object(objectName) - .stream(is, is.available(), -1) - .contentType(file.getContentType()) - .build() - ); - - // DB 저장 - MinioAttachmentEntity attachment = MinioAttachmentEntity.builder() - .refId(refId) - .refType(refType) - .originalName(file.getOriginalFilename()) - .storedName(storedName) - .contentType(file.getContentType()) - .size(file.getSize()) - .storagePath(objectName) - .title(title != null ? title : file.getOriginalFilename()) - .version(version) - .description(description) - .regUserId(regUserId) - .projectId(projectId) - .build(); - - return minioAttachmentRepository.save(attachment); - } - } - - /** 파일 다운로드 */ - public byte[] downloadFile(String objectName, String type) { - MinioClient client = getClientByType(type); - String bucketName = getBucketByType(type); - - 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 (Exception e) { - throw new RuntimeException("MinIO 파일 다운로드 실패: " + objectName, e); - } - } - - /** - * MinIO에서 파일을 다운로드하여 서버 로컬에 저장 - * @param objectName MinIO 객체 경로 - * @param type MinIO 타입 - * @param localPath 서버에 저장할 로컬 경로 (예: downloads/temp) - * @return 저장된 로컬 파일 - */ - public File downloadFileToServer(String objectName, String type, String localPath) { - MinioClient client = getClientByType(type); - String bucketName = getBucketByType(type); - - try { - // mlflow-artifacts:/ 접두어 제거 - String cleanObjectName = objectName.replaceFirst("^mlflow-artifacts:/", "") - .replaceAll("^/+", "").replaceAll("/+$", ""); - - // 파일 확장자 체크 - if (!cleanObjectName.matches(".*\\.[a-zA-Z0-9]+$")) { - throw new RuntimeException("요청된 객체가 파일이 아닙니다: " + cleanObjectName); - } - - // 파일명 추출 - String fileName = Paths.get(cleanObjectName).getFileName().toString(); - File localDir = new File(localPath); - if (!localDir.exists()) localDir.mkdirs(); - File localFile = new File(localDir, fileName); - - try (InputStream is = client.getObject( - GetObjectArgs.builder() - .bucket(bucketName) - .object(cleanObjectName) - .build() - ); - FileOutputStream fos = new FileOutputStream(localFile) - ) { - byte[] buffer = new byte[8192]; - int bytesRead; - while ((bytesRead = is.read(buffer)) != -1) { - fos.write(buffer, 0, bytesRead); - } - } - - return localFile; - - } catch (Exception e) { - throw new RuntimeException("MinIO 파일 다운로드 실패: " + objectName + ". 원인: " + e.getMessage(), e); - } - } - - - /** YAML 텍스트 읽기 */ - public String readYamlText(String objectName, String type) { - MinioClient client = getClientByType(type); - String bucketName = getBucketByType(type); - - try (InputStream is = client.getObject( - GetObjectArgs.builder().bucket(bucketName).object(objectName).build() - )) { - return new String(is.readAllBytes(), StandardCharsets.UTF_8); - } catch (Exception e) { - throw new RuntimeException("MinIO YAML 읽기 실패: " + objectName, e); - } - } - - /** 파일 삭제 */ - public boolean deleteFile(Long id, String type) { - Optional attachmentOpt = minioAttachmentRepository.findById(id); - if (attachmentOpt.isEmpty()) return false; - - MinioAttachmentEntity attachment = attachmentOpt.get(); - MinioClient client = getClientByType(type); - String bucketName = getBucketByType(type); - - try { - client.removeObject( - RemoveObjectArgs.builder() - .bucket(bucketName) - .object(attachment.getStoragePath()) - .build() - ); - minioAttachmentRepository.deleteById(id); - return true; - } catch (Exception e) { - log.error("MinIO 파일 삭제 실패: " + attachment.getStoragePath(), e); - return false; - } - } - - // 이하 기존 search, findById, update 등 기존 DB 관련 메서드는 그대로 재사용 가능 - public List findAll() { - return minioAttachmentRepository.findAll(); - } - - public Optional findById(Long id) { - return minioAttachmentRepository.findById(id); - } - - public Page search(ProjectBaseSearchRequest request, String refType, Integer refId) { - int pageIndex = request.getPage() > 0 ? request.getPage() - 1 : 0; - - Pageable pageable = PageRequest.of( - pageIndex, - request.getSize(), - Sort.by(Sort.Direction.fromString(request.getSortDirection()), request.getSortField()) - ); - - LocalDate startDate = parseDate(request.getStartDate()); - LocalDate endDate = parseDate(request.getEndDate()); - - Specification spec = - minioAttachmentSpecification.searchByConditions( - refType, - refId, - request.getSearchType(), - request.getKeyword(), - startDate, - endDate - ); - - if (request.getProjectId() != null) { - spec = spec.and((root, query, cb) -> - cb.equal(root.get("projectId"), request.getProjectId()) - ); - } - - return minioAttachmentRepository.findAll(spec, pageable); - } - - private LocalDate parseDate(String dateStr) { - if (dateStr == null || dateStr.isBlank()) return null; - try { - return LocalDate.parse(dateStr, formatter); - } catch (DateTimeParseException e) { - throw new IllegalArgumentException("날짜 형식이 잘못되었습니다. yyyy-MM-dd 형식이어야 합니다: " + dateStr); - } - } -} diff --git a/src/main/java/kr/re/etri/autoflow/service/DynamicStorageAttachmentService.java b/src/main/java/kr/re/etri/autoflow/service/DynamicStorageAttachmentService.java new file mode 100644 index 0000000..c5fbf06 --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/service/DynamicStorageAttachmentService.java @@ -0,0 +1,173 @@ +package kr.re.etri.autoflow.service; + +import jakarta.transaction.Transactional; +import kr.re.etri.autoflow.entity.StorageAttachmentEntity; +import kr.re.etri.autoflow.payload.request.ProjectBaseSearchRequest; +import kr.re.etri.autoflow.repository.StorageAttachmentRepository; +import kr.re.etri.autoflow.service.storage.StorageProvider; +import kr.re.etri.autoflow.specification.StorageAttachmentSpecification; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.*; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Service; +import org.springframework.web.multipart.MultipartFile; + +import java.io.File; +import java.io.InputStream; +import java.time.LocalDate; +import java.time.format.DateTimeFormatter; +import java.time.format.DateTimeParseException; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional +public class DynamicStorageAttachmentService { + + private final StorageAttachmentRepository storageAttachmentRepository; + private final StorageAttachmentSpecification storageAttachmentSpecification; + private final StorageProvider storageProvider; + private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); + + /** 파일 업로드 & DB 저장 */ + public StorageAttachmentEntity uploadFile(MultipartFile file, + String path, + Long refId, + String refType, + String title, + String description, + Integer version, + String regUserId, + Long projectId, + String type + ) throws Exception { + + try (InputStream is = file.getInputStream()) { + String storedName = UUID.randomUUID() + "-" + file.getOriginalFilename(); + String objectName = (path == null || path.isEmpty()) + ? storedName + : path + "/" + storedName; + + // 스토리지 업로드 + storageProvider.uploadFile(null, objectName, is, file.getContentType(), file.getSize(), type); + + // DB 저장 + StorageAttachmentEntity attachment = StorageAttachmentEntity.builder() + .refId(refId) + .refType(refType) + .originalName(file.getOriginalFilename()) + .storedName(storedName) + .contentType(file.getContentType()) + .size(file.getSize()) + .storagePath(objectName) + .title(title != null ? title : file.getOriginalFilename()) + .version(version) + .description(description) + .regUserId(regUserId) + .projectId(projectId) + .build(); + + return storageAttachmentRepository.save(attachment); + } + } + + /** 파일 다운로드 */ + public byte[] downloadFile(String objectName, String type) { + try { + // mlflow-artifacts:/ 등 처리는 provider 단에서 혹은 여기서 할 수 있음. + // 현재 provider에 넣었으니 바로 위임 + return storageProvider.downloadFile(null, objectName, type); + } catch (Exception e) { + throw new RuntimeException("스토리지 파일 다운로드 실패: " + objectName, e); + } + } + + /** + * 스토리지에서 파일을 다운로드하여 서버 로컬에 저장 + */ + public File downloadFileToServer(String objectName, String type, String localPath) { + try { + return storageProvider.downloadFileToServer(null, objectName, localPath, type); + } catch (Exception e) { + throw new RuntimeException("스토리지 파일 다운로드 실패: " + objectName + ". 원인: " + e.getMessage(), e); + } + } + + /** YAML 텍스트 읽기 */ + public String readYamlText(String objectName, String type) { + try { + return storageProvider.readYamlText(null, objectName, type); + } catch (Exception e) { + throw new RuntimeException("스토리지 YAML 읽기 실패: " + objectName, e); + } + } + + /** 파일 삭제 */ + public boolean deleteFile(Long id, String type) { + Optional attachmentOpt = storageAttachmentRepository.findById(id); + if (attachmentOpt.isEmpty()) return false; + + StorageAttachmentEntity attachment = attachmentOpt.get(); + + try { + storageProvider.deleteFile(null, attachment.getStoragePath(), type); + storageAttachmentRepository.deleteById(id); + return true; + } catch (Exception e) { + log.error("스토리지 파일 삭제 실패: " + attachment.getStoragePath(), e); + return false; + } + } + + public List findAll() { + return storageAttachmentRepository.findAll(); + } + + public Optional findById(Long id) { + return storageAttachmentRepository.findById(id); + } + + public Page search(ProjectBaseSearchRequest request, String refType, Integer refId) { + int pageIndex = request.getPage() > 0 ? request.getPage() - 1 : 0; + + Pageable pageable = PageRequest.of( + pageIndex, + request.getSize(), + Sort.by(Sort.Direction.fromString(request.getSortDirection()), request.getSortField()) + ); + + LocalDate startDate = parseDate(request.getStartDate()); + LocalDate endDate = parseDate(request.getEndDate()); + + Specification spec = + storageAttachmentSpecification.searchByConditions( + refType, + refId, + request.getSearchType(), + request.getKeyword(), + startDate, + endDate + ); + + if (request.getProjectId() != null) { + spec = spec.and((root, query, cb) -> + cb.equal(root.get("projectId"), request.getProjectId()) + ); + } + + return storageAttachmentRepository.findAll(spec, pageable); + } + + private LocalDate parseDate(String dateStr) { + if (dateStr == null || dateStr.isBlank()) return null; + try { + return LocalDate.parse(dateStr, formatter); + } catch (DateTimeParseException e) { + throw new IllegalArgumentException("날짜 형식이 잘못되었습니다. yyyy-MM-dd 형식이어야 합니다: " + dateStr); + } + } +} diff --git a/src/main/java/kr/re/etri/autoflow/service/MinioAttachmentService.java b/src/main/java/kr/re/etri/autoflow/service/StorageAttachmentService.java similarity index 55% rename from src/main/java/kr/re/etri/autoflow/service/MinioAttachmentService.java rename to src/main/java/kr/re/etri/autoflow/service/StorageAttachmentService.java index a358a8f..57a1af8 100644 --- a/src/main/java/kr/re/etri/autoflow/service/MinioAttachmentService.java +++ b/src/main/java/kr/re/etri/autoflow/service/StorageAttachmentService.java @@ -1,26 +1,21 @@ package kr.re.etri.autoflow.service; -import io.minio.GetObjectArgs; -import io.minio.MinioClient; -import io.minio.PutObjectArgs; -import io.minio.RemoveObjectArgs; -import jakarta.transaction.Transactional; -import kr.re.etri.autoflow.entity.MinioAttachmentEntity; +import kr.re.etri.autoflow.entity.StorageAttachmentEntity; import kr.re.etri.autoflow.payload.request.BaseSearchRequest; import kr.re.etri.autoflow.payload.request.ProjectBaseSearchRequest; -import kr.re.etri.autoflow.repository.MinioAttachmentRepository; -import kr.re.etri.autoflow.specification.MinioAttachmentSpecification; +import kr.re.etri.autoflow.repository.StorageAttachmentRepository; +import kr.re.etri.autoflow.service.storage.StorageProvider; +import kr.re.etri.autoflow.specification.StorageAttachmentSpecification; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.BeanUtils; -import org.springframework.beans.factory.annotation.Value; import org.springframework.data.domain.*; import org.springframework.data.jpa.domain.Specification; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; import java.io.InputStream; -import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeParseException; @@ -32,32 +27,26 @@ import java.util.UUID; @Service @RequiredArgsConstructor @Transactional -public class MinioAttachmentService { +public class StorageAttachmentService { - private final MinioClient minioClient; - private final MinioAttachmentRepository minioAttachmentRepository; - private final MinioAttachmentSpecification minioAttachmentSpecification; - - @Value("${minio.bucket}") - private String bucketName; - - @Value("${minio.endpoint}") - private String minioEndpoint; + private final StorageProvider storageProvider; + private final StorageAttachmentRepository storageAttachmentRepository; + private final StorageAttachmentSpecification storageAttachmentSpecification; private final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd"); /** * 파일 업로드 & DB 저장 */ - public MinioAttachmentEntity uploadFile(MultipartFile file, - String path, - Long refId, - String refType, - String title, - String description, - Integer version, - String regUserId, - Long projectId + public StorageAttachmentEntity uploadFile(MultipartFile file, + String path, + Long refId, + String refType, + String title, + String description, + Integer version, + String regUserId, + Long projectId ) throws Exception { try (InputStream is = file.getInputStream()) { @@ -66,18 +55,11 @@ public class MinioAttachmentService { ? storedName : path + "/" + storedName; - // MinIO 업로드 - minioClient.putObject( - PutObjectArgs.builder() - .bucket(bucketName) - .object(objectName) - .stream(is, is.available(), -1) - .contentType(file.getContentType()) - .build() - ); + // 스토리지 업로드 + storageProvider.uploadFileToDefault(objectName, is, file.getContentType(), file.getSize()); // DB 저장 - MinioAttachmentEntity attachment = MinioAttachmentEntity.builder() + StorageAttachmentEntity attachment = StorageAttachmentEntity.builder() .refId(refId) .refType(refType) .originalName(file.getOriginalFilename()) @@ -92,28 +74,28 @@ public class MinioAttachmentService { .projectId(projectId) .build(); - return minioAttachmentRepository.save(attachment); + return storageAttachmentRepository.save(attachment); } } /** * 전체 조회 */ - public List findAll() { - return minioAttachmentRepository.findAll(); + public List findAll() { + return storageAttachmentRepository.findAll(); } /** * ID 조회 */ - public Optional findById(Long id) { - return minioAttachmentRepository.findById(id); + public Optional findById(Long id) { + return storageAttachmentRepository.findById(id); } /** * 검색 + 페이지네이션 */ - public Page search(ProjectBaseSearchRequest request, String refType, Integer refId) { + public Page search(ProjectBaseSearchRequest request, String refType, Integer refId) { int pageIndex = request.getPage() > 0 ? request.getPage() - 1 : 0; Pageable pageable = PageRequest.of( @@ -125,8 +107,8 @@ public class MinioAttachmentService { LocalDate startDate = parseDate(request.getStartDate()); LocalDate endDate = parseDate(request.getEndDate()); - Specification spec = - minioAttachmentSpecification.searchByConditions( + Specification spec = + storageAttachmentSpecification.searchByConditions( refType, refId, request.getSearchType(), @@ -141,7 +123,7 @@ public class MinioAttachmentService { ); } - return minioAttachmentRepository.findAll(spec, pageable); + return storageAttachmentRepository.findAll(spec, pageable); } private LocalDate parseDate(String dateStr) { @@ -154,28 +136,23 @@ public class MinioAttachmentService { } public byte[] downloadFile(String bucketName, String objectName) { - try (InputStream is = minioClient.getObject( - GetObjectArgs.builder().bucket(bucketName).object(objectName).build() - )) { - return is.readAllBytes(); + try { + return storageProvider.downloadFileFromDefault(objectName); } catch (Exception e) { - throw new RuntimeException("MinIO 파일 다운로드 실패: " + objectName, e); + throw new RuntimeException("스토리지 파일 다운로드 실패: " + objectName, e); } } - // YAML 텍스트 읽기 public String readYamlText(String bucketName, String objectName) { - try (InputStream is = minioClient.getObject( - GetObjectArgs.builder().bucket(bucketName).object(objectName).build() - )) { - return new String(is.readAllBytes(), StandardCharsets.UTF_8); + try { + return storageProvider.readYamlTextFromDefault(objectName); } catch (Exception e) { - throw new RuntimeException("MinIO YAML 읽기 실패: " + objectName, e); + throw new RuntimeException("YAML 읽기 실패: " + objectName, e); } } - public MinioAttachmentEntity updateFile( + public StorageAttachmentEntity updateFile( Long id, Long projectId, // 추가 MultipartFile file, @@ -185,13 +162,13 @@ public class MinioAttachmentService { String regUserId ) throws Exception { // 기존 엔티티 조회 - MinioAttachmentEntity existing = minioAttachmentRepository.findById(id) + StorageAttachmentEntity existing = storageAttachmentRepository.findById(id) .orElseThrow(() -> new IllegalArgumentException("첨부파일을 찾을 수 없습니다. ID=" + id)); // 최신 버전 조회 - Integer latestVersion = minioAttachmentRepository + Integer latestVersion = storageAttachmentRepository .findTopByRefIdAndRefTypeOrderByVersionDesc(existing.getRefId(), existing.getRefType()) - .map(MinioAttachmentEntity::getVersion) + .map(StorageAttachmentEntity::getVersion) .orElse(0); int newVersion = latestVersion + 1; @@ -203,18 +180,11 @@ public class MinioAttachmentService { : path + "/" + storedName; try (InputStream is = file.getInputStream()) { - minioClient.putObject( - PutObjectArgs.builder() - .bucket(bucketName) - .object(objectName) - .stream(is, is.available(), -1) - .contentType(file.getContentType()) - .build() - ); + storageProvider.uploadFileToDefault(objectName, is, file.getContentType(), file.getSize()); } // 새로운 엔티티 생성 (이전 데이터는 그대로 두고, 새로운 버전 생성) - MinioAttachmentEntity newAttachment = MinioAttachmentEntity.builder() + StorageAttachmentEntity newAttachment = StorageAttachmentEntity.builder() .projectId(projectId) // 추가 .refId(existing.getRefId()) .refType(existing.getRefType()) @@ -229,24 +199,24 @@ public class MinioAttachmentService { .regUserId(regUserId) .build(); - return minioAttachmentRepository.save(newAttachment); + return storageAttachmentRepository.save(newAttachment); } /** * 생성 (DB만 저장, 파일은 따로 업로드된 경우) */ - public MinioAttachmentEntity create(MinioAttachmentEntity entity) { - return minioAttachmentRepository.save(entity); + public StorageAttachmentEntity create(StorageAttachmentEntity entity) { + return storageAttachmentRepository.save(entity); } /** * 업데이트 */ - public Optional update(Long id, MinioAttachmentEntity dto) { - return minioAttachmentRepository.findById(id) + public Optional update(Long id, StorageAttachmentEntity dto) { + return storageAttachmentRepository.findById(id) .map(entity -> { BeanUtils.copyProperties(dto, entity, "id", "regDt"); // ID, 등록일 제외 - return minioAttachmentRepository.save(entity); + return storageAttachmentRepository.save(entity); }); } @@ -255,35 +225,30 @@ public class MinioAttachmentService { */ @Transactional public boolean delete(Long id) { - Optional attachmentOpt = minioAttachmentRepository.findById(id); + Optional attachmentOpt = storageAttachmentRepository.findById(id); if (attachmentOpt.isEmpty()) { return false; } - MinioAttachmentEntity attachment = attachmentOpt.get(); + StorageAttachmentEntity attachment = attachmentOpt.get(); try { - // MinIO 파일 삭제 - minioClient.removeObject( - RemoveObjectArgs.builder() - .bucket(bucketName) - .object(attachment.getStoragePath()) - .build() - ); + // 스토리지 파일 삭제 + storageProvider.deleteFileFromDefault(attachment.getStoragePath()); // DB에서 삭제 - minioAttachmentRepository.deleteById(id); + storageAttachmentRepository.deleteById(id); return true; } catch (Exception e) { - log.error("MinIO 파일 삭제 실패: " + attachment.getStoragePath(), e); + log.error("스토리지 파일 삭제 실패: " + attachment.getStoragePath(), e); return false; } } /** - * MinIO URL 반환 + * 파일 URL 반환 */ public String getFileUrl(String objectName) { - return String.format("%s/%s/%s", minioEndpoint, bucketName, objectName); + return storageProvider.getFileUrl(null, objectName, null); } } diff --git a/src/main/java/kr/re/etri/autoflow/service/storage/MinioStorageProvider.java b/src/main/java/kr/re/etri/autoflow/service/storage/MinioStorageProvider.java new file mode 100644 index 0000000..fbb1637 --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/service/storage/MinioStorageProvider.java @@ -0,0 +1,155 @@ +package kr.re.etri.autoflow.service.storage; + +import io.minio.GetObjectArgs; +import io.minio.MinioClient; +import io.minio.PutObjectArgs; +import io.minio.RemoveObjectArgs; +import kr.re.etri.autoflow.common.MinioType; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.nio.file.Paths; + +@Service("storageProvider") +@ConditionalOnProperty(name = "storage.provider", havingValue = "minio", matchIfMissing = true) +public class MinioStorageProvider implements StorageProvider { + + @Value("${minio.bucket:mlpipeline}") + private String defaultBucket; + + @Value("${minio.endpoint}") + private String minioEndpoint; + + private final MinioClient defaultMinioClient; + + public MinioStorageProvider(MinioClient minioClient) { + this.defaultMinioClient = minioClient; + } + + private MinioClient getClientByType(String type) { + if (type == null) return defaultMinioClient; + MinioType config = MinioType.of(type); + return MinioClient.builder() + .endpoint(config.getEndpoint()) + .credentials(config.getAccessKey(), config.getSecretKey()) + .build(); + } + + private String getBucketByType(String bucketName, String type) { + if (bucketName != null && !bucketName.isEmpty()) return bucketName; + if (type != null) { + return MinioType.of(type).getBucket(); + } + return defaultBucket; + } + + @Override + public void uploadFile(String bucketName, String objectName, InputStream is, String contentType, long size, String type) throws Exception { + MinioClient client = getClientByType(type); + String targetBucket = getBucketByType(bucketName, type); + + client.putObject( + PutObjectArgs.builder() + .bucket(targetBucket) + .object(objectName) + .stream(is, size, -1) + .contentType(contentType) + .build() + ); + } + + @Override + public void uploadFileToDefault(String objectName, InputStream is, String contentType, long size) throws Exception { + uploadFile(null, objectName, is, contentType, size, null); + } + + @Override + public byte[] downloadFile(String bucketName, String objectName, String type) throws Exception { + MinioClient client = getClientByType(type); + String targetBucket = getBucketByType(bucketName, type); + + try (InputStream is = client.getObject( + GetObjectArgs.builder() + .bucket(targetBucket) + .object(objectName) + .build() + )) { + return is.readAllBytes(); + } + } + + @Override + public byte[] downloadFileFromDefault(String objectName) throws Exception { + return downloadFile(null, objectName, null); + } + + @Override + public File downloadFileToServer(String bucketName, String objectName, String localPath, String type) throws Exception { + MinioClient client = getClientByType(type); + String targetBucket = getBucketByType(bucketName, type); + + String cleanObjectName = objectName.replaceFirst("^mlflow-artifacts:/", "") + .replaceAll("^/+", "").replaceAll("/+$", ""); + + String fileName = Paths.get(cleanObjectName).getFileName().toString(); + File localDir = new File(localPath); + if (!localDir.exists()) localDir.mkdirs(); + File localFile = new File(localDir, fileName); + + try (InputStream is = client.getObject( + GetObjectArgs.builder() + .bucket(targetBucket) + .object(cleanObjectName) + .build() + ); + FileOutputStream fos = new FileOutputStream(localFile)) { + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = is.read(buffer)) != -1) { + fos.write(buffer, 0, bytesRead); + } + } + return localFile; + } + + @Override + public String readYamlText(String bucketName, String objectName, String type) throws Exception { + byte[] bytes = downloadFile(bucketName, objectName, type); + return new String(bytes, StandardCharsets.UTF_8); + } + + @Override + public String readYamlTextFromDefault(String objectName) throws Exception { + return readYamlText(null, objectName, null); + } + + @Override + public void deleteFile(String bucketName, String objectName, String type) throws Exception { + MinioClient client = getClientByType(type); + String targetBucket = getBucketByType(bucketName, type); + + client.removeObject( + RemoveObjectArgs.builder() + .bucket(targetBucket) + .object(objectName) + .build() + ); + } + + @Override + public void deleteFileFromDefault(String objectName) throws Exception { + deleteFile(null, objectName, null); + } + + @Override + public String getFileUrl(String bucketName, String objectName, String type) { + String targetBucket = getBucketByType(bucketName, type); + String endpoint = (type != null) ? MinioType.of(type).getEndpoint() : minioEndpoint; + return String.format("%s/%s/%s", endpoint, targetBucket, objectName); + } +} diff --git a/src/main/java/kr/re/etri/autoflow/service/storage/S3StorageProvider.java b/src/main/java/kr/re/etri/autoflow/service/storage/S3StorageProvider.java new file mode 100644 index 0000000..2327f41 --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/service/storage/S3StorageProvider.java @@ -0,0 +1,139 @@ +package kr.re.etri.autoflow.service.storage; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Service; +import software.amazon.awssdk.core.sync.RequestBody; +import software.amazon.awssdk.core.ResponseInputStream; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.model.DeleteObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.s3.model.GetObjectResponse; +import software.amazon.awssdk.services.s3.model.PutObjectRequest; + +import java.io.File; +import java.io.FileOutputStream; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import java.nio.file.Paths; + +@Service("storageProvider") +@ConditionalOnProperty(name = "storage.provider", havingValue = "s3") +public class S3StorageProvider implements StorageProvider { + + @Value("${cloud.aws.s3.bucket:mlpipeline}") + private String defaultBucket; + + private final S3Client s3Client; + + public S3StorageProvider(S3Client s3Client) { + this.s3Client = s3Client; + } + + private String getBucketByType(String bucketName, String type) { + if (bucketName != null && !bucketName.isEmpty()) return bucketName; + // 향후 application.properties에서 type별 s3 bucket을 가져오는 로직 추가 가능 + // 현재는 defaultBucket 사용 (type 파라미터 무시) + return defaultBucket; + } + + @Override + public void uploadFile(String bucketName, String objectName, InputStream is, String contentType, long size, String type) throws Exception { + String targetBucket = getBucketByType(bucketName, type); + + PutObjectRequest putObjectRequest = PutObjectRequest.builder() + .bucket(targetBucket) + .key(objectName) + .contentType(contentType) + .build(); + + s3Client.putObject(putObjectRequest, RequestBody.fromInputStream(is, size)); + } + + @Override + public void uploadFileToDefault(String objectName, InputStream is, String contentType, long size) throws Exception { + uploadFile(null, objectName, is, contentType, size, null); + } + + @Override + public byte[] downloadFile(String bucketName, String objectName, String type) throws Exception { + String targetBucket = getBucketByType(bucketName, type); + + GetObjectRequest getObjectRequest = GetObjectRequest.builder() + .bucket(targetBucket) + .key(objectName) + .build(); + + try (ResponseInputStream is = s3Client.getObject(getObjectRequest)) { + return is.readAllBytes(); + } + } + + @Override + public byte[] downloadFileFromDefault(String objectName) throws Exception { + return downloadFile(null, objectName, null); + } + + @Override + public File downloadFileToServer(String bucketName, String objectName, String localPath, String type) throws Exception { + String targetBucket = getBucketByType(bucketName, type); + + String cleanObjectName = objectName.replaceFirst("^mlflow-artifacts:/", "") + .replaceAll("^/+", "").replaceAll("/+$", ""); + + String fileName = Paths.get(cleanObjectName).getFileName().toString(); + File localDir = new File(localPath); + if (!localDir.exists()) localDir.mkdirs(); + File localFile = new File(localDir, fileName); + + GetObjectRequest getObjectRequest = GetObjectRequest.builder() + .bucket(targetBucket) + .key(cleanObjectName) + .build(); + + try (ResponseInputStream is = s3Client.getObject(getObjectRequest); + FileOutputStream fos = new FileOutputStream(localFile)) { + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = is.read(buffer)) != -1) { + fos.write(buffer, 0, bytesRead); + } + } + return localFile; + } + + @Override + public String readYamlText(String bucketName, String objectName, String type) throws Exception { + byte[] bytes = downloadFile(bucketName, objectName, type); + return new String(bytes, StandardCharsets.UTF_8); + } + + @Override + public String readYamlTextFromDefault(String objectName) throws Exception { + return readYamlText(null, objectName, null); + } + + @Override + public void deleteFile(String bucketName, String objectName, String type) throws Exception { + String targetBucket = getBucketByType(bucketName, type); + + DeleteObjectRequest deleteObjectRequest = DeleteObjectRequest.builder() + .bucket(targetBucket) + .key(objectName) + .build(); + + s3Client.deleteObject(deleteObjectRequest); + } + + @Override + public void deleteFileFromDefault(String objectName) throws Exception { + deleteFile(null, objectName, null); + } + + @Override + public String getFileUrl(String bucketName, String objectName, String type) { + String targetBucket = getBucketByType(bucketName, type); + // S3 URL 형식 + return String.format("https://%s.s3.amazonaws.com/%s", targetBucket, objectName); + } +} diff --git a/src/main/java/kr/re/etri/autoflow/service/storage/StorageProvider.java b/src/main/java/kr/re/etri/autoflow/service/storage/StorageProvider.java new file mode 100644 index 0000000..41f7003 --- /dev/null +++ b/src/main/java/kr/re/etri/autoflow/service/storage/StorageProvider.java @@ -0,0 +1,57 @@ +package kr.re.etri.autoflow.service.storage; + +import java.io.File; +import java.io.InputStream; + +public interface StorageProvider { + + /** + * 파일 업로드 + */ + void uploadFile(String bucketName, String objectName, InputStream is, String contentType, long size, String type) throws Exception; + + /** + * 기본 스토리지에 파일 업로드 + */ + void uploadFileToDefault(String objectName, InputStream is, String contentType, long size) throws Exception; + + /** + * 파일 다운로드 (byte[]) + */ + byte[] downloadFile(String bucketName, String objectName, String type) throws Exception; + + /** + * 기본 스토리지에서 파일 다운로드 + */ + byte[] downloadFileFromDefault(String objectName) throws Exception; + + /** + * 파일 다운로드 후 로컬 디렉토리에 저장 + */ + File downloadFileToServer(String bucketName, String objectName, String localPath, String type) throws Exception; + + /** + * YAML 텍스트 읽기 + */ + String readYamlText(String bucketName, String objectName, String type) throws Exception; + + /** + * 기본 스토리지에서 YAML 읽기 + */ + String readYamlTextFromDefault(String objectName) throws Exception; + + /** + * 파일 삭제 + */ + void deleteFile(String bucketName, String objectName, String type) throws Exception; + + /** + * 기본 스토리지에서 파일 삭제 + */ + void deleteFileFromDefault(String objectName) throws Exception; + + /** + * 파일 URL 생성 (가능한 경우) + */ + String getFileUrl(String bucketName, String objectName, String type); +} diff --git a/src/main/java/kr/re/etri/autoflow/specification/MinioAttachmentSpecification.java b/src/main/java/kr/re/etri/autoflow/specification/StorageAttachmentSpecification.java similarity index 89% rename from src/main/java/kr/re/etri/autoflow/specification/MinioAttachmentSpecification.java rename to src/main/java/kr/re/etri/autoflow/specification/StorageAttachmentSpecification.java index 53edeb4..f2f0da5 100644 --- a/src/main/java/kr/re/etri/autoflow/specification/MinioAttachmentSpecification.java +++ b/src/main/java/kr/re/etri/autoflow/specification/StorageAttachmentSpecification.java @@ -6,7 +6,7 @@ import jakarta.persistence.criteria.Predicate; import jakarta.persistence.metamodel.Attribute; import jakarta.persistence.metamodel.EntityType; import jakarta.persistence.metamodel.Metamodel; -import kr.re.etri.autoflow.entity.MinioAttachmentEntity; +import kr.re.etri.autoflow.entity.StorageAttachmentEntity; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.data.jpa.domain.Specification; @@ -20,7 +20,7 @@ import java.util.stream.Collectors; @Component @RequiredArgsConstructor -public class MinioAttachmentSpecification { +public class StorageAttachmentSpecification { private final EntityManager entityManager; @@ -29,7 +29,7 @@ public class MinioAttachmentSpecification { @PostConstruct public void init() { Metamodel metamodel = entityManager.getMetamodel(); - EntityType entityType = metamodel.entity(MinioAttachmentEntity.class); + EntityType entityType = metamodel.entity(StorageAttachmentEntity.class); // 문자열 타입 필드명만 추출 stringFields = entityType.getAttributes().stream() @@ -37,10 +37,10 @@ public class MinioAttachmentSpecification { .map(Attribute::getName) .collect(Collectors.toSet()); - log.info("MinioAttachmentEntity string fields: {}", stringFields); + log.info("StorageAttachmentEntity string fields: {}", stringFields); } - public Specification searchByConditions( + public Specification searchByConditions( String refType, Integer refId, String searchType, diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 7f3cef8..5f4c61c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -50,6 +50,9 @@ spring.servlet.multipart.max-request-size=500MB springdoc.swagger-ui.tags-sorter=alpha +# Storage Provider (minio or s3) +storage.provider=minio + # MinIO ?? minio.endpoint=http://192.168.10.135:31795 minio.access-key=minio @@ -77,4 +80,5 @@ external.auth.sw-search-url=https://a659120d3e2ff43ff94087b29396fd96-1057696791. cloud.aws.region.static=ap-northeast-2 cloud.aws.credentials.access-key=AKIA2UC3EPERDDR4UOWN -cloud.aws.credentials.secret-key=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 \ No newline at end of file +cloud.aws.credentials.secret-key=Ps7ShmtcemhhTmZi+aUCpSpfZxjqFGyy51qgDSGD +cloud.aws.s3.bucket=mlpipeline \ No newline at end of file