RED-5205 - direct stream download implementation #122

Merged
timo.bejan.ext merged 2 commits from RED-5202-direct-download into master 2023-09-14 14:46:55 +02:00
3 changed files with 45 additions and 48 deletions

View File

@ -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<ResponseEntity<InputStreamResource>> 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<InputStreamResource> 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<String> generateOneTimeToken(@RequestBody JSONPrimitive<String> storageIdWrapper) {
@ -282,21 +275,24 @@ public class DownloadController implements DownloadResource {
@Override
public CompletableFuture<ResponseEntity<InputStreamResource>> 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;

View File

@ -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<ResponseEntity<InputStreamResource>> 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<ResponseEntity<InputStreamResource>> 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);
}

View File

@ -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));
}