|
|
|
|
@ -5,6 +5,7 @@ import org.springframework.beans.factory.annotation.Value;
|
|
|
|
|
import org.springframework.stereotype.Component;
|
|
|
|
|
import org.springframework.web.multipart.MultipartFile;
|
|
|
|
|
|
|
|
|
|
import javax.annotation.PostConstruct;
|
|
|
|
|
import javax.servlet.http.HttpServletRequest;
|
|
|
|
|
import javax.servlet.http.HttpServletResponse;
|
|
|
|
|
import java.io.File;
|
|
|
|
|
@ -12,12 +13,15 @@ import java.io.FileInputStream;
|
|
|
|
|
import java.io.IOException;
|
|
|
|
|
import java.io.OutputStream;
|
|
|
|
|
import java.net.URLEncoder;
|
|
|
|
|
import java.nio.charset.StandardCharsets;
|
|
|
|
|
import java.nio.file.Files;
|
|
|
|
|
import java.nio.file.Path;
|
|
|
|
|
import java.nio.file.Paths;
|
|
|
|
|
import java.util.ArrayList;
|
|
|
|
|
import java.util.Arrays;
|
|
|
|
|
import java.util.List;
|
|
|
|
|
import java.util.UUID;
|
|
|
|
|
import java.util.stream.Collectors;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 파일 업로드/다운로드 공통 유틸리티 클래스
|
|
|
|
|
@ -49,6 +53,86 @@ public class FileUtil {
|
|
|
|
|
@Value("${file.upload.real-file-delete:true}")
|
|
|
|
|
private boolean realFileDelete;
|
|
|
|
|
|
|
|
|
|
/** 허용된 파일 확장자 목록 */
|
|
|
|
|
private List<String> allowedExtensionList;
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 초기화 메서드
|
|
|
|
|
* allowedExtensions 문자열을 파싱하여 allowedExtensionList를 초기화
|
|
|
|
|
*/
|
|
|
|
|
@PostConstruct
|
|
|
|
|
public void init() {
|
|
|
|
|
allowedExtensionList = Arrays.stream(allowedExtensions.split(","))
|
|
|
|
|
.map(String::trim)
|
|
|
|
|
.map(String::toLowerCase)
|
|
|
|
|
.collect(Collectors.toList());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 경로 유효성 검증
|
|
|
|
|
* 경로 탐색(Path Traversal) 공격 방지
|
|
|
|
|
* @param path 검증할 경로
|
|
|
|
|
* @param isFileName 파일명 여부 (true: 파일명, false: 디렉토리명)
|
|
|
|
|
* @throws IOException 잘못된 경로일 경우 예외 발생
|
|
|
|
|
*/
|
|
|
|
|
private void validatePath(String path, boolean isFileName) throws IOException {
|
|
|
|
|
if (path == null || path.contains("..") || path.contains("/") || path.contains("\\")) {
|
|
|
|
|
String type = isFileName ? "파일명" : "디렉토리 경로";
|
|
|
|
|
throw new IOException("잘못된 " + type + "입니다: " + path);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 파일 확장자 유효성 검증
|
|
|
|
|
* @param fileExt 검증할 파일 확장자
|
|
|
|
|
* @return 허용된 확장자인지 여부
|
|
|
|
|
*/
|
|
|
|
|
private boolean isAllowedExtension(String fileExt) {
|
|
|
|
|
return allowedExtensionList.contains(fileExt.toLowerCase());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 브라우저별 파일명 인코딩 처리
|
|
|
|
|
* @param fileName 원본 파일명
|
|
|
|
|
* @param userAgent 브라우저 User-Agent
|
|
|
|
|
* @return 인코딩된 파일명
|
|
|
|
|
* @throws IOException 인코딩 처리 중 오류 발생 시
|
|
|
|
|
*/
|
|
|
|
|
private String getEncodedFilename(String fileName, String userAgent) throws IOException {
|
|
|
|
|
if (userAgent.contains("MSIE") || userAgent.contains("Trident") || userAgent.contains("Chrome")) {
|
|
|
|
|
// IE, Chrome
|
|
|
|
|
return URLEncoder.encode(fileName, "UTF-8").replaceAll("\\+", "%20");
|
|
|
|
|
} else if (userAgent.contains("Firefox")) {
|
|
|
|
|
// Firefox
|
|
|
|
|
return "\"" + new String(fileName.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1) + "\"";
|
|
|
|
|
} else {
|
|
|
|
|
// 기타 브라우저
|
|
|
|
|
return URLEncoder.encode(fileName, "UTF-8");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* FileVO 객체 생성
|
|
|
|
|
* @param originalFilename 원본 파일명
|
|
|
|
|
* @param storedFilename 저장 파일명
|
|
|
|
|
* @param subDir 저장 디렉토리
|
|
|
|
|
* @param fileSize 파일 크기
|
|
|
|
|
* @param fileExt 파일 확장자
|
|
|
|
|
* @param contentType 컨텐츠 타입
|
|
|
|
|
* @return 생성된 FileVO 객체
|
|
|
|
|
*/
|
|
|
|
|
private FileVO createFileVO(String originalFilename, String storedFilename, String subDir,
|
|
|
|
|
long fileSize, String fileExt, String contentType) {
|
|
|
|
|
FileVO fileVO = new FileVO();
|
|
|
|
|
fileVO.setOriginalFileNm(originalFilename);
|
|
|
|
|
fileVO.setStoredFileNm(storedFilename);
|
|
|
|
|
fileVO.setFilePath(subDir);
|
|
|
|
|
fileVO.setFileSize(fileSize);
|
|
|
|
|
fileVO.setFileExt(fileExt);
|
|
|
|
|
fileVO.setContentType(contentType);
|
|
|
|
|
return fileVO;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 파일 업로드 처리
|
|
|
|
|
* @param files 업로드할 파일 목록
|
|
|
|
|
@ -57,78 +141,136 @@ public class FileUtil {
|
|
|
|
|
* @throws IOException 파일 처리 중 오류 발생 시
|
|
|
|
|
*/
|
|
|
|
|
public List<FileVO> uploadFiles(List<MultipartFile> files, String subDir) throws IOException {
|
|
|
|
|
List<FileVO> uploadedFiles = new ArrayList<>();
|
|
|
|
|
long totalSize = 0;
|
|
|
|
|
// 파일 유효성 검증
|
|
|
|
|
validateFiles(files);
|
|
|
|
|
|
|
|
|
|
// 디렉토리 경로 검증
|
|
|
|
|
validatePath(subDir, false);
|
|
|
|
|
|
|
|
|
|
// 디렉토리 생성
|
|
|
|
|
String uploadDir = uploadPath + File.separator + subDir;
|
|
|
|
|
createDirectoryIfNotExists(uploadDir);
|
|
|
|
|
|
|
|
|
|
// 파일 업로드 처리
|
|
|
|
|
return processFileUploads(files, subDir, uploadDir);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 파일 목록 유효성 검증
|
|
|
|
|
* @param files 검증할 파일 목록
|
|
|
|
|
* @throws IOException 유효성 검증 실패 시
|
|
|
|
|
*/
|
|
|
|
|
private void validateFiles(List<MultipartFile> files) throws IOException {
|
|
|
|
|
// 파일 개수 검증
|
|
|
|
|
int validFileCount = 0;
|
|
|
|
|
for (MultipartFile file : files) {
|
|
|
|
|
if (!file.isEmpty()) {
|
|
|
|
|
validFileCount++;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
int validFileCount = (int) files.stream()
|
|
|
|
|
.filter(file -> !file.isEmpty())
|
|
|
|
|
.count();
|
|
|
|
|
|
|
|
|
|
if (validFileCount > maxFiles) {
|
|
|
|
|
throw new IOException("파일 개수가 제한을 초과했습니다. 최대 " + maxFiles + "개까지 가능합니다.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 디렉토리 생성
|
|
|
|
|
String uploadDir = uploadPath + File.separator + subDir;
|
|
|
|
|
createDirectoryIfNotExists(uploadDir);
|
|
|
|
|
|
|
|
|
|
// 파일 확장자 목록
|
|
|
|
|
String[] extensions = allowedExtensions.split(",");
|
|
|
|
|
/**
|
|
|
|
|
* 파일 업로드 처리
|
|
|
|
|
* @param files 업로드할 파일 목록
|
|
|
|
|
* @param subDir 저장할 하위 디렉토리
|
|
|
|
|
* @param uploadDir 업로드 디렉토리 전체 경로
|
|
|
|
|
* @return 업로드된 파일 정보 목록
|
|
|
|
|
* @throws IOException 파일 처리 중 오류 발생 시
|
|
|
|
|
*/
|
|
|
|
|
private List<FileVO> processFileUploads(List<MultipartFile> files, String subDir, String uploadDir) throws IOException {
|
|
|
|
|
List<FileVO> uploadedFiles = new ArrayList<>();
|
|
|
|
|
long totalSize = 0;
|
|
|
|
|
|
|
|
|
|
for (MultipartFile file : files) {
|
|
|
|
|
if (file.isEmpty()) continue;
|
|
|
|
|
|
|
|
|
|
// 파일 크기 검증
|
|
|
|
|
if (file.getSize() > maxFileSize * 1024 * 1024) {
|
|
|
|
|
throw new IOException("파일 크기가 제한을 초과했습니다. 최대 " + maxFileSize + "MB까지 가능합니다.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
validateFileSize(file, totalSize);
|
|
|
|
|
totalSize += file.getSize();
|
|
|
|
|
if (totalSize > maxTotalSize * 1024 * 1024) {
|
|
|
|
|
throw new IOException("총 파일 크기가 제한을 초과했습니다. 최대 " + maxTotalSize + "MB까지 가능합니다.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 원본 파일명 및 확장자 추출
|
|
|
|
|
String originalFilename = file.getOriginalFilename();
|
|
|
|
|
String fileExt = getFileExtension(originalFilename);
|
|
|
|
|
|
|
|
|
|
// 확장자 검증
|
|
|
|
|
boolean isAllowedExtension = false;
|
|
|
|
|
for (String ext : extensions) {
|
|
|
|
|
if (ext.trim().equalsIgnoreCase(fileExt)) {
|
|
|
|
|
isAllowedExtension = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// 파일 저장 및 정보 생성
|
|
|
|
|
FileVO fileVO = saveFile(file, subDir, uploadDir);
|
|
|
|
|
uploadedFiles.add(fileVO);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!isAllowedExtension) {
|
|
|
|
|
throw new IOException("허용되지 않은 파일 형식입니다: " + fileExt);
|
|
|
|
|
}
|
|
|
|
|
return uploadedFiles;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// UUID를 이용한 저장 파일명 생성
|
|
|
|
|
String storedFilename = UUID.randomUUID().toString() + "." + fileExt;
|
|
|
|
|
/**
|
|
|
|
|
* 파일 크기 검증
|
|
|
|
|
* @param file 검증할 파일
|
|
|
|
|
* @param currentTotalSize 현재까지의 총 파일 크기
|
|
|
|
|
* @throws IOException 파일 크기 제한 초과 시
|
|
|
|
|
*/
|
|
|
|
|
private void validateFileSize(MultipartFile file, long currentTotalSize) throws IOException {
|
|
|
|
|
// 단일 파일 크기 검증
|
|
|
|
|
if (file.getSize() > maxFileSize * 1024 * 1024) {
|
|
|
|
|
throw new IOException("파일 크기가 제한을 초과했습니다. 최대 " + maxFileSize + "MB까지 가능합니다.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 파일 저장
|
|
|
|
|
Path filePath = Paths.get(uploadDir, storedFilename);
|
|
|
|
|
Files.write(filePath, file.getBytes());
|
|
|
|
|
// 총 파일 크기 검증
|
|
|
|
|
if (currentTotalSize + file.getSize() > maxTotalSize * 1024 * 1024) {
|
|
|
|
|
throw new IOException("총 파일 크기가 제한을 초과했습니다. 최대 " + maxTotalSize + "MB까지 가능합니다.");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 파일 정보 생성
|
|
|
|
|
FileVO fileVO = new FileVO();
|
|
|
|
|
fileVO.setOriginalFileNm(originalFilename);
|
|
|
|
|
fileVO.setStoredFileNm(storedFilename);
|
|
|
|
|
fileVO.setFilePath(subDir);
|
|
|
|
|
fileVO.setFileSize(file.getSize());
|
|
|
|
|
fileVO.setFileExt(fileExt);
|
|
|
|
|
fileVO.setContentType(file.getContentType());
|
|
|
|
|
/**
|
|
|
|
|
* 파일 저장 및 정보 생성
|
|
|
|
|
* @param file 저장할 파일
|
|
|
|
|
* @param subDir 저장할 하위 디렉토리
|
|
|
|
|
* @param uploadDir 업로드 디렉토리 전체 경로
|
|
|
|
|
* @return 생성된 파일 정보
|
|
|
|
|
* @throws IOException 파일 저장 중 오류 발생 시
|
|
|
|
|
*/
|
|
|
|
|
private FileVO saveFile(MultipartFile file, String subDir, String uploadDir) throws IOException {
|
|
|
|
|
// 원본 파일명 및 확장자 추출
|
|
|
|
|
String originalFilename = file.getOriginalFilename();
|
|
|
|
|
String fileExt = getFileExtension(originalFilename);
|
|
|
|
|
|
|
|
|
|
uploadedFiles.add(fileVO);
|
|
|
|
|
// 확장자 검증
|
|
|
|
|
if (!isAllowedExtension(fileExt)) {
|
|
|
|
|
throw new IOException("허용되지 않은 파일 형식입니다: " + fileExt);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return uploadedFiles;
|
|
|
|
|
// UUID를 이용한 저장 파일명 생성
|
|
|
|
|
String storedFilename = UUID.randomUUID() + "." + fileExt;
|
|
|
|
|
|
|
|
|
|
// 파일명 검증
|
|
|
|
|
validatePath(storedFilename, true);
|
|
|
|
|
|
|
|
|
|
// 파일 저장 경로 생성 및 검증
|
|
|
|
|
Path filePath = createAndValidateFilePath(uploadDir, storedFilename);
|
|
|
|
|
|
|
|
|
|
// 파일 저장
|
|
|
|
|
Files.write(filePath, file.getBytes());
|
|
|
|
|
|
|
|
|
|
// 파일 정보 생성 및 반환
|
|
|
|
|
return createFileVO(originalFilename, storedFilename, subDir,
|
|
|
|
|
file.getSize(), fileExt, file.getContentType());
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 파일 저장 경로 생성 및 검증
|
|
|
|
|
* @param uploadDir 업로드 디렉토리 전체 경로
|
|
|
|
|
* @param storedFilename 저장 파일명
|
|
|
|
|
* @return 검증된 파일 경로
|
|
|
|
|
* @throws IOException 경로 검증 실패 시
|
|
|
|
|
*/
|
|
|
|
|
private Path createAndValidateFilePath(String uploadDir, String storedFilename) throws IOException {
|
|
|
|
|
Path filePath = Paths.get(uploadDir).normalize().resolve(storedFilename).normalize();
|
|
|
|
|
|
|
|
|
|
// 생성된 경로가 업로드 디렉토리 내에 있는지 확인
|
|
|
|
|
File targetFile = filePath.toFile();
|
|
|
|
|
String canonicalUploadDir = new File(uploadDir).getCanonicalPath();
|
|
|
|
|
String canonicalTargetPath = targetFile.getCanonicalPath();
|
|
|
|
|
|
|
|
|
|
if (!canonicalTargetPath.startsWith(canonicalUploadDir)) {
|
|
|
|
|
throw new IOException("잘못된 파일 경로입니다.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return filePath;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@ -139,6 +281,30 @@ public class FileUtil {
|
|
|
|
|
* @throws IOException 파일 처리 중 오류 발생 시
|
|
|
|
|
*/
|
|
|
|
|
public void downloadFile(FileVO fileVO, HttpServletRequest request, HttpServletResponse response) throws IOException {
|
|
|
|
|
// 파일 정보 검증 및 파일 객체 생성
|
|
|
|
|
File file = validateAndGetFile(fileVO);
|
|
|
|
|
|
|
|
|
|
// 파일 확장자 검증
|
|
|
|
|
validateFileExtension(fileVO.getOriginalFileNm());
|
|
|
|
|
|
|
|
|
|
// 브라우저별 인코딩된 파일명 생성
|
|
|
|
|
String userAgent = request.getHeader("User-Agent");
|
|
|
|
|
String encodedFilename = getEncodedFilename(fileVO.getOriginalFileNm(), userAgent);
|
|
|
|
|
|
|
|
|
|
// 응답 헤더 설정
|
|
|
|
|
setResponseHeaders(response, fileVO, file, encodedFilename);
|
|
|
|
|
|
|
|
|
|
// 파일 전송
|
|
|
|
|
sendFile(file, response);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 파일 정보 검증 및 파일 객체 생성
|
|
|
|
|
* @param fileVO 파일 정보
|
|
|
|
|
* @return 검증된 파일 객체
|
|
|
|
|
* @throws IOException 파일 검증 실패 시
|
|
|
|
|
*/
|
|
|
|
|
private File validateAndGetFile(FileVO fileVO) throws IOException {
|
|
|
|
|
// 파일 경로 생성
|
|
|
|
|
String filePath = uploadPath + File.separator + fileVO.getFilePath() + File.separator + fileVO.getStoredFileNm();
|
|
|
|
|
File file = new File(filePath);
|
|
|
|
|
@ -154,53 +320,53 @@ public class FileUtil {
|
|
|
|
|
throw new IOException("잘못된 파일 경로입니다.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 파일 확장자 검증
|
|
|
|
|
String fileExt = getFileExtension(fileVO.getOriginalFileNm());
|
|
|
|
|
String[] extensions = allowedExtensions.split(",");
|
|
|
|
|
boolean isAllowedExtension = false;
|
|
|
|
|
for (String ext : extensions) {
|
|
|
|
|
if (ext.trim().equalsIgnoreCase(fileExt)) {
|
|
|
|
|
isAllowedExtension = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return file;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!isAllowedExtension) {
|
|
|
|
|
/**
|
|
|
|
|
* 파일 확장자 검증
|
|
|
|
|
* @param filename 파일명
|
|
|
|
|
* @throws IOException 허용되지 않은 확장자일 경우
|
|
|
|
|
*/
|
|
|
|
|
private void validateFileExtension(String filename) throws IOException {
|
|
|
|
|
String fileExt = getFileExtension(filename);
|
|
|
|
|
if (!isAllowedExtension(fileExt)) {
|
|
|
|
|
throw new IOException("허용되지 않은 파일 형식입니다: " + fileExt);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 브라우저 종류 확인
|
|
|
|
|
String userAgent = request.getHeader("User-Agent");
|
|
|
|
|
String encodedFilename;
|
|
|
|
|
|
|
|
|
|
// 브라우저별 인코딩 처리
|
|
|
|
|
if (userAgent.contains("MSIE") || userAgent.contains("Trident")) {
|
|
|
|
|
// IE
|
|
|
|
|
encodedFilename = URLEncoder.encode(fileVO.getOriginalFileNm(), "UTF-8").replaceAll("\\+", "%20");
|
|
|
|
|
} else if (userAgent.contains("Firefox")) {
|
|
|
|
|
// Firefox
|
|
|
|
|
encodedFilename = "\"" + new String(fileVO.getOriginalFileNm().getBytes("UTF-8"), "ISO-8859-1") + "\"";
|
|
|
|
|
} else if (userAgent.contains("Chrome")) {
|
|
|
|
|
// Chrome
|
|
|
|
|
encodedFilename = URLEncoder.encode(fileVO.getOriginalFileNm(), "UTF-8").replaceAll("\\+", "%20");
|
|
|
|
|
} else {
|
|
|
|
|
// 기타 브라우저
|
|
|
|
|
encodedFilename = URLEncoder.encode(fileVO.getOriginalFileNm(), "UTF-8");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 응답 헤더 설정
|
|
|
|
|
/**
|
|
|
|
|
* 응답 헤더 설정
|
|
|
|
|
* @param response HTTP 응답
|
|
|
|
|
* @param fileVO 파일 정보
|
|
|
|
|
* @param file 파일 객체
|
|
|
|
|
* @param encodedFilename 인코딩된 파일명
|
|
|
|
|
*/
|
|
|
|
|
private void setResponseHeaders(HttpServletResponse response, FileVO fileVO, File file, String encodedFilename) {
|
|
|
|
|
// 컨텐츠 타입 및 길이 설정
|
|
|
|
|
response.setContentType(fileVO.getContentType());
|
|
|
|
|
response.setContentLength((int) file.length());
|
|
|
|
|
|
|
|
|
|
// 다운로드 관련 헤더 설정
|
|
|
|
|
response.setHeader("Content-Disposition", "attachment; filename=" + encodedFilename);
|
|
|
|
|
response.setHeader("Content-Transfer-Encoding", "binary");
|
|
|
|
|
|
|
|
|
|
// 캐시 관련 헤더 설정
|
|
|
|
|
response.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
|
|
|
|
|
response.setHeader("Pragma", "no-cache");
|
|
|
|
|
response.setHeader("Expires", "0");
|
|
|
|
|
|
|
|
|
|
// XSS 방지를 위한 헤더 설정
|
|
|
|
|
// 보안 관련 헤더 설정
|
|
|
|
|
response.setHeader("X-Content-Type-Options", "nosniff");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 파일 전송
|
|
|
|
|
/**
|
|
|
|
|
* 파일 전송
|
|
|
|
|
* @param file 전송할 파일
|
|
|
|
|
* @param response HTTP 응답
|
|
|
|
|
* @throws IOException 파일 전송 중 오류 발생 시
|
|
|
|
|
*/
|
|
|
|
|
private void sendFile(File file, HttpServletResponse response) throws IOException {
|
|
|
|
|
try (FileInputStream fis = new FileInputStream(file);
|
|
|
|
|
OutputStream os = response.getOutputStream()) {
|
|
|
|
|
|
|
|
|
|
@ -225,15 +391,44 @@ public class FileUtil {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
String filePath = uploadPath + File.separator + fileVO.getFilePath() + File.separator + fileVO.getStoredFileNm();
|
|
|
|
|
try {
|
|
|
|
|
// 파일 경로 검증 및 파일 객체 생성
|
|
|
|
|
File file = getValidatedFileForDelete(fileVO);
|
|
|
|
|
|
|
|
|
|
// 파일 존재 여부 확인 및 삭제
|
|
|
|
|
return file.exists() && file.delete();
|
|
|
|
|
} catch (IOException e) {
|
|
|
|
|
// 로그 기록 등의 처리를 추가할 수 있음
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 삭제를 위한 파일 객체 생성 및 검증
|
|
|
|
|
* @param fileVO 파일 정보
|
|
|
|
|
* @return 검증된 파일 객체
|
|
|
|
|
* @throws IOException 파일 경로 검증 실패 시
|
|
|
|
|
*/
|
|
|
|
|
private File getValidatedFileForDelete(FileVO fileVO) throws IOException {
|
|
|
|
|
// 파일 경로 및 파일명 검증
|
|
|
|
|
String subDir = fileVO.getFilePath();
|
|
|
|
|
String storedFilename = fileVO.getStoredFileNm();
|
|
|
|
|
|
|
|
|
|
// 경로 검증
|
|
|
|
|
validatePath(subDir, false);
|
|
|
|
|
validatePath(storedFilename, true);
|
|
|
|
|
|
|
|
|
|
// 파일 경로 생성
|
|
|
|
|
String filePath = uploadPath + File.separator + subDir + File.separator + storedFilename;
|
|
|
|
|
File file = new File(filePath);
|
|
|
|
|
|
|
|
|
|
// 파일 존재 여부 확인 및 삭제
|
|
|
|
|
if (file.exists()) {
|
|
|
|
|
return file.delete();
|
|
|
|
|
// 보안 검사: 경로 검증 (경로 탐색 공격 방지)
|
|
|
|
|
String canonicalPath = file.getCanonicalPath();
|
|
|
|
|
if (!canonicalPath.startsWith(new File(uploadPath).getCanonicalPath())) {
|
|
|
|
|
throw new IOException("잘못된 파일 경로입니다.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
return file;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
@ -250,12 +445,18 @@ public class FileUtil {
|
|
|
|
|
/**
|
|
|
|
|
* 파일 확장자 추출
|
|
|
|
|
* @param filename 파일명
|
|
|
|
|
* @return 파일 확장자
|
|
|
|
|
* @return 파일 확장자 (소문자로 변환됨)
|
|
|
|
|
*/
|
|
|
|
|
private String getFileExtension(String filename) {
|
|
|
|
|
if (filename == null || filename.isEmpty() || !filename.contains(".")) {
|
|
|
|
|
if (filename == null || filename.isEmpty()) {
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
return filename.substring(filename.lastIndexOf(".") + 1).toLowerCase();
|
|
|
|
|
|
|
|
|
|
int lastDotIndex = filename.lastIndexOf(".");
|
|
|
|
|
if (lastDotIndex == -1 || lastDotIndex == filename.length() - 1) {
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return filename.substring(lastDotIndex + 1).toLowerCase();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|