From 7058cb3035286b9a55c04dd8d2f634e01a952c48 Mon Sep 17 00:00:00 2001 From: Timo Bejan Date: Thu, 14 Sep 2023 15:10:17 +0300 Subject: [PATCH 1/2] RED-5205 - direct stream download implementation --- .../impl/controller/DownloadController.java | 76 +++++++++---------- .../external/resource/DownloadResource.java | 14 ++-- .../service/FileManagementStorageService.java | 3 +- 3 files changed, 45 insertions(+), 48 deletions(-) diff --git a/persistence-service-v1/persistence-service-external-api-impl-v1/src/main/java/com/iqser/red/persistence/service/v1/external/api/impl/controller/DownloadController.java b/persistence-service-v1/persistence-service-external-api-impl-v1/src/main/java/com/iqser/red/persistence/service/v1/external/api/impl/controller/DownloadController.java index 5744e389f..f62218c83 100644 --- a/persistence-service-v1/persistence-service-external-api-impl-v1/src/main/java/com/iqser/red/persistence/service/v1/external/api/impl/controller/DownloadController.java +++ b/persistence-service-v1/persistence-service-external-api-impl-v1/src/main/java/com/iqser/red/persistence/service/v1/external/api/impl/controller/DownloadController.java @@ -3,6 +3,7 @@ package com.iqser.red.persistence.service.v1.external.api.impl.controller; import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.PROCESS_DOWNLOAD; import static com.iqser.red.service.persistence.management.v1.processor.roles.ActionRoles.READ_DOWNLOAD_STATUS; +import java.io.BufferedInputStream; import java.util.List; import java.util.Map; import java.util.Optional; @@ -22,6 +23,9 @@ import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.context.request.RequestAttributes; +import org.springframework.web.context.request.RequestContextHolder; +import org.springframework.web.context.request.ServletRequestAttributes; import com.iqser.red.persistence.service.v1.external.api.impl.service.OneTimeTokenService; import com.iqser.red.service.persistence.management.v1.processor.exception.BadRequestException; @@ -52,6 +56,7 @@ import com.iqser.red.storage.commons.service.StorageService; import com.knecon.fforesight.keycloakcommons.security.KeycloakSecurity; import com.knecon.fforesight.tenantcommons.TenantContext; +import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; @@ -154,7 +159,7 @@ public class DownloadController implements DownloadResource { // special corner case: unapproved files, no reports and only REDACTED type selected if (approvedFiles.isEmpty() && (request.getReportTemplateIds() == null || request.getReportTemplateIds() .isEmpty()) && request.getDownloadFileTypes() != null && request.getDownloadFileTypes().size() == 1 && request.getDownloadFileTypes() - .contains(DownloadFileType.REDACTED)) { + .contains(DownloadFileType.REDACTED)) { throw new BadRequestException("Unapproved files in redacted state with no reports cannot be included"); } } @@ -211,20 +216,23 @@ public class DownloadController implements DownloadResource { } + @SneakyThrows @PreAuthorize("hasAuthority('" + PROCESS_DOWNLOAD + "')") - public CompletableFuture> downloadFile(@RequestParam(STORAGE_ID) String storageId, - @RequestParam(value = "inline", required = false, defaultValue = FALSE) boolean inline) { + public void downloadFile(@RequestParam(STORAGE_ID) String storageId) { + + var requestAttributes = RequestContextHolder.getRequestAttributes(); + HttpServletResponse response = ((ServletRequestAttributes) requestAttributes).getResponse(); + var userId = KeycloakSecurity.getUserId(); - var tenantId = TenantContext.getTenantId(); + var downloadStatus = getDownloadStatus(storageId, userId); + var fileDownloadStream = getFileForDownload(storageId, userId); - return CompletableFuture.supplyAsync(() -> { + response.setContentType("application/zip"); + response.setHeader("Content-Disposition", "attachment" + "; filename*=utf-8''" + StringEncodingUtils.urlEncode(downloadStatus.getFilename())); + response.setHeader("Content-Length", String.valueOf(downloadStatus.getFileSize())); - TenantContext.setTenantId(tenantId); - var downloadStatus = getDownloadStatus(storageId, userId); - var fileDownloadStream = getFileForDownload(storageId, userId); - - return getResponseEntity(inline, fileDownloadStream, downloadStatus.getFilename(), MediaType.parseMediaType("application/zip"), downloadStatus.getFileSize()); - }); + org.apache.commons.io.IOUtils.copyLarge(fileDownloadStream.getInputStream(), response.getOutputStream()); + response.flushBuffer(); } @@ -250,28 +258,13 @@ public class DownloadController implements DownloadResource { .build()); downloadService.setDownloaded(JSONPrimitive.of(storageId)); - return response; + return new InputStreamResource(new BufferedInputStream(response.getInputStream())); } catch (Exception e) { throw new NotFoundException(e.getMessage(), e); } } - @SneakyThrows - private ResponseEntity getResponseEntity(boolean inline, InputStreamResource resource, String filename, MediaType mediaType, long fileSize) { - - HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.setContentType(mediaType); - httpHeaders.setContentLength(fileSize); - if (filename != null) { - httpHeaders.add("Content-Disposition", inline ? "inline" : "attachment" + "; filename*=utf-8''" + StringEncodingUtils.urlEncode(filename)); - } - - return new ResponseEntity<>(resource, httpHeaders, HttpStatus.OK); - - } - - @Override @PreAuthorize("hasAuthority('" + PROCESS_DOWNLOAD + "')") public JSONPrimitive generateOneTimeToken(@RequestBody JSONPrimitive storageIdWrapper) { @@ -282,21 +275,24 @@ public class DownloadController implements DownloadResource { @Override - public CompletableFuture> downloadFileUsingOTT(@PathVariable(OTT) String oneTimeToken, - @RequestParam(value = "inline", required = false, defaultValue = FALSE) boolean inline, - @RequestParam(value = "tenantId") String tenantId) { - return CompletableFuture.supplyAsync(() -> { - TenantContext.setTenantId(tenantId); + @SneakyThrows + public void downloadFileUsingOTT(@PathVariable(OTT) String oneTimeToken, + @RequestParam(value = "tenantId") String tenantId) { - log.debug("downloadFileUsingOTT {}", oneTimeToken); - var token = oneTimeTokenDownloadService.getToken(oneTimeToken); - var downloadStatus = getDownloadStatus(token.getStorageId(), token.getUserId()); - var fileDownloadStream = getFileForDownload(token.getStorageId(), token.getUserId()); + TenantContext.setTenantId(tenantId); + var token = oneTimeTokenDownloadService.getToken(oneTimeToken); + var requestAttributes = RequestContextHolder.getRequestAttributes(); + HttpServletResponse response = ((ServletRequestAttributes) requestAttributes).getResponse(); - TenantContext.clear(); + var downloadStatus = getDownloadStatus(token.getStorageId(), token.getUserId()); + var fileDownloadStream = getFileForDownload(token.getStorageId(), token.getUserId()); - return getResponseEntity(inline, fileDownloadStream, downloadStatus.getFilename(), MediaType.parseMediaType("application/zip"), downloadStatus.getFileSize()); - }); + response.setContentType("application/zip"); + response.setHeader("Content-Disposition", "attachment" + "; filename*=utf-8''" + StringEncodingUtils.urlEncode(downloadStatus.getFilename())); + response.setHeader("Content-Length", String.valueOf(downloadStatus.getFileSize())); + + org.apache.commons.io.IOUtils.copyLarge(fileDownloadStream.getInputStream(), response.getOutputStream()); + response.flushBuffer(); } @@ -305,11 +301,13 @@ public class DownloadController implements DownloadResource { return DownloadRequest.builder().dossierId(request.getDossierId()).userId(KeycloakSecurity.getUserId()).fileIds(request.getFileIds()).build(); } + private String generateReportJsonStorageIdForS3(String storageId) { return storageId.substring(0, storageId.length() - 3) + REPORT_INFO; } + private String generateReportJsonStorageIdForAzure(String storageId) { return storageId.substring(0, storageId.length() - 4) + REPORT_INFO; diff --git a/persistence-service-v1/persistence-service-external-api-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/external/resource/DownloadResource.java b/persistence-service-v1/persistence-service-external-api-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/external/resource/DownloadResource.java index ee76db26c..939874a7e 100644 --- a/persistence-service-v1/persistence-service-external-api-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/external/resource/DownloadResource.java +++ b/persistence-service-v1/persistence-service-external-api-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/external/resource/DownloadResource.java @@ -66,11 +66,11 @@ public interface DownloadResource { @ResponseBody @ResponseStatus(value = HttpStatus.OK) - @Operation(summary = "Returns a downloadable byte stream of the requested file", description = "Use the optional \"inline\" request parameter " + "to select, if this report will be opened in the browser.") + @Operation(summary = "Returns a downloadable byte stream of the requested file") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK"), @ApiResponse(responseCode = "404", description = "Download with this Id is no longer available")}) - @GetMapping(value = REST_PATH) - CompletableFuture> downloadFile(@RequestParam(STORAGE_ID) String storageId, - @RequestParam(value = "inline", required = false, defaultValue = FALSE) boolean inline); + @GetMapping(value = REST_PATH+"/primitive") + void downloadFile(@RequestParam(STORAGE_ID) String storageId; + @ResponseBody @@ -83,11 +83,9 @@ public interface DownloadResource { @ResponseBody @ResponseStatus(value = HttpStatus.OK) - @Operation(summary = "Returns a downloadable byte stream of the requested file using a valid oneTimeToken", description = "Use the optional \"inline\" request parameter " + "to select, if this report will be opened in the browser.") + @Operation(summary = "Returns a downloadable byte stream of the requested file using a valid oneTimeToken") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK"), @ApiResponse(responseCode = "404", description = "Download with this Id is no longer available"), @ApiResponse(responseCode = "400", description = "OTT is not valid")}) @GetMapping(value = REST_PATH + OTT_PATH + OTT_PATH_VARIABLE) - CompletableFuture> downloadFileUsingOTT(@PathVariable(OTT) String oneTimeToken, - @RequestParam(value = "inline", required = false, defaultValue = FALSE) boolean inline, - @RequestParam(value = "tenantId") String tenantId); + void downloadFileUsingOTT(@PathVariable(OTT) String oneTimeToken, @RequestParam(value = "tenantId") String tenantId); } diff --git a/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/service/FileManagementStorageService.java b/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/service/FileManagementStorageService.java index 4d42b9a67..55b17bcbf 100644 --- a/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/service/FileManagementStorageService.java +++ b/persistence-service-v1/persistence-service-processor-v1/src/main/java/com/iqser/red/service/persistence/management/v1/processor/service/FileManagementStorageService.java @@ -1,5 +1,6 @@ package com.iqser.red.service.persistence.management.v1.processor.service; +import java.io.BufferedInputStream; import java.io.File; import java.io.InputStream; import java.nio.file.Files; @@ -60,7 +61,7 @@ public class FileManagementStorageService { File tempFile = File.createTempFile("temp", ".data"); storageService.downloadTo(tenantId, storageId, tempFile); - return Files.newInputStream(Paths.get(tempFile.getPath()), StandardOpenOption.DELETE_ON_CLOSE); + return new BufferedInputStream(Files.newInputStream(Paths.get(tempFile.getPath()), StandardOpenOption.DELETE_ON_CLOSE)); } -- 2.47.2 From 81de9adcba2a0660d20616eb307d5c7350029f47 Mon Sep 17 00:00:00 2001 From: Andrei Isvoran Date: Thu, 14 Sep 2023 15:18:37 +0300 Subject: [PATCH 2/2] RED-5205 - fix compile error --- .../service/v1/api/external/resource/DownloadResource.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/persistence-service-v1/persistence-service-external-api-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/external/resource/DownloadResource.java b/persistence-service-v1/persistence-service-external-api-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/external/resource/DownloadResource.java index 939874a7e..b3c954ded 100644 --- a/persistence-service-v1/persistence-service-external-api-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/external/resource/DownloadResource.java +++ b/persistence-service-v1/persistence-service-external-api-v1/src/main/java/com/iqser/red/service/persistence/service/v1/api/external/resource/DownloadResource.java @@ -69,7 +69,7 @@ public interface DownloadResource { @Operation(summary = "Returns a downloadable byte stream of the requested file") @ApiResponses(value = {@ApiResponse(responseCode = "200", description = "OK"), @ApiResponse(responseCode = "404", description = "Download with this Id is no longer available")}) @GetMapping(value = REST_PATH+"/primitive") - void downloadFile(@RequestParam(STORAGE_ID) String storageId; + void downloadFile(@RequestParam(STORAGE_ID) String storageId); -- 2.47.2