diff --git a/src/main/java/com/kamco/cd/kamcoback/common/download/DownloadExecutor.java b/src/main/java/com/kamco/cd/kamcoback/common/download/DownloadExecutor.java new file mode 100644 index 00000000..eb778736 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/common/download/DownloadExecutor.java @@ -0,0 +1,48 @@ +package com.kamco.cd.kamcoback.common.download; + +import com.kamco.cd.kamcoback.common.download.dto.DownloadSpec; +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.Files; +import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; + +@Service +@RequiredArgsConstructor +public class DownloadExecutor { + + public ResponseEntity stream(DownloadSpec spec) throws IOException { + + if (!Files.isReadable(spec.filePath())) { + return ResponseEntity.notFound().build(); + } + + long fileSize = Files.size(spec.filePath()); + + StreamingResponseBody body = + os -> { + try (InputStream in = Files.newInputStream(spec.filePath())) { + in.transferTo(os); + os.flush(); + } catch (Exception e) { + // 고용량은 중간 끊김 흔하니까 throw 금지 + } + }; + + String fileName = + spec.downloadName() != null + ? spec.downloadName() + : spec.filePath().getFileName().toString(); + + return ResponseEntity.ok() + .contentType( + spec.contentType() != null ? spec.contentType() : MediaType.APPLICATION_OCTET_STREAM) + .contentLength(fileSize) + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"") + .body(body); + } +} diff --git a/src/main/java/com/kamco/cd/kamcoback/common/download/dto/DownloadSpec.java b/src/main/java/com/kamco/cd/kamcoback/common/download/dto/DownloadSpec.java new file mode 100644 index 00000000..bc3b5570 --- /dev/null +++ b/src/main/java/com/kamco/cd/kamcoback/common/download/dto/DownloadSpec.java @@ -0,0 +1,12 @@ +package com.kamco.cd.kamcoback.common.download.dto; + +import java.nio.file.Path; +import java.util.UUID; +import org.springframework.http.MediaType; + +public record DownloadSpec( + UUID uuid, // 다운로드 식별(로그/정책용) + Path filePath, // 실제 파일 경로 + String downloadName, // 사용자에게 보일 파일명 + MediaType contentType // 보통 OCTET_STREAM + ) {} diff --git a/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java b/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java index 58d44f92..963396d5 100644 --- a/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java +++ b/src/main/java/com/kamco/cd/kamcoback/label/LabelAllocateApiController.java @@ -1,5 +1,7 @@ package com.kamco.cd.kamcoback.label; +import com.kamco.cd.kamcoback.common.download.DownloadExecutor; +import com.kamco.cd.kamcoback.common.download.dto.DownloadSpec; import com.kamco.cd.kamcoback.config.api.ApiResponseDto; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto; import com.kamco.cd.kamcoback.label.dto.LabelAllocateDto.InferenceDetail; @@ -23,17 +25,15 @@ import io.swagger.v3.oas.annotations.responses.ApiResponses; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; 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.UUID; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.coyote.BadRequestException; import org.springframework.beans.factory.annotation.Value; -import org.springframework.core.io.Resource; 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.GetMapping; @@ -43,6 +43,7 @@ import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.mvc.method.annotation.StreamingResponseBody; @Slf4j @Tag(name = "라벨링 작업 관리", description = "라벨링 작업 배정 및 통계 조회 API") @@ -52,6 +53,7 @@ import org.springframework.web.bind.annotation.RestController; public class LabelAllocateApiController { private final LabelAllocateService labelAllocateService; + private final DownloadExecutor downloadExecutor; @Value("${file.dataset-response}") private String responsePath; @@ -380,25 +382,19 @@ public class LabelAllocateApiController { @ApiResponse(responseCode = "500", description = "서버 오류", content = @Content) }) @GetMapping("/download/{uuid}") - public ResponseEntity download( + public ResponseEntity download( @Parameter(example = "6d8d49dc-0c9d-4124-adc7-b9ca610cc394") @PathVariable UUID uuid) throws IOException { - // - // if (!labelAllocateService.isDownloadable(uuid)) { - // throw new BadRequestException(); - // } + + if (!labelAllocateService.isDownloadable(uuid)) { + throw new BadRequestException(); + } + String uid = labelAllocateService.findLearnUid(uuid); Path zipPath = Paths.get(responsePath).resolve(uid + ".zip"); - long size = Files.size(zipPath); - Resource resource = new org.springframework.core.io.UrlResource(zipPath.toUri()); - return ResponseEntity.ok() - .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + uid + ".zip" + "\"") - .header(HttpHeaders.ACCEPT_RANGES, "bytes") - .header("X-Accel-Buffering", "no") // nginx/ingress 버퍼링 방지 힌트 - .contentType(MediaType.APPLICATION_OCTET_STREAM) - .contentLength(size) - .body(resource); + return downloadExecutor.stream( + new DownloadSpec(uuid, zipPath, uid + ".zip", MediaType.APPLICATION_OCTET_STREAM)); } @Operation(summary = "라벨 파일 다운로드 이력 조회", description = "라벨 파일 다운로드 이력 조회") diff --git a/src/main/resources/static/download_progress_test.html b/src/main/resources/static/download_progress_test.html index c0efd7c7..ab4c4501 100644 --- a/src/main/resources/static/download_progress_test.html +++ b/src/main/resources/static/download_progress_test.html @@ -9,6 +9,12 @@ UUID: +

+ +JWT Token: + +

+

@@ -18,11 +24,25 @@ UUID: