대용량 다운로드 테스트
This commit is contained in:
@@ -5,7 +5,6 @@ import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import org.springframework.core.io.FileSystemResource;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.core.io.support.ResourceRegion;
|
||||
@@ -15,43 +14,11 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* Range(이어받기) 지원 파일 다운로드 응답 생성기.
|
||||
*
|
||||
* <p>- Range 헤더 없으면 200 + Resource - Range 헤더 있으면 206 + ResourceRegion - 잘못된 Range면 416 +
|
||||
* Content-Range: bytes
|
||||
*
|
||||
* <p>주의: - 다중 Range는 첫 번째 Range만 처리(안정성 목적). - 파일이 이미 생성되어 있는 대용량(zip 등) 다운로드에 적합.
|
||||
*/
|
||||
@Component
|
||||
public class RangeDownloadResponder {
|
||||
|
||||
/** 기본(zip) 다운로드 응답 생성. */
|
||||
public ResponseEntity<?> buildZipResponse(
|
||||
Path filePath, String downloadFileName, HttpServletRequest request) throws IOException {
|
||||
return buildResponse(
|
||||
filePath, downloadFileName, MediaType.APPLICATION_OCTET_STREAM, request, Map.of());
|
||||
}
|
||||
|
||||
/** 추가 헤더가 필요한 경우(예: X-Accel-Buffering) 사용. */
|
||||
public ResponseEntity<?> buildZipResponse(
|
||||
Path filePath,
|
||||
String downloadFileName,
|
||||
HttpServletRequest request,
|
||||
Map<String, String> extraHeaders)
|
||||
throws IOException {
|
||||
return buildResponse(
|
||||
filePath, downloadFileName, MediaType.APPLICATION_OCTET_STREAM, request, extraHeaders);
|
||||
}
|
||||
|
||||
/** 콘텐츠 타입까지 지정하고 싶을 때 사용. */
|
||||
public ResponseEntity<?> buildResponse(
|
||||
Path filePath,
|
||||
String downloadFileName,
|
||||
MediaType contentType,
|
||||
HttpServletRequest request,
|
||||
Map<String, String> extraHeaders)
|
||||
throws IOException {
|
||||
|
||||
if (!Files.isRegularFile(filePath)) {
|
||||
return ResponseEntity.notFound().build();
|
||||
@@ -60,73 +27,53 @@ public class RangeDownloadResponder {
|
||||
long totalSize = Files.size(filePath);
|
||||
Resource resource = new FileSystemResource(filePath);
|
||||
|
||||
// 공통 헤더(200/206 공통)
|
||||
String disposition = "attachment; filename=\"" + downloadFileName + "\"";
|
||||
String rangeHeader = request.getHeader(HttpHeaders.RANGE);
|
||||
|
||||
if (rangeHeader == null || rangeHeader.isBlank()) {
|
||||
// 200 OK (전체 다운로드)
|
||||
ResponseEntity.BodyBuilder ok =
|
||||
// 🔥 공통 헤더 (여기 고정)
|
||||
ResponseEntity.BodyBuilder base =
|
||||
ResponseEntity.ok()
|
||||
.contentType(contentType)
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, disposition)
|
||||
.header(HttpHeaders.ACCEPT_RANGES, "bytes")
|
||||
.contentLength(totalSize);
|
||||
.header("X-Accel-Buffering", "no");
|
||||
|
||||
applyExtraHeaders(ok, extraHeaders);
|
||||
return ok.body(resource);
|
||||
if (rangeHeader == null || rangeHeader.isBlank()) {
|
||||
return base.contentLength(totalSize).body(resource);
|
||||
}
|
||||
|
||||
// Range 파싱
|
||||
List<HttpRange> ranges;
|
||||
try {
|
||||
ranges = HttpRange.parseRanges(rangeHeader);
|
||||
} catch (IllegalArgumentException ex) {
|
||||
// 416 Range Not Satisfiable
|
||||
ResponseEntity.BodyBuilder badRange =
|
||||
ResponseEntity.status(416).header(HttpHeaders.CONTENT_RANGE, "bytes */" + totalSize);
|
||||
applyExtraHeaders(badRange, extraHeaders);
|
||||
return badRange.build();
|
||||
return ResponseEntity.status(416)
|
||||
.header(HttpHeaders.CONTENT_RANGE, "bytes */" + totalSize)
|
||||
.header("X-Accel-Buffering", "no")
|
||||
.build();
|
||||
}
|
||||
|
||||
// 실무에선 단일 Range만 처리하는 게 안전합니다.
|
||||
HttpRange range = ranges.get(0);
|
||||
|
||||
long start = range.getRangeStart(totalSize);
|
||||
long end = range.getRangeEnd(totalSize);
|
||||
|
||||
if (start >= totalSize) {
|
||||
ResponseEntity.BodyBuilder badRange =
|
||||
ResponseEntity.status(416).header(HttpHeaders.CONTENT_RANGE, "bytes */" + totalSize);
|
||||
applyExtraHeaders(badRange, extraHeaders);
|
||||
return badRange.build();
|
||||
return ResponseEntity.status(416)
|
||||
.header(HttpHeaders.CONTENT_RANGE, "bytes */" + totalSize)
|
||||
.header("X-Accel-Buffering", "no")
|
||||
.build();
|
||||
}
|
||||
|
||||
long regionLength = end - start + 1;
|
||||
ResourceRegion region = new ResourceRegion(resource, start, regionLength);
|
||||
|
||||
// 206 Partial Content
|
||||
ResponseEntity.BodyBuilder partial =
|
||||
ResponseEntity.status(206)
|
||||
.contentType(contentType)
|
||||
return ResponseEntity.status(206)
|
||||
.contentType(MediaType.APPLICATION_OCTET_STREAM)
|
||||
.header(HttpHeaders.CONTENT_DISPOSITION, disposition)
|
||||
.header(HttpHeaders.ACCEPT_RANGES, "bytes")
|
||||
.header("X-Accel-Buffering", "no")
|
||||
.header(HttpHeaders.CONTENT_RANGE, "bytes " + start + "-" + end + "/" + totalSize)
|
||||
.contentLength(regionLength);
|
||||
|
||||
applyExtraHeaders(partial, extraHeaders);
|
||||
return partial.body(region);
|
||||
}
|
||||
|
||||
private void applyExtraHeaders(
|
||||
ResponseEntity.HeadersBuilder<?> builder, Map<String, String> extraHeaders) {
|
||||
if (extraHeaders == null || extraHeaders.isEmpty()) return;
|
||||
|
||||
extraHeaders.forEach(
|
||||
(k, v) -> {
|
||||
if (k != null && !k.isBlank() && v != null) {
|
||||
builder.header(k, v);
|
||||
}
|
||||
});
|
||||
.contentLength(regionLength)
|
||||
.body(region);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -374,8 +374,7 @@ public class InferenceResultApiController {
|
||||
Path zipPath = Path.of(path);
|
||||
|
||||
// Range + 200/206/416 공통 처리 (추가 헤더 포함)
|
||||
return rangeDownloadResponder.buildZipResponse(
|
||||
zipPath, uid + ".zip", request, Map.of("X-Accel-Buffering", "no"));
|
||||
return rangeDownloadResponder.buildZipResponse(zipPath, uid + ".zip", request);
|
||||
}
|
||||
|
||||
@Operation(summary = "shp 파일 다운로드 이력 조회", description = "추론관리 분석결과 shp 파일 다운로드 이력 조회")
|
||||
|
||||
Reference in New Issue
Block a user