I am trying to create a REST resource with Quarkus that allows file downloads. After reading a lot in the Quarkus documentation, I assume that I should wrap blocking I/O operations like file reading in a Uni or Multi. I have tried this and came up with the following:
@GET
@Path("/download1/{fileId}")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Uni<Response> download1(@RestPath String fileId) {
File file = Paths.get(archivePath, fileId).toFile();
if (file.exists()) {
return Uni.createFrom().item(Response.ok(file, MediaType.APPLICATION_OCTET_STREAM)
.header("Content-Disposition", "attachment; filename=\"" + file.getName() + "\"")
.build());
} else {
return Uni.createFrom().item(Response.status(Response.Status.NOT_FOUND).entity("File not found").build());
}
}
@GET
@Path("/download2/{fileId}")
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public Uni<Response> download2(@RestPath String fileId) {
File file = Paths.get(archivePath, fileId).toFile();
OpenOptions oo = new OpenOptions().setRead(true)
.setCreate(false)
.setCreateNew(false);
return vertx.fileSystem().open(file.toString(), oo)
.onItem()
.transform(asyncFile -> Response.ok(asyncFile)
.header("X-File-Size", asyncFile.sizeBlocking())
.header("Content-Disposition", "attachment;filename=\"" + file.getName() + "\"")
.build());
}
When I start Quarkus locally and download a 4 GB file via the Swagger UI using download1, the Task Manager shows disk usage for the browser occasionally exceeding 400 MB/s. The download works relatively quickly. The response includes the Content-Length with the file size.
With download2, the Task Manager shows usage of 20-40 MB/s for both the JVM and the browser. The download is significantly slower, and the headers include transfer-encoding: chunked.
My goal was to create a download resource that does not load the entire file into memory. However, I am not entirely clear on why both variants behave so differently here. Unfortunately, I have not been able to understand why this is the case so far. Where do these differences come from and which version is better?
Edit: a third option may be @RunOnVirtualThread
with Response
instead of Uni<Response>
as a return.
The updated Quarkus Rest Client allows you to serve and receive File objects. You can simply use Uni<File> and forget the hassle of setting headers and handling file sizes on your own. That means you don't have to handle the part about manually reading and streaming the file.
https://quarkus.io/guides/rest
When you use AsyncFile, the Vertx library is doing some fancy stuff to allow the streaming. When you use a basic File, Quarkus is doing the heavy lifting (with probably better file stream handling).
If you want more control, just using InputStream works alright.
@GET
@Path("/{userId}/commit/timeline/{username}/{repo}")
@Produces("image/svg+xml")
public Uni<InputStream> fetchCommitTimeline(@PathParam("userId") String userId,
@PathParam("username") String username,
@PathParam("repo") String repo,
@QueryParam("count") @DefaultValue("30") int count) {
return mainService.fetchCommitTimeline(userId, username, repo, count);
}