대용량 다운로드 테스트
This commit is contained in:
@@ -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<StreamingResponseBody> 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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
) {}
|
||||
@@ -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<Resource> download(
|
||||
public ResponseEntity<StreamingResponseBody> 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 = "라벨 파일 다운로드 이력 조회")
|
||||
|
||||
@@ -9,6 +9,12 @@
|
||||
|
||||
UUID:
|
||||
<input id="uuid" value="6d8d49dc-0c9d-4124-adc7-b9ca610cc394" />
|
||||
<br><br>
|
||||
|
||||
JWT Token:
|
||||
<input id="token" style="width:600px;" placeholder="Bearer 토큰 붙여넣기" />
|
||||
<br><br>
|
||||
|
||||
<button onclick="download()">다운로드</button>
|
||||
|
||||
<br><br>
|
||||
@@ -18,11 +24,25 @@ UUID:
|
||||
<script>
|
||||
async function download() {
|
||||
const uuid = document.getElementById("uuid").value.trim();
|
||||
const token = document.getElementById("token").value.trim();
|
||||
|
||||
if (!uuid) {
|
||||
alert("UUID 입력하세요");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!token) {
|
||||
alert("토큰 입력하세요");
|
||||
return;
|
||||
}
|
||||
|
||||
const url = `/api/training-data/stage/download/${uuid}`;
|
||||
|
||||
const res = await fetch(url, {
|
||||
headers: {
|
||||
"Authorization": `Bearer eyJhbGciOiJIUzM4NCJ9.eyJzdWIiOiJmYzUwNDYxZC0wOGYwLTRhYjYtOTg4MC03ZmJmYzM2ZjNmOGIiLCJpYXQiOjE3NzA3Nzg3ODIsImV4cCI6MTc3MDg2NTE4Mn0.4OOF4r2lxymr8UwpqRW_qLecE8IpwNOFJHXSsZX6d6Yuk2fT7NcYPS5LenFRli3N`,
|
||||
"Authorization": token.startsWith("Bearer ")
|
||||
? token
|
||||
: `Bearer ${token}`,
|
||||
"kamco-download-uuid": uuid
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user