[REMOVE] DynamicMinioAttachmentController, 관련 서비스 및 엔티티 클래스 삭제

main
bjkim 2 months ago
parent ffec74b7ee
commit 40d2cc9367

@ -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}

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

@ -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<List<MinioAttachmentEntity>> listAll() {
List<MinioAttachmentEntity> list = minioService.findAll();
public ResponseEntity<List<StorageAttachmentEntity>> listAll() {
List<StorageAttachmentEntity> list = minioService.findAll();
return ResponseEntity.ok(list);
}
@ -158,12 +158,12 @@ public class DynamicMinioAttachmentController {
* +
*/
@PostMapping("/search")
public ResponseEntity<Page<MinioAttachmentEntity>> search(
public ResponseEntity<Page<StorageAttachmentEntity>> search(
@RequestBody ProjectBaseSearchRequest request,
@RequestParam String refType,
@RequestParam Integer refId
) {
Page<MinioAttachmentEntity> page = minioService.search(request, refType, refId);
Page<StorageAttachmentEntity> page = minioService.search(request, refType, refId);
return ResponseEntity.ok(page);
}
}

@ -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;

@ -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<String, Object> 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) {

@ -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<Map<String, Object>> 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<String, Object> response = new HashMap<>();

@ -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<List<MinioAttachmentEntity>> getAll() {
return ResponseEntity.ok(minioAttachmentService.findAll());
public ResponseEntity<List<StorageAttachmentEntity>> getAll() {
return ResponseEntity.ok(storageAttachmentService.findAll());
}
@Operation(summary = "ID로 첨부파일 조회")
@GetMapping("/{id}")
public ResponseEntity<MinioAttachmentEntity> getById(
public ResponseEntity<StorageAttachmentEntity> 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<Page<MinioAttachmentEntity>> search(
public ResponseEntity<Page<StorageAttachmentEntity>> 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<MinioAttachmentEntity> page = minioAttachmentService.search(request, refType, refId);
Page<StorageAttachmentEntity> page = storageAttachmentService.search(request, refType, refId);
return ResponseEntity.ok(page);
}
@ -81,7 +81,7 @@ public class MinioAttachmentController {
public ResponseEntity<Void> 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<byte[]> 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<String> 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<String, Object> 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<String, Object> 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);

@ -19,7 +19,7 @@ import java.time.LocalDateTime;
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class MinioAttachmentEntity {
public class StorageAttachmentEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)

@ -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<MinioAttachmentEntity, Long>, JpaSpecificationExecutor<MinioAttachmentEntity> {
//최신버전 파일 가져오기
Optional<MinioAttachmentEntity> findTopByRefIdAndRefTypeOrderByVersionDesc(Long refId, String refType);
List<MinioAttachmentEntity> findAllByRefId(Long refId);
}

@ -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<StorageAttachmentEntity, Long>, JpaSpecificationExecutor<StorageAttachmentEntity> {
//최신버전 파일 가져오기
Optional<StorageAttachmentEntity> findTopByRefIdAndRefTypeOrderByVersionDesc(Long refId, String refType);
List<StorageAttachmentEntity> findAllByRefId(Long refId);
}

@ -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<MinioAttachmentEntity> attachments =
minioAttachmentRepository.findAllByRefId(dataGroupId);
List<StorageAttachmentEntity> 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);
}

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

@ -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<MinioAttachmentEntity> 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<MinioAttachmentEntity> findAll() {
return minioAttachmentRepository.findAll();
}
public Optional<MinioAttachmentEntity> findById(Long id) {
return minioAttachmentRepository.findById(id);
}
public Page<MinioAttachmentEntity> 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<MinioAttachmentEntity> 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);
}
}
}

@ -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<StorageAttachmentEntity> 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<StorageAttachmentEntity> findAll() {
return storageAttachmentRepository.findAll();
}
public Optional<StorageAttachmentEntity> findById(Long id) {
return storageAttachmentRepository.findById(id);
}
public Page<StorageAttachmentEntity> 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<StorageAttachmentEntity> 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);
}
}
}

@ -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<MinioAttachmentEntity> findAll() {
return minioAttachmentRepository.findAll();
public List<StorageAttachmentEntity> findAll() {
return storageAttachmentRepository.findAll();
}
/**
* ID
*/
public Optional<MinioAttachmentEntity> findById(Long id) {
return minioAttachmentRepository.findById(id);
public Optional<StorageAttachmentEntity> findById(Long id) {
return storageAttachmentRepository.findById(id);
}
/**
* +
*/
public Page<MinioAttachmentEntity> search(ProjectBaseSearchRequest request, String refType, Integer refId) {
public Page<StorageAttachmentEntity> 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<MinioAttachmentEntity> spec =
minioAttachmentSpecification.searchByConditions(
Specification<StorageAttachmentEntity> 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<MinioAttachmentEntity> update(Long id, MinioAttachmentEntity dto) {
return minioAttachmentRepository.findById(id)
public Optional<StorageAttachmentEntity> 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<MinioAttachmentEntity> attachmentOpt = minioAttachmentRepository.findById(id);
Optional<StorageAttachmentEntity> 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);
}
}

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

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

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

@ -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<MinioAttachmentEntity> entityType = metamodel.entity(MinioAttachmentEntity.class);
EntityType<StorageAttachmentEntity> 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<MinioAttachmentEntity> searchByConditions(
public Specification<StorageAttachmentEntity> searchByConditions(
String refType,
Integer refId,
String searchType,

@ -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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
cloud.aws.credentials.secret-key=Ps7ShmtcemhhTmZi+aUCpSpfZxjqFGyy51qgDSGD
cloud.aws.s3.bucket=mlpipeline
Loading…
Cancel
Save