Search code examples
javaquarkusminio

How to use Uni and AsyncFile in Quarkus for serve a preview of large file?


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


Solution

  • 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.