학습결과 파일 베스트 에폭 제외 삭제 추가, 납품데이터 등록 비동기 수정
This commit is contained in:
@@ -330,8 +330,27 @@ public class ModelTrainDetailApiController {
|
||||
}
|
||||
|
||||
@Operation(
|
||||
summary = "모델관리 > 모델 상세 > best epoch 제외 삭제",
|
||||
description = "best epoch 제외 pth 파일 삭제 API")
|
||||
summary = "모델관리 > 모델 상세 > best epoch 제외 삭제 될 파일 미리보기",
|
||||
description = "best epoch 제외 삭제 될 파일 미리보기 API")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
responseCode = "200",
|
||||
description = "조회 성공",
|
||||
content =
|
||||
@Content(
|
||||
mediaType = "application/json",
|
||||
schema = @Schema(implementation = CleanupResult.class))),
|
||||
@ApiResponse(responseCode = "404", description = "데이터셋을 찾을 수 없음", content = @Content),
|
||||
@ApiResponse(responseCode = "500", description = "서버 오류", content = @Content)
|
||||
})
|
||||
@GetMapping("/{uuid}/cleanup/preview")
|
||||
public ApiResponseDto<CleanupResult> previewCleanup(
|
||||
@Parameter(description = "모델 uuid") @PathVariable UUID uuid) {
|
||||
return ApiResponseDto.ok(modelTrainDetailService.previewCleanup(uuid));
|
||||
}
|
||||
|
||||
@Operation(summary = "모델관리 > 모델 상세 > best epoch 제외 삭제", description = "best epoch 제외 파일 삭제 API")
|
||||
@ApiResponses(
|
||||
value = {
|
||||
@ApiResponse(
|
||||
|
||||
@@ -346,5 +346,8 @@ public class ModelTrainMngDto {
|
||||
|
||||
// 유지된 파일명 (best epoch 기준)
|
||||
private String keptFile;
|
||||
|
||||
// 삭제 될 파일
|
||||
private List<String> deleteTargets;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.kamco.cd.training.model.service;
|
||||
|
||||
import com.kamco.cd.training.common.enums.ModelType;
|
||||
import com.kamco.cd.training.common.enums.TrainStatusType;
|
||||
import com.kamco.cd.training.common.exception.CustomApiException;
|
||||
import com.kamco.cd.training.dataset.dto.DatasetDto.DatasetReq;
|
||||
import com.kamco.cd.training.dataset.dto.DatasetDto.SelectTransferDataSet;
|
||||
@@ -22,10 +23,17 @@ import com.kamco.cd.training.model.dto.ModelTrainMngDto.ModelProgressStepDto;
|
||||
import com.kamco.cd.training.postgres.core.ModelTrainDetailCoreService;
|
||||
import com.kamco.cd.training.postgres.core.ModelTrainMngCoreService;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.AccessDeniedException;
|
||||
import java.nio.file.FileVisitOption;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.ArrayList;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import java.util.stream.Stream;
|
||||
@@ -167,15 +175,16 @@ public class ModelTrainDetailService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 베스트 에폭 제외 *.pth 파일 삭제
|
||||
* 삭제될 파일목록 및 유지될 파일 목록
|
||||
*
|
||||
* @param uuid 학습 모델 uuid
|
||||
* @param uuid
|
||||
* @return
|
||||
*/
|
||||
public CleanupResult cleanup(UUID uuid) {
|
||||
public CleanupResult previewCleanup(UUID uuid) {
|
||||
|
||||
CleanupResult result = new CleanupResult();
|
||||
|
||||
// 학습 정보 조회
|
||||
// ===== 모델 조회 =====
|
||||
ModelTrainMngDto.Basic model = modelTrainDetailCoreService.findByModelByUUID(uuid);
|
||||
|
||||
if (model == null) {
|
||||
@@ -183,11 +192,98 @@ public class ModelTrainDetailService {
|
||||
"NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "모델을 찾을 수 없습니다. UUID: " + uuid);
|
||||
}
|
||||
|
||||
// 학습 결과 폴더 경로
|
||||
Path dir = Paths.get(responseDir, model.getUuid().toString());
|
||||
Path dir = Paths.get(responseDir, model.getUuid().toString()).toAbsolutePath().normalize();
|
||||
|
||||
if (!Files.exists(dir) || !Files.isDirectory(dir)) {
|
||||
throw new CustomApiException("NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "디렉토리가 없습니다.");
|
||||
}
|
||||
|
||||
if (!Files.isReadable(dir)) {
|
||||
throw new CustomApiException("FORBIDDEN", HttpStatus.FORBIDDEN, "디렉토리 읽기 권한이 없습니다.");
|
||||
}
|
||||
|
||||
try (Stream<Path> stream = Files.list(dir)) {
|
||||
|
||||
List<Path> files = stream.toList();
|
||||
|
||||
if (files.isEmpty()) {
|
||||
throw new CustomApiException("NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "파일이 없습니다.");
|
||||
}
|
||||
|
||||
// ===== keep 파일 찾기 =====
|
||||
Path keep =
|
||||
files.stream()
|
||||
.filter(
|
||||
p -> {
|
||||
String name = p.getFileName().toString();
|
||||
return name.endsWith(".zip") && name.contains(model.getUuid().toString());
|
||||
})
|
||||
.findFirst()
|
||||
.orElseThrow(
|
||||
() ->
|
||||
new CustomApiException(
|
||||
"NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "zip 파일이 없습니다."));
|
||||
|
||||
log.info("유지 파일: {}", keep.getFileName());
|
||||
|
||||
// ===== 결과 세팅 =====
|
||||
result.setTotalCount(files.size());
|
||||
result.setKeptFile(keep.getFileName().toString());
|
||||
|
||||
// ===== 삭제 대상 =====
|
||||
List<String> deleteTargets =
|
||||
files.stream()
|
||||
.filter(
|
||||
p -> !p.toAbsolutePath().normalize().equals(keep.toAbsolutePath().normalize()))
|
||||
.map(p -> p.getFileName().toString())
|
||||
.toList();
|
||||
|
||||
result.setDeleteTargets(deleteTargets);
|
||||
|
||||
log.info(
|
||||
"previewCleanup 완료. total={}, deleteTargets={}",
|
||||
result.getTotalCount(),
|
||||
deleteTargets.size());
|
||||
|
||||
return result;
|
||||
|
||||
} catch (IOException e) {
|
||||
log.error("파일 목록 조회 실패: {}", dir, e);
|
||||
throw new CustomApiException(
|
||||
"INTERNAL_SERVER_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, "파일 목록 조회 실패");
|
||||
}
|
||||
}
|
||||
|
||||
public CleanupResult cleanup(UUID uuid) {
|
||||
// ===== 모델 조회 =====
|
||||
ModelTrainMngDto.Basic model = modelTrainDetailCoreService.findByModelByUUID(uuid);
|
||||
|
||||
if (model == null) {
|
||||
throw new CustomApiException(
|
||||
"NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "모델을 찾을 수 없습니다. UUID: " + uuid);
|
||||
}
|
||||
|
||||
if (!TrainStatusType.COMPLETED.getId().equals(model.getStep2Status())) {
|
||||
throw new CustomApiException("CONFLICT", HttpStatus.CONFLICT, "테스트가 완료되지 않았습니다.");
|
||||
}
|
||||
|
||||
// ===== 경로 =====
|
||||
Path dir = Paths.get(responseDir, model.getUuid().toString()).toAbsolutePath().normalize();
|
||||
|
||||
return executeCleanup(model, dir);
|
||||
}
|
||||
|
||||
/**
|
||||
* 베스트 에폭 제외 파일 삭제, 베스트 에폭 zip 파일만 남김
|
||||
*
|
||||
* @param model model 정보
|
||||
* @param dir response 폴더 경로
|
||||
* @return 삭제 정보
|
||||
*/
|
||||
public CleanupResult executeCleanup(ModelTrainMngDto.Basic model, Path dir) {
|
||||
CleanupResult result = new CleanupResult();
|
||||
|
||||
if (!Files.exists(dir) || !Files.isDirectory(dir)) {
|
||||
log.info("디렉토리가 없습니다.: {}", dir);
|
||||
throw new CustomApiException("NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "디렉토리가 없습니다.");
|
||||
}
|
||||
|
||||
@@ -199,7 +295,6 @@ public class ModelTrainDetailService {
|
||||
throw new CustomApiException("FORBIDDEN", HttpStatus.FORBIDDEN, "디렉토리 삭제 권한이 없습니다.");
|
||||
}
|
||||
|
||||
// 저장된 best epoch
|
||||
int bestEpoch = model.getBestEpoch();
|
||||
|
||||
if (bestEpoch <= 0) {
|
||||
@@ -211,74 +306,74 @@ public class ModelTrainDetailService {
|
||||
|
||||
try (Stream<Path> stream = Files.list(dir)) {
|
||||
|
||||
List<Path> pthFiles =
|
||||
stream.filter(p -> p.getFileName().toString().endsWith(".pth")).toList();
|
||||
List<Path> files = stream.toList();
|
||||
|
||||
if (pthFiles.isEmpty()) {
|
||||
log.info("pth 파일이 없습니다.");
|
||||
throw new CustomApiException("NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "pth 파일이 없습니다.");
|
||||
if (files.isEmpty()) {
|
||||
throw new CustomApiException("NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "파일이 없습니다.");
|
||||
}
|
||||
|
||||
// ===== keep 파일 찾기 =====
|
||||
Path keep = null;
|
||||
|
||||
// 우선순위 1: best_*_epoch_{bestEpoch}.pth
|
||||
for (Path p : pthFiles) {
|
||||
for (Path p : files) {
|
||||
String name = p.getFileName().toString();
|
||||
if (name.startsWith("best_") && name.contains("epoch_" + bestEpoch + ".pth")) {
|
||||
if (name.endsWith(".zip") && name.contains(model.getUuid().toString())) {
|
||||
keep = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 우선순위 2: epoch_{bestEpoch}.pth
|
||||
if (keep == null) {
|
||||
for (Path p : pthFiles) {
|
||||
if (p.getFileName().toString().equals("epoch_" + bestEpoch + ".pth")) {
|
||||
keep = p;
|
||||
break;
|
||||
}
|
||||
}
|
||||
throw new CustomApiException("NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "zip 파일이 없습니다.");
|
||||
}
|
||||
|
||||
if (keep == null) {
|
||||
log.info("bestEpoch에 해당하는 파일이 없습니다. epoch={}", bestEpoch);
|
||||
throw new CustomApiException(
|
||||
"NOT_FOUND_DATA", HttpStatus.NOT_FOUND, "bestEpoch에 해당하는 파일이 없습니다. : " + bestEpoch);
|
||||
}
|
||||
log.info("유지 파일: {}", keep.getFileName());
|
||||
|
||||
log.info("유지할 파일: {}", keep.getFileName());
|
||||
|
||||
// ===== 결과 세팅 =====
|
||||
result.setTotalCount(pthFiles.size());
|
||||
result.setTotalCount(files.size());
|
||||
result.setKeptFile(keep.getFileName().toString());
|
||||
|
||||
int deletedCount = 0;
|
||||
List<String> failed = new ArrayList<>();
|
||||
|
||||
// ===== 삭제 처리 =====
|
||||
for (Path p : pthFiles) {
|
||||
// ===== 삭제 =====
|
||||
for (Path p : files) {
|
||||
|
||||
if (!p.toAbsolutePath().normalize().equals(keep.toAbsolutePath().normalize())) {
|
||||
if (p.equals(keep)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
boolean deleted = Files.deleteIfExists(p);
|
||||
try {
|
||||
|
||||
if (deleted) {
|
||||
deletedCount++;
|
||||
log.info("삭제됨: {}", p.getFileName());
|
||||
} else {
|
||||
log.info("이미 없음 (skip): {}", p.getFileName());
|
||||
}
|
||||
|
||||
} catch (IOException e) {
|
||||
failed.add(p.getFileName().toString());
|
||||
log.error("삭제 실패: {}", p.getFileName(), e);
|
||||
// 심볼릭 링크 → 링크만 삭제
|
||||
if (Files.isSymbolicLink(p)) {
|
||||
Files.deleteIfExists(p);
|
||||
log.info("심볼릭 링크 삭제: {}", p.getFileName());
|
||||
}
|
||||
|
||||
// 디렉토리 → 재귀 삭제
|
||||
else if (Files.isDirectory(p, LinkOption.NOFOLLOW_LINKS)) {
|
||||
log.info("디렉토리 재귀 삭제: {}", p.getFileName());
|
||||
deleteDirectory(p);
|
||||
}
|
||||
|
||||
// 일반 파일
|
||||
else {
|
||||
Files.deleteIfExists(p);
|
||||
log.info("파일 삭제: {}", p.getFileName());
|
||||
}
|
||||
|
||||
deletedCount++;
|
||||
|
||||
} catch (AccessDeniedException e) {
|
||||
failed.add(p.getFileName().toString());
|
||||
log.error("권한 없음: {}", p.getFileName(), e);
|
||||
|
||||
} catch (IOException e) {
|
||||
failed.add(p.getFileName().toString());
|
||||
log.error("삭제 실패: {}", p.getFileName(), e);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 결과 저장 =====
|
||||
result.setDeletedCount(deletedCount);
|
||||
result.setFailedCount(failed.size());
|
||||
result.setFailedFiles(failed);
|
||||
@@ -297,4 +392,43 @@ public class ModelTrainDetailService {
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
// 디렉토리 재귀 삭제
|
||||
private void deleteDirectory(Path dir) throws IOException {
|
||||
|
||||
if (!Files.exists(dir, LinkOption.NOFOLLOW_LINKS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// dir 자체가 심볼릭 링크면 링크만 삭제
|
||||
if (Files.isSymbolicLink(dir)) {
|
||||
Files.delete(dir);
|
||||
return;
|
||||
}
|
||||
|
||||
Files.walkFileTree(
|
||||
dir,
|
||||
EnumSet.noneOf(FileVisitOption.class), // NOFOLLOW_LINKS
|
||||
Integer.MAX_VALUE,
|
||||
new SimpleFileVisitor<>() {
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
|
||||
throws IOException {
|
||||
|
||||
Files.delete(file); // 링크면 링크만 삭제됨
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path directory, IOException exc)
|
||||
throws IOException {
|
||||
|
||||
if (exc != null) throw exc;
|
||||
|
||||
Files.delete(directory);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ package com.kamco.cd.training.model.service;
|
||||
import com.kamco.cd.training.common.dto.HyperParam;
|
||||
import com.kamco.cd.training.common.enums.HyperParamSelectType;
|
||||
import com.kamco.cd.training.common.enums.ModelType;
|
||||
import com.kamco.cd.training.common.enums.TrainStatusType;
|
||||
import com.kamco.cd.training.common.enums.TrainType;
|
||||
import com.kamco.cd.training.common.exception.CustomApiException;
|
||||
import com.kamco.cd.training.dataset.dto.DatasetDto.DatasetReq;
|
||||
@@ -14,10 +15,21 @@ import com.kamco.cd.training.model.dto.ModelTrainMngDto.SearchReq;
|
||||
import com.kamco.cd.training.postgres.core.HyperParamCoreService;
|
||||
import com.kamco.cd.training.postgres.core.ModelTrainMngCoreService;
|
||||
import com.kamco.cd.training.train.service.TrainJobService;
|
||||
import java.io.IOException;
|
||||
import java.nio.file.FileVisitOption;
|
||||
import java.nio.file.FileVisitResult;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.LinkOption;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.SimpleFileVisitor;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.util.EnumSet;
|
||||
import java.util.List;
|
||||
import java.util.UUID;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.stereotype.Service;
|
||||
@@ -32,6 +44,13 @@ public class ModelTrainMngService {
|
||||
private final ModelTrainMngCoreService modelTrainMngCoreService;
|
||||
private final HyperParamCoreService hyperParamCoreService;
|
||||
private final TrainJobService trainJobService;
|
||||
private final ModelTrainDetailService modelTrainDetailService;
|
||||
|
||||
@Value("${train.docker.basePath}")
|
||||
private String basePath;
|
||||
|
||||
@Value("${train.docker.responseDir}")
|
||||
private String responseDir;
|
||||
|
||||
/**
|
||||
* 모델학습 조회
|
||||
@@ -46,11 +65,199 @@ public class ModelTrainMngService {
|
||||
/**
|
||||
* 모델학습 삭제
|
||||
*
|
||||
* @param uuid
|
||||
* <p>순서: 1. tmp 구조 검증 (예외 발생 가능) 2. DB 삭제 (트랜잭션) 3. 파일 삭제 (실패해도 로그만)
|
||||
*/
|
||||
@Transactional
|
||||
public void deleteModelTrain(UUID uuid) {
|
||||
|
||||
log.info("deleteModelTrain 시작. uuid={}", uuid);
|
||||
|
||||
// ===== 1. 모델 조회 =====
|
||||
ModelTrainMngDto.Basic model = modelTrainMngCoreService.findModelByUuid(uuid);
|
||||
|
||||
if (model == null) {
|
||||
throw new CustomApiException("NOT_FOUND", HttpStatus.NOT_FOUND, "모델 없음");
|
||||
}
|
||||
|
||||
// ===== 2. 경로 생성 =====
|
||||
Path tmpBase = Path.of(basePath, "tmp").toAbsolutePath().normalize();
|
||||
Path tmp = tmpBase.resolve(model.getRequestPath()).normalize();
|
||||
|
||||
Path responseBase = Paths.get(responseDir).toAbsolutePath().normalize();
|
||||
Path response = responseBase.resolve(model.getUuid().toString()).normalize();
|
||||
|
||||
// ===== 3. 경로 탈출 방지 =====
|
||||
if (!tmp.startsWith(tmpBase)) {
|
||||
throw new CustomApiException("INVALID_PATH", HttpStatus.BAD_REQUEST, "잘못된 tmp 경로");
|
||||
}
|
||||
|
||||
if (!response.startsWith(responseBase)) {
|
||||
throw new CustomApiException("INVALID_PATH", HttpStatus.BAD_REQUEST, "잘못된 response 경로");
|
||||
}
|
||||
|
||||
// ===== 4. 상태 로그 =====
|
||||
log.info(
|
||||
"tmp 상태: exists={}, isDir={}, isSymlink={}",
|
||||
Files.exists(tmp, LinkOption.NOFOLLOW_LINKS),
|
||||
Files.isDirectory(tmp, LinkOption.NOFOLLOW_LINKS),
|
||||
Files.isSymbolicLink(tmp));
|
||||
|
||||
log.info(
|
||||
"response 상태: exists={}, isDir={}, isSymlink={}",
|
||||
Files.exists(response, LinkOption.NOFOLLOW_LINKS),
|
||||
Files.isDirectory(response, LinkOption.NOFOLLOW_LINKS),
|
||||
Files.isSymbolicLink(response));
|
||||
|
||||
// ===== 5. tmp 구조 검증 =====
|
||||
validateTmpStructure(tmp);
|
||||
|
||||
// ===== 6. DB 삭제 =====
|
||||
modelTrainMngCoreService.deleteModel(uuid);
|
||||
log.info("DB 삭제 완료. uuid={}", uuid);
|
||||
|
||||
// ===== 7. tmp 삭제 =====
|
||||
log.info("tmp 삭제 시작: {}", tmp);
|
||||
try {
|
||||
deleteTmpDirectory(tmp);
|
||||
log.info("tmp 삭제 완료: {}", tmp);
|
||||
} catch (Exception e) {
|
||||
log.error("tmp 삭제 실패 (DB는 이미 삭제됨): {}", tmp, e);
|
||||
}
|
||||
|
||||
// ===== 8. response 삭제 =====
|
||||
log.info("response 삭제 시작: {}", response);
|
||||
try {
|
||||
// 테스트 완료되었으면 베스트 에폭은 삭제안함
|
||||
if (TrainStatusType.COMPLETED.getId().equals(model.getStep2Status())) {
|
||||
modelTrainDetailService.executeCleanup(model, response);
|
||||
} else {
|
||||
deleteResponseDirectory(response);
|
||||
}
|
||||
|
||||
log.info("response 삭제 완료: {}", response);
|
||||
} catch (Exception e) {
|
||||
log.error("response 삭제 실패 (DB는 이미 삭제됨): {}", response, e);
|
||||
}
|
||||
|
||||
log.info("deleteModelTrain 완료. uuid={}", uuid);
|
||||
}
|
||||
|
||||
/** tmp 디렉토리 삭제 */
|
||||
private void deleteTmpDirectory(Path dir) throws IOException {
|
||||
|
||||
if (!Files.exists(dir, LinkOption.NOFOLLOW_LINKS)) {
|
||||
log.warn("삭제 대상 없음: {}", dir);
|
||||
return;
|
||||
}
|
||||
|
||||
Files.walkFileTree(
|
||||
dir,
|
||||
EnumSet.noneOf(FileVisitOption.class),
|
||||
Integer.MAX_VALUE,
|
||||
new SimpleFileVisitor<>() {
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
|
||||
throws IOException {
|
||||
|
||||
Files.delete(file);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path directory, IOException exc)
|
||||
throws IOException {
|
||||
|
||||
if (exc != null) {
|
||||
throw exc;
|
||||
}
|
||||
|
||||
Files.delete(directory);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** response 디렉토리 삭제 */
|
||||
private void deleteResponseDirectory(Path dir) throws IOException {
|
||||
|
||||
if (!Files.exists(dir, LinkOption.NOFOLLOW_LINKS)) {
|
||||
log.warn("삭제 대상 없음: {}", dir);
|
||||
return;
|
||||
}
|
||||
|
||||
Files.walkFileTree(
|
||||
dir,
|
||||
EnumSet.noneOf(FileVisitOption.class),
|
||||
Integer.MAX_VALUE,
|
||||
new SimpleFileVisitor<>() {
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
|
||||
throws IOException {
|
||||
|
||||
Files.delete(file);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult postVisitDirectory(Path directory, IOException exc)
|
||||
throws IOException {
|
||||
|
||||
if (exc != null) {
|
||||
throw exc;
|
||||
}
|
||||
|
||||
Files.delete(directory);
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** tmp 내부 구조 검증 - 내부는 반드시 symlink만 허용 */
|
||||
private void validateTmpStructure(Path dir) {
|
||||
|
||||
if (!Files.exists(dir, LinkOption.NOFOLLOW_LINKS)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Files.walkFileTree(
|
||||
dir,
|
||||
EnumSet.noneOf(FileVisitOption.class),
|
||||
Integer.MAX_VALUE,
|
||||
new SimpleFileVisitor<>() {
|
||||
|
||||
@Override
|
||||
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs)
|
||||
throws IOException {
|
||||
|
||||
if (!Files.isSymbolicLink(file)) {
|
||||
log.error("tmp 내부에 일반 파일 존재: {}", file);
|
||||
throw new CustomApiException(
|
||||
"BAD_REQUEST", HttpStatus.BAD_REQUEST, "tmp 내부는 symlink만 허용");
|
||||
}
|
||||
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileVisitResult preVisitDirectory(Path directory, BasicFileAttributes attrs)
|
||||
throws IOException {
|
||||
|
||||
if (!directory.equals(dir) && Files.isSymbolicLink(directory)) {
|
||||
log.error("tmp 내부에 symlink 디렉토리 존재: {}", directory);
|
||||
throw new CustomApiException(
|
||||
"BAD_REQUEST", HttpStatus.BAD_REQUEST, "tmp 내부에 symlink 디렉토리 금지");
|
||||
}
|
||||
|
||||
return FileVisitResult.CONTINUE;
|
||||
}
|
||||
});
|
||||
} catch (IOException e) {
|
||||
throw new CustomApiException(
|
||||
"INTERNAL_SERVER_ERROR", HttpStatus.INTERNAL_SERVER_ERROR, "tmp 구조 검증 실패");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user