How to use Uni
and AsyncFile
in Quarkus for serve a preview of a large file as, for example, a video/mp4
file, while reading it from minio?
I tried to implement this with Response
class, following this, but without success:
@GET
@Path("/download/{id}/ctx/{ctx}")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Response downloadFile(@PathParam("id") UUID fileId, @PathParam("ctx") String ctx, @QueryParam("preview") boolean preview,
@HeaderParam(value = "Range") String httpRangeList) {
try {
ResFile resFile = resFileService.getResFile(fileId, ctx);
String contentType = resFile.getMimeType();
Long size = resFile.getSize();
long rangeStart = 0;
long rangeEnd = size - 1;
if (preview) {
if (httpRangeList == null) {
return Response.status(Response.Status.OK)
.header("Access-Control-Expose-Headers", HttpHeaders.CONTENT_DISPOSITION)
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + resFileService.getResFile(fileId, ctx).getFileName() + "\"")
.header(HttpHeaders.CONTENT_TYPE, contentType)
.header("Accept-Ranges", "bytes")
.header("Content-Range", "bytes " + rangeStart + "-" + rangeEnd + "/" + size)
.header("Content-Length", String.valueOf(size))
.entity(resFileService.loadFileAsResourceRange(fileId, ctx, rangeStart, size)).build();
} else {
String[] ranges = httpRangeList.split("-");
rangeStart = Long.parseLong(ranges[0].substring(6));
if (ranges.length > 1) {
rangeEnd = Long.parseLong(ranges[1]);
} else {
rangeEnd = rangeStart + chunkSize;
}
rangeEnd = Math.min(rangeEnd, size - 1);
final byte[] data = resFileService.loadFileAsResourceRange(fileId, ctx, rangeStart, rangeEnd).readAllBytes();
log.info("data size: {}", data.length);
final String contentLength = String.valueOf((rangeEnd - rangeStart) + 1);
if (rangeEnd >= size) {
return Response.status(Response.Status.OK)
.header("Access-Control-Expose-Headers", HttpHeaders.CONTENT_DISPOSITION)
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + resFileService.getResFile(fileId, ctx).getFileName() + "\"")
.header(HttpHeaders.CONTENT_TYPE, contentType)
.header("Accept-Ranges", "bytes")
.header("Content-Range", "bytes " + rangeStart + "-" + rangeEnd + "/" + size)
.header("Content-Length", contentLength)
.entity(data).build();
}
else {
return Response.status(Response.Status.PARTIAL_CONTENT)
.header("Access-Control-Expose-Headers", HttpHeaders.CONTENT_DISPOSITION)
.header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + resFileService.getResFile(fileId, ctx).getFileName() + "\"")
.header(HttpHeaders.CONTENT_TYPE, contentType)
.header("Accept-Ranges", "bytes")
.header("Content-Range", "bytes " + rangeStart + "-" + rangeEnd + "/" + size)
.header("Content-Length", contentLength)
.entity(data).build();
}
}
} else {
return Response.status(Response.Status.OK)
.header("Access-Control-Expose-Headers", HttpHeaders.CONTENT_DISPOSITION)
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resFileService.getResFile(fileId, ctx).getFileName() + "\"")
.header(HttpHeaders.CONTENT_TYPE, contentType)
.entity(resFileService.loadFileAsResource(fileId, ctx)).build();
}
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(String.format(FILE_NOT_FOUND, fileId)).build();
} catch (IOException | MinIOException e) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(String.format(MINIO_EXCEPTION, e.getMessage())).build();
}
}
public InputStream loadFileAsResource(UUID uuid, String ctx) throws MinIOException, NotFoundException {
ResFile resFile = resFileRepository.findByResourceIdAndCtx(uuid, ctx).orElseThrow(() -> new NotFoundException(String.format(RESFILE_NOT_FOUND, uuid)));
return minIOService.downloadFile(Buckets.FILES.name().toLowerCase(), resFile.getPath());
}
public ByteArrayInputStream loadFileAsResourceRange(UUID fileId, String ctx, Long rangeStart, Long rangeEnd) throws MinIOException, NotFoundException, IOException {
ResFile resFile = resFileRepository.findByResourceIdAndCtx(fileId, ctx).orElseThrow(() -> new NotFoundException(String.format(RESFILE_NOT_FOUND, fileId)));
Files.copy(minIOService.downloadFile(Buckets.FILES.name().toLowerCase(), resFile.getPath()),
Paths.get("/tmp/" + resFile.getFileName()), StandardCopyOption.REPLACE_EXISTING);
return new ByteArrayInputStream(IOUtils.toByteArray(new FileInputStream("/tmp/" + resFile.getFileName())), rangeStart.intValue(), rangeEnd.intValue());
}
public InputStream downloadFile(String bucket, String filename) throws MinIOException {
try {
if(minioClient == null)
initializeMinIOClient();
// Get the object from bucket
return minioClient.getObject(GetObjectArgs.builder().bucket(bucket).object(filename).build());
} catch (Exception e) {
throw new MinIOException(String.format("Error occurred while downloading file '%s' | ", filename), e);
}
}
And from this I read about Uni
and AsyncFile
.
But I don't know how to implement it.
NOTE: Preferably I would like to return dirrectly reading the InputStream without saving the file as I take it from MinIO
Thanks to this
I was able to solve in this way:
@GET
@Path("/download/{id}/ctx/{ctx}")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public CompletionStage<Response> downloadFile(@PathParam("id") UUID fileId, @PathParam("ctx") String ctx,
@QueryParam("preview") boolean preview, @HeaderParam("Range") String range) {
ResFile resFile;
try {
resFile = resFileService.getResFile(fileId, ctx);
} catch (NotFoundException e) {
return CompletableFuture.supplyAsync(() -> Response.status(Response.Status.NOT_FOUND).entity(String.format(FILE_NOT_FOUND, fileId)).build());
}
if (preview) {
if (range == null) {
String contentType = resFile.getMimeType();
return CompletableFuture.supplyAsync(() -> {
try {
return resFileService.loadFileAsResource(fileId, ctx);
} catch (MinIOException e) {
return CompletableFuture.supplyAsync(() -> Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(String.format(SERVER_EXCEPTION, e)).build());
} catch (NotFoundException e) {
return CompletableFuture.supplyAsync(() -> Response.status(Response.Status.NOT_FOUND).entity(String.format(FILE_NOT_FOUND, fileId)).build());
}
}).thenApplyAsync(file -> {
try {
return Response.ok((StreamingOutput) output -> {
try (final InputStream is = (InputStream) file) {
IOUtils.copyLarge(is, output);
}
}).header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + resFileService.getResFile(fileId, ctx).getFileName() + "\"")
.header(HttpHeaders.CONTENT_TYPE, contentType)
.header(HttpHeaders.CONTENT_LENGTH, resFile.getSize())
.status(HttpStatus.SC_PARTIAL_CONTENT).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(String.format(FILE_NOT_FOUND, fileId)).build();
}
}).handleAsync((r, e) -> {
if (e != null) {
log.error("Error", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(String.format(SERVER_EXCEPTION, e)).build();
}
return r;
});
} else {
String[] ranges = range.split("-");
long rangeStart = Long.parseLong(ranges[0].substring(6));
long rangeEnd;
if (ranges.length > 1) {
rangeEnd = Long.parseLong(ranges[1]);
} else {
rangeEnd = rangeStart + chunkSize;
}
log.info("@@@Range: {} - {}", rangeStart, rangeEnd);
String contentType = resFile.getMimeType();
rangeEnd = Math.min(rangeEnd, resFile.getSize() - 1);
final String contentLength = String.valueOf((rangeEnd - rangeStart) + 1);
long finalRangeEnd = rangeEnd;
return CompletableFuture.supplyAsync(() -> {
try {
return resFileService.loadFileAsResource(fileId, ctx);
} catch (MinIOException e) {
return CompletableFuture.supplyAsync(() -> Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(String.format(MINIO_EXCEPTION, e)).build());
} catch (NotFoundException e) {
return CompletableFuture.supplyAsync(() -> Response.status(Response.Status.NOT_FOUND).entity(String.format(FILE_NOT_FOUND, fileId)).build());
}
}).thenApplyAsync(file -> {
try {
return Response.ok((StreamingOutput) output -> {
try (final InputStream is = (InputStream) file) {
long byets = IOUtils.copyLarge(is, output, rangeStart, finalRangeEnd);
log.info("@@@Bytes: {}", byets);
}
}).header(HttpHeaders.CONTENT_DISPOSITION, "inline; filename=\"" + resFileService.getResFile(fileId, ctx).getFileName() + "\"")
.header("Accept-Ranges", "bytes")
.header(HttpHeaders.CONTENT_TYPE, contentType)
.header(HttpHeaders.CONTENT_LENGTH, contentLength)
.header("Content-Range", "bytes " + rangeStart + "-" + finalRangeEnd + "/" + resFile.getSize())
.status(HttpStatus.SC_PARTIAL_CONTENT)
.build();
} catch (NotFoundException e) {
log.error("File not found", e);
return Response.status(Response.Status.NOT_FOUND).entity(String.format(FILE_NOT_FOUND, fileId)).build();
}
}).handleAsync((r, e) -> {
if (e != null) {
log.error("Error", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(String.format(SERVER_EXCEPTION, e)).build();
}
return r;
});
}
} else {
String contentType = resFile.getMimeType();
return CompletableFuture.supplyAsync(() -> {
try {
return resFileService.loadFileAsResource(fileId, ctx);
} catch (MinIOException e) {
return CompletableFuture.supplyAsync(() -> Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(String.format(MINIO_EXCEPTION, e)).build());
} catch (NotFoundException e) {
return CompletableFuture.supplyAsync(() -> Response.status(Response.Status.NOT_FOUND).entity(String.format(FILE_NOT_FOUND, fileId)).build());
}
}).thenApplyAsync(file -> {
try {
return Response.ok((StreamingOutput) output -> {
try (final InputStream is = (InputStream) file) {
IOUtils.copyLarge(is, output);
}
}).header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + resFileService.getResFile(fileId, ctx).getFileName() + "\"")
.header(HttpHeaders.CONTENT_TYPE, contentType).build();
} catch (NotFoundException e) {
return Response.status(Response.Status.NOT_FOUND).entity(String.format(FILE_NOT_FOUND, fileId)).build();
}
}).handleAsync((r, e) -> {
if (e != null) {
log.error("Error", e);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).entity(String.format(SERVER_EXCEPTION, e)).build();
}
return r;
});
}
}
No Uni
and AsyncFile
needed.