2

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

Kambei
  • 458
  • 9
  • 23

1 Answers1

0

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.

Kambei
  • 458
  • 9
  • 23