Skip to content

Commit

Permalink
perf($OSS): support streaming partial content
Browse files Browse the repository at this point in the history
support streaming partial content by returning resource region;
default byte count is 4 MB

BREAKING CHANGE: support streaming partial content(HTTP 206)
  • Loading branch information
johnnymillergh committed Aug 10, 2021
1 parent adcc201 commit beaa80d
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 16 deletions.
2 changes: 2 additions & 0 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,8 @@ services:
redis-slave-1:
container_name: ${REDIS_SLAVE_1_CONTAINER_NAME}
image: redis:${REDIS_TAG}
depends_on:
- redis-master
ports:
- "6380:6379"
restart: always
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,12 @@
import io.swagger.annotations.ApiOperation;
import lombok.RequiredArgsConstructor;
import org.springframework.core.io.Resource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;

import javax.validation.Valid;
Expand All @@ -31,7 +33,8 @@ public class ReadResourceController {
@GetMapping("/{bucket}/{object}")
@ApiOperation(value = "Get single resource", notes = "Get or download single resource")
public ResponseEntity<Resource> getSingleResource(@PathVariable String bucket, @PathVariable String object,
@Valid GetSingleResourcePayload payload) {
return readResourceService.getSingleResource(bucket, object, payload);
@Valid GetSingleResourcePayload payload,
@RequestHeader(name = HttpHeaders.RANGE, required = false) String range) {
return this.readResourceService.getSingleResource(bucket, object, payload, range);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.jmsoftware.maf.osscenter.read.entity.GetSingleResourcePayload;
import org.springframework.core.io.Resource;
import org.springframework.http.ResponseEntity;
import org.springframework.lang.Nullable;
import org.springframework.validation.annotation.Validated;

import javax.validation.Valid;
Expand All @@ -27,8 +28,10 @@ public interface ReadResourceService {
* @param bucket the bucket
* @param object the object
* @param payload the payload
* @param range
* @return the single resource
*/
ResponseEntity<Resource> getSingleResource(@NotBlank String bucket, @NotBlank String object,
@Valid @NotNull GetSingleResourcePayload payload);
@Valid @NotNull GetSingleResourcePayload payload,
@Nullable String range);
}
Original file line number Diff line number Diff line change
@@ -1,23 +1,25 @@
package com.jmsoftware.maf.osscenter.read.service.impl;

import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.util.BooleanUtil;
import cn.hutool.core.util.ObjectUtil;
import com.jmsoftware.maf.osscenter.read.entity.GetSingleResourcePayload;
import com.jmsoftware.maf.osscenter.read.service.ReadResourceService;
import com.jmsoftware.maf.springcloudstarter.helper.MinioHelper;
import io.minio.StatObjectResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.core.io.InputStreamResource;
import org.springframework.core.io.Resource;
import org.springframework.http.*;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service;
import org.springframework.util.unit.DataSize;

import javax.validation.Valid;
import javax.validation.constraints.NotBlank;
import javax.validation.constraints.NotNull;

import static java.lang.Math.min;
import java.util.List;

/**
* <h1>ReadResourceServiceImpl</h1>
Expand All @@ -30,25 +32,55 @@
@Service
@RequiredArgsConstructor
public class ReadResourceServiceImpl implements ReadResourceService {
private static final DataSize SMALL_DATA_SIZE = DataSize.ofMegabytes(1);
private static final DataSize MEDIUM_DATA_SIZE = DataSize.ofMegabytes(4);
private static final DataSize LARGE_DATA_SIZE = DataSize.ofMegabytes(8);
private final MinioHelper minioHelper;

@Override
public ResponseEntity<Resource> getSingleResource(@NotBlank String bucket, @NotBlank String object,
@Valid @NotNull GetSingleResourcePayload payload) {
val statObjectResponse = minioHelper.statObject(bucket, object);
if (ObjectUtil.isNull(statObjectResponse)) {
@Valid @NotNull GetSingleResourcePayload payload,
@Nullable String range) {
StatObjectResponse statObjectResponse;
try {
statObjectResponse = this.minioHelper.statObject(bucket, object);
} catch (Exception e) {
log.error("Exception occurred when looking for object. Exception message: {}", e.getMessage());
return ResponseEntity.notFound().build();
}
val inputStream = minioHelper.getObject(bucket, object);
val bodyBuilder = ResponseEntity.ok();
if (BooleanUtil.isTrue(payload.getDownloadable())) {
String contentDisposition = ContentDisposition.builder("attachment").filename(object).build().toString();
bodyBuilder.header(HttpHeaders.CONTENT_DISPOSITION, contentDisposition);
val httpRanges = HttpRange.parseRanges(range);
if (CollUtil.isEmpty(httpRanges)) {
val bodyBuilder = ResponseEntity.ok();
if (BooleanUtil.isTrue(payload.getDownloadable())) {
bodyBuilder.header(HttpHeaders.CONTENT_DISPOSITION,
ContentDisposition.builder("attachment").filename(object).build().toString());
}
val getObjectResponse = this.minioHelper.getObject(bucket, object, 0, MEDIUM_DATA_SIZE.toBytes());
return bodyBuilder
.header(HttpHeaders.ACCEPT_RANGES, "bytes")
.contentLength(statObjectResponse.size())
.contentType(MediaType.parseMediaType(statObjectResponse.contentType()))
.body(new InputStreamResource(getObjectResponse));
}
return this.getResourceRegion(bucket, object, statObjectResponse, httpRanges);
}

private ResponseEntity<Resource> getResourceRegion(String bucket, String object,
StatObjectResponse statObjectResponse,
List<HttpRange> httpRanges) {
val bodyBuilder = ResponseEntity.status(HttpStatus.PARTIAL_CONTENT);
val getObjectResponse = this.minioHelper.getObject(bucket, object, httpRanges.get(0).getRangeStart(0),
MEDIUM_DATA_SIZE.toBytes());
val start = httpRanges.get(0).getRangeStart(0);
var end = start + MEDIUM_DATA_SIZE.toBytes() - 1;
val resourceLength = statObjectResponse.size();
end = Math.min(end, resourceLength - 1);
val rangeLength = end - start + 1;
return bodyBuilder
.header(HttpHeaders.ACCEPT_RANGES, "bytes")
.contentLength(statObjectResponse.size())
.header(HttpHeaders.CONTENT_RANGE, String.format("bytes %d-%d/%d", start, end, resourceLength))
.contentLength(rangeLength)
.contentType(MediaType.parseMediaType(statObjectResponse.contentType()))
.body(new InputStreamResource(inputStream));
.body(new InputStreamResource(getObjectResponse));
}
}

0 comments on commit beaa80d

Please sign in to comment.