[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,24 +27,18 @@ 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,
public StorageAttachmentEntity uploadFile(MultipartFile file,
String path,
Long refId,
String refType,
@ -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
@ -78,3 +81,4 @@ 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.s3.bucket=mlpipeline
Loading…
Cancel
Save