|
68 | 68 | import static com.adobe.testing.s3mock.util.HeaderUtil.storeHeadersFrom; |
69 | 69 | import static com.adobe.testing.s3mock.util.HeaderUtil.userMetadataFrom; |
70 | 70 | import static com.adobe.testing.s3mock.util.HeaderUtil.userMetadataHeadersFrom; |
| 71 | +import static org.springframework.http.HttpHeaders.ACCEPT_RANGES; |
| 72 | +import static org.springframework.http.HttpHeaders.CONTENT_RANGE; |
71 | 73 | import static org.springframework.http.HttpHeaders.CONTENT_TYPE; |
72 | 74 | import static org.springframework.http.HttpHeaders.IF_MATCH; |
73 | 75 | import static org.springframework.http.HttpHeaders.IF_MODIFIED_SINCE; |
@@ -288,7 +290,7 @@ public ResponseEntity<Void> headObject( |
288 | 290 |
|
289 | 291 | return ResponseEntity.ok() |
290 | 292 | .eTag(s3ObjectMetadata.etag()) |
291 | | - .header(HttpHeaders.ACCEPT_RANGES, RANGES_BYTES) |
| 293 | + .header(ACCEPT_RANGES, RANGES_BYTES) |
292 | 294 | .lastModified(s3ObjectMetadata.lastModified()) |
293 | 295 | .contentLength(Long.parseLong(s3ObjectMetadata.size())) |
294 | 296 | .contentType(mediaTypeFrom(s3ObjectMetadata.contentType())) |
@@ -408,7 +410,7 @@ public ResponseEntity<StreamingResponseBody> getObject( |
408 | 410 | return ResponseEntity |
409 | 411 | .ok() |
410 | 412 | .eTag(s3ObjectMetadata.etag()) |
411 | | - .header(HttpHeaders.ACCEPT_RANGES, RANGES_BYTES) |
| 413 | + .header(ACCEPT_RANGES, RANGES_BYTES) |
412 | 414 | .lastModified(s3ObjectMetadata.lastModified()) |
413 | 415 | .contentLength(Long.parseLong(s3ObjectMetadata.size())) |
414 | 416 | .contentType(mediaTypeFrom(s3ObjectMetadata.contentType())) |
@@ -973,48 +975,52 @@ public ResponseEntity<CopyObjectResult> copyObject( |
973 | 975 | .body(new CopyObjectResult(copyS3ObjectMetadata)); |
974 | 976 | } |
975 | 977 |
|
976 | | - /** |
977 | | - * Supports returning different ranges of an object. |
978 | | - * E.g., if content has 100 bytes, the range request could be: bytes=10-100, 10--1 and 10-200 |
979 | | - * <a href="https://docs.aws.amazon.com/AmazonS3/latest/API/API_GetObject.html">API Reference</a> |
980 | | - * |
981 | | - * @param range {@link String} |
982 | | - * @param s3ObjectMetadata {@link S3ObjectMetadata} |
983 | | - */ |
984 | | - private ResponseEntity<StreamingResponseBody> getObjectWithRange(HttpRange range, |
985 | | - S3ObjectMetadata s3ObjectMetadata) { |
| 978 | + private ResponseEntity<StreamingResponseBody> getObjectWithRange( |
| 979 | + HttpRange range, |
| 980 | + S3ObjectMetadata s3ObjectMetadata |
| 981 | + ) { |
986 | 982 | var fileSize = s3ObjectMetadata.dataPath().toFile().length(); |
987 | | - var bytesToRead = Math.min(fileSize - 1, range.getRangeEnd(fileSize)) |
988 | | - - range.getRangeStart(fileSize) + 1; |
| 983 | + var startInclusive = range.getRangeStart(fileSize); |
| 984 | + var endInclusive = Math.min(fileSize - 1, range.getRangeEnd(fileSize)); |
| 985 | + var contentLength = endInclusive - startInclusive + 1; |
989 | 986 |
|
990 | | - if (bytesToRead < 0 || fileSize < range.getRangeStart(fileSize)) { |
991 | | - return ResponseEntity.status(REQUESTED_RANGE_NOT_SATISFIABLE.value()).build(); |
| 987 | + if (contentLength < 0 || fileSize <= startInclusive) { |
| 988 | + return ResponseEntity.status(REQUESTED_RANGE_NOT_SATISFIABLE).build(); |
992 | 989 | } |
993 | 990 |
|
994 | 991 | return ResponseEntity |
995 | | - .status(PARTIAL_CONTENT.value()) |
996 | | - .headers(headers -> headers.setAll(userMetadataHeadersFrom(s3ObjectMetadata))) |
997 | | - .headers(headers -> headers.setAll(s3ObjectMetadata.storeHeaders())) |
998 | | - .headers(headers -> headers.setAll(s3ObjectMetadata.encryptionHeaders())) |
999 | | - .header(HttpHeaders.ACCEPT_RANGES, RANGES_BYTES) |
1000 | | - .header(HttpHeaders.CONTENT_RANGE, |
1001 | | - String.format("bytes %s-%s/%s", |
1002 | | - range.getRangeStart(fileSize), bytesToRead + range.getRangeStart(fileSize) - 1, |
1003 | | - s3ObjectMetadata.size())) |
| 992 | + .status(PARTIAL_CONTENT) |
| 993 | + .headers(headers -> applyS3MetadataHeaders(headers, s3ObjectMetadata)) |
| 994 | + .header(ACCEPT_RANGES, RANGES_BYTES) |
| 995 | + .header(CONTENT_RANGE, String.format("bytes %d-%d/%d", startInclusive, endInclusive, fileSize)) |
1004 | 996 | .eTag(s3ObjectMetadata.etag()) |
1005 | 997 | .contentType(mediaTypeFrom(s3ObjectMetadata.contentType())) |
1006 | 998 | .lastModified(s3ObjectMetadata.lastModified()) |
1007 | | - .contentLength(bytesToRead) |
| 999 | + .contentLength(contentLength) |
1008 | 1000 | .body(outputStream -> |
1009 | | - extractBytesToOutputStream(range, s3ObjectMetadata, outputStream, fileSize, bytesToRead) |
| 1001 | + extractBytesToOutputStream(startInclusive, s3ObjectMetadata, outputStream, contentLength) |
1010 | 1002 | ); |
1011 | 1003 | } |
1012 | 1004 |
|
1013 | | - private static void extractBytesToOutputStream(HttpRange range, S3ObjectMetadata s3ObjectMetadata, |
1014 | | - OutputStream outputStream, long fileSize, long bytesToRead) throws IOException { |
| 1005 | + private void applyS3MetadataHeaders(HttpHeaders headers, S3ObjectMetadata metadata) { |
| 1006 | + headers.setAll(userMetadataHeadersFrom(metadata)); |
| 1007 | + if (metadata.storeHeaders() != null) { |
| 1008 | + headers.setAll(metadata.storeHeaders()); |
| 1009 | + } |
| 1010 | + if (metadata.encryptionHeaders() != null) { |
| 1011 | + headers.setAll(metadata.encryptionHeaders()); |
| 1012 | + } |
| 1013 | + } |
| 1014 | + |
| 1015 | + private static void extractBytesToOutputStream( |
| 1016 | + long startOffset, |
| 1017 | + S3ObjectMetadata s3ObjectMetadata, |
| 1018 | + OutputStream outputStream, |
| 1019 | + long bytesToRead |
| 1020 | + ) throws IOException { |
1015 | 1021 | try (var fis = Files.newInputStream(s3ObjectMetadata.dataPath())) { |
1016 | | - var skip = fis.skip(range.getRangeStart(fileSize)); |
1017 | | - if (skip == range.getRangeStart(fileSize)) { |
| 1022 | + var skipped = fis.skip(startOffset); |
| 1023 | + if (skipped == startOffset) { |
1018 | 1024 | try (var bis = BoundedInputStream |
1019 | 1025 | .builder() |
1020 | 1026 | .setInputStream(fis) |
|
0 commit comments