[ADD] MinIO 활용 파일 업로드, 다운로드 및 삭제 API 추가, 관련 Service 및 Controller 로직 구현, application.properties 파일의 MLflow 비밀번호 수정
parent
1452b1265a
commit
0a59aa1523
@ -0,0 +1,27 @@
|
|||||||
|
package kr.re.etri.autoflow.common;
|
||||||
|
|
||||||
|
import lombok.AllArgsConstructor;
|
||||||
|
import lombok.Getter;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@AllArgsConstructor
|
||||||
|
@Getter
|
||||||
|
public enum MinioType {
|
||||||
|
TYPE1("mlpipeline", "http://192.168.10.135:31795", "minio", "minio123"),
|
||||||
|
TYPE2("mlflow-artifacts", "http://192.168.10.135:31000", "admin", "YAt76ajBtr");
|
||||||
|
|
||||||
|
private final String bucket;
|
||||||
|
private final String endpoint;
|
||||||
|
private final String accessKey;
|
||||||
|
private final String secretKey;
|
||||||
|
|
||||||
|
public static final Map<String, MinioType> TYPE_MAP = Map.of(
|
||||||
|
"type1", TYPE1,
|
||||||
|
"type2", TYPE2
|
||||||
|
);
|
||||||
|
|
||||||
|
public static MinioType of(String type) {
|
||||||
|
return TYPE_MAP.getOrDefault(type.toLowerCase(), TYPE1);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,136 @@
|
|||||||
|
package kr.re.etri.autoflow.controllers;
|
||||||
|
|
||||||
|
import kr.re.etri.autoflow.entity.MinioAttachmentEntity;
|
||||||
|
import kr.re.etri.autoflow.payload.request.ProjectBaseSearchRequest;
|
||||||
|
import kr.re.etri.autoflow.service.DynamicMinioAttachmentService;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
|
import org.springframework.http.MediaType;
|
||||||
|
import org.springframework.http.ResponseEntity;
|
||||||
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
import org.springframework.web.multipart.MultipartFile;
|
||||||
|
|
||||||
|
import java.net.URLEncoder;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
|
@RestController
|
||||||
|
@RequestMapping("/api/minio")
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class DynamicMinioAttachmentController {
|
||||||
|
|
||||||
|
private final DynamicMinioAttachmentService minioService;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 파일 업로드
|
||||||
|
*/
|
||||||
|
@PostMapping("/upload")
|
||||||
|
public ResponseEntity<Map<String, Object>> uploadFile(
|
||||||
|
@RequestParam MultipartFile file,
|
||||||
|
@RequestParam(required = false) String path,
|
||||||
|
@RequestParam Long refId,
|
||||||
|
@RequestParam(defaultValue = "DATASET") String refType,
|
||||||
|
@RequestParam(required = false) String title,
|
||||||
|
@RequestParam(required = false) String description,
|
||||||
|
@RequestParam(defaultValue = "1") Integer version,
|
||||||
|
@RequestParam String regUserId,
|
||||||
|
@RequestParam Long projectId,
|
||||||
|
@RequestParam(defaultValue = "type1") String type
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
MinioAttachmentEntity attachment = minioService.uploadFile(
|
||||||
|
file, path, refId, refType, title, description, version, regUserId, projectId, type
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, Object> response = new HashMap<>();
|
||||||
|
response.put("attachment", attachment);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(response);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("파일 업로드 실패", e);
|
||||||
|
return ResponseEntity.internalServerError()
|
||||||
|
.body(Map.of("error", e.getMessage()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 파일 다운로드
|
||||||
|
*/
|
||||||
|
@GetMapping("/download")
|
||||||
|
public ResponseEntity<byte[]> downloadFile(
|
||||||
|
@RequestParam String objectName,
|
||||||
|
@RequestParam(defaultValue = "type1") String type
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
byte[] bytes = minioService.downloadFile(objectName, type);
|
||||||
|
|
||||||
|
String encodedFileName = URLEncoder.encode(objectName, StandardCharsets.UTF_8)
|
||||||
|
.replaceAll("\\+", "%20");
|
||||||
|
|
||||||
|
return ResponseEntity.ok()
|
||||||
|
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename*=UTF-8''" + encodedFileName)
|
||||||
|
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||||
|
.body(bytes);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("파일 다운로드 실패", e);
|
||||||
|
return ResponseEntity.internalServerError().build();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* YAML 텍스트 읽기
|
||||||
|
*/
|
||||||
|
@GetMapping(value = "/readYamlText", produces = MediaType.TEXT_PLAIN_VALUE)
|
||||||
|
public ResponseEntity<String> readYamlText(
|
||||||
|
@RequestParam String objectName,
|
||||||
|
@RequestParam(defaultValue = "type1") String type
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
String content = minioService.readYamlText(objectName, type);
|
||||||
|
return ResponseEntity.ok(content);
|
||||||
|
} catch (Exception e) {
|
||||||
|
log.error("MinIO YAML 읽기 실패: {}", objectName, e);
|
||||||
|
return ResponseEntity.internalServerError()
|
||||||
|
.body("Error reading file: " + e.getMessage());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 파일 삭제
|
||||||
|
*/
|
||||||
|
@DeleteMapping("/delete")
|
||||||
|
public ResponseEntity<Map<String, Object>> deleteFile(
|
||||||
|
@RequestParam Long id,
|
||||||
|
@RequestParam(defaultValue = "type1") String type
|
||||||
|
) {
|
||||||
|
boolean result = minioService.deleteFile(id, type);
|
||||||
|
return ResponseEntity.ok(Map.of("deleted", result));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 전체 조회
|
||||||
|
*/
|
||||||
|
@GetMapping("/list")
|
||||||
|
public ResponseEntity<List<MinioAttachmentEntity>> listAll() {
|
||||||
|
List<MinioAttachmentEntity> list = minioService.findAll();
|
||||||
|
return ResponseEntity.ok(list);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 검색 + 페이지네이션
|
||||||
|
*/
|
||||||
|
@PostMapping("/search")
|
||||||
|
public ResponseEntity<Page<MinioAttachmentEntity>> search(
|
||||||
|
@RequestBody ProjectBaseSearchRequest request,
|
||||||
|
@RequestParam String refType,
|
||||||
|
@RequestParam Integer refId
|
||||||
|
) {
|
||||||
|
Page<MinioAttachmentEntity> page = minioService.search(request, refType, refId);
|
||||||
|
return ResponseEntity.ok(page);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,205 @@
|
|||||||
|
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.InputStream;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
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 (InputStream is = client.getObject(
|
||||||
|
GetObjectArgs.builder().bucket(bucketName).object(objectName).build()
|
||||||
|
)) {
|
||||||
|
return is.readAllBytes();
|
||||||
|
} catch (Exception e) {
|
||||||
|
throw new RuntimeException("MinIO 파일 다운로드 실패: " + objectName, 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in new issue