Feat: 이미지 폴더채로 업로드 구현 - S11P21S002-182

This commit is contained in:
kimtaesoo7 2024-09-19 17:49:04 +09:00
parent ae2a55434f
commit 7ad391741c
3 changed files with 121 additions and 7 deletions

View File

@ -1,5 +1,6 @@
package com.worlabel.domain.image.controller;
import com.worlabel.domain.folder.entity.dto.FolderResponse;
import com.worlabel.domain.image.entity.dto.*;
import com.worlabel.domain.image.service.ImageService;
import com.worlabel.global.annotation.CurrentUser;
@ -14,6 +15,7 @@ import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.List;
@Slf4j
@ -37,6 +39,17 @@ public class ImageController {
imageService.uploadImageList(imageList, folderId, projectId, memberId);
}
@PostMapping("/upload")
@SwaggerApiSuccess(description = "폴더와 이미지 파일을 성공적으로 업로드합니다.")
@Operation(summary = "폴더 업로드", description = "폴더와 이미지 파일을 업로드합니다.")
@SwaggerApiError({ErrorCode.BAD_REQUEST, ErrorCode.NOT_AUTHOR, ErrorCode.SERVER_ERROR})
public void uploadFolder(
@RequestParam("folderZip") MultipartFile folderZip,
@PathVariable("project_id") Integer projectId,
@RequestParam(value = "parentId", defaultValue = "0") Integer parentId) throws IOException {
imageService.uploadFolderWithImages(folderZip, projectId, parentId);
}
@GetMapping("/{image_id}")
@SwaggerApiSuccess(description = "이미지를 단일 조회합니다.")
@Operation(summary = "이미지 단일 조회", description = "이미지 정보를 단일 조회합니다.")

View File

@ -0,0 +1,12 @@
package com.worlabel.domain.image.entity.dto;
import lombok.Getter;
import lombok.Setter;
import org.springframework.web.multipart.MultipartFile;
@Getter
@Setter
public class FolderUploadRequest {
private MultipartFile folder; // 폴더를 압축 파일로 받을 있음 (zip )
}

View File

@ -3,12 +3,11 @@ package com.worlabel.domain.image.service;
import com.worlabel.domain.folder.entity.Folder;
import com.worlabel.domain.folder.repository.FolderRepository;
import com.worlabel.domain.image.entity.Image;
import com.worlabel.domain.image.entity.dto.DetailImageResponse;
import com.worlabel.domain.image.entity.dto.ImageLabelRequest;
import com.worlabel.domain.image.entity.dto.ImageResponse;
import com.worlabel.domain.image.entity.dto.ImageStatusRequest;
import com.worlabel.domain.image.entity.dto.*;
import com.worlabel.domain.image.repository.ImageRepository;
import com.worlabel.domain.participant.entity.PrivilegeType;
import com.worlabel.domain.project.entity.Project;
import com.worlabel.domain.project.repository.ProjectRepository;
import com.worlabel.global.annotation.CheckPrivilege;
import com.worlabel.global.exception.CustomException;
import com.worlabel.global.exception.ErrorCode;
@ -19,7 +18,14 @@ import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
@Slf4j
@Service
@ -30,6 +36,9 @@ public class ImageService {
private final S3UploadService s3UploadService;
private final ImageRepository imageRepository;
private final FolderRepository folderRepository;
private final ProjectRepository projectRepository;
private static int orderCount = 0;
/**
* 이미지 리스트 업로드
@ -40,7 +49,7 @@ public class ImageService {
for (int order = 0; order < imageList.size(); order++) {
MultipartFile file = imageList.get(order);
String extension = getExtension(file);
String imageKey = s3UploadService.upload(file,extension, projectId);
String imageKey = s3UploadService.upload(file, extension, projectId);
Image image = Image.of(file.getOriginalFilename(), imageKey, extension, order, folder);
imageRepository.save(image);
}
@ -105,6 +114,86 @@ public class ImageService {
save(imageId, labelRequest.getData());
}
// 폴더 압축 파일을 받아 폴더와 이미지 파일을 저장하는 메서드
public void uploadFolderWithImages(MultipartFile folderZip, Integer projectId, Integer parentId) throws IOException {
orderCount = 0;
// 프로젝트 정보 가져오기
Project project = projectRepository.findById(projectId)
.orElseThrow(() -> new CustomException(ErrorCode.PROJECT_NOT_FOUND));
// 압축 해제 경로 설정 (임시 폴더에 압축 해제)
Path tempDir = Files.createTempDirectory("uploadedFolder");
unzip(folderZip, tempDir.toString());
// 부모 폴더가 최상위인지 확인
Folder parentFolder = (parentId == 0) ? null : folderRepository.findById(parentId)
.orElseThrow(() -> new CustomException(ErrorCode.DATA_NOT_FOUND));
// 압축 풀린 폴더를 재귀적으로 탐색하여 하위 폴더 이미지 파일을 저장
processFolderRecursively(tempDir.toFile(), parentFolder, project);
}
// 폴더 내부 구조를 재귀적으로 탐색하여 저장
private void processFolderRecursively(File directory, Folder parentFolder, Project project) {
if (directory.exists() && directory.isDirectory()) {
Folder currentFolder = Folder.of(directory.getName(), parentFolder, project);
folderRepository.save(currentFolder);
File[] files = directory.listFiles();
if (files != null) {
for (File file : files) {
if (file.isDirectory()) {
// 하위 폴더인 경우 재귀 호출
processFolderRecursively(file, currentFolder, project);
} else if (isImageFile(file)) {
// 이미지 파일인 경우
String fileName = file.getName();
String extension = fileName.substring(fileName.lastIndexOf(".") + 1);
// todo MultipartFile 캐스팅 하면 안됨 이거 수정해야함
String imageKey = s3UploadService.upload((MultipartFile) file, extension, project.getId());
Image image = Image.of(file.getName(), imageKey, extension, orderCount++, currentFolder);
imageRepository.save(image);
}
}
}
}
}
// 이미지 파일인지 확인하는 메서드
private boolean isImageFile(File file) {
String fileName = file.getName().toLowerCase();
return fileName.endsWith(".jpg") || fileName.endsWith(".png") || fileName.endsWith(".jpeg");
}
// 압축 파일을 임시 폴더에 압축 해제하는 메서드
private void unzip(MultipartFile zipFile, String destDir) throws IOException {
try (ZipInputStream zis = new ZipInputStream(zipFile.getInputStream())) {
ZipEntry zipEntry;
while ((zipEntry = zis.getNextEntry()) != null) {
Path newPath = zipSlipProtect(zipEntry, Paths.get(destDir));
if (zipEntry.isDirectory()) {
Files.createDirectories(newPath);
} else {
Files.createDirectories(newPath.getParent());
Files.copy(zis, newPath);
}
zis.closeEntry();
}
}
}
// 보안 보호를 위해 압축 파일 경로를 보호하는 메서드
private Path zipSlipProtect(ZipEntry zipEntry, Path targetDir) throws IOException {
Path targetDirResolved = targetDir.resolve(zipEntry.getName());
Path normalizePath = targetDirResolved.normalize();
if (!normalizePath.startsWith(targetDir)) {
throw new IOException("Zip entry is outside of the target dir: " + zipEntry.getName());
}
return normalizePath;
}
private void save(final long imageId, final String data) {
Image image = imageRepository.findById(imageId)
.orElseThrow(() -> new CustomException(ErrorCode.DATA_NOT_FOUND));
@ -117,14 +206,14 @@ public class ImageService {
String fileName = file.getOriginalFilename();
return fileName.substring(fileName.lastIndexOf(".") + 1); // 파일 확장자
}
// 폴더 가져오기
private Folder getFolder(final Integer folderId) {
return folderRepository.findById(folderId)
.orElseThrow(() -> new CustomException(ErrorCode.DATA_NOT_FOUND));
}
// 이미지 가져오면서 프로젝트 소속 여부를 확인
private Image getImageByIdAndFolderIdAndFolderProjectId(final Integer folderId, final Long imageId, final Integer projectId) {
return imageRepository.findByIdAndFolderIdAndFolderProjectId(imageId, folderId, projectId)
.orElseThrow(() -> new CustomException(ErrorCode.DATA_NOT_FOUND));