parent
ffec74b7ee
commit
40d2cc9367
@ -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);
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
Loading…
Reference in new issue