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