RFC: Upload and download handlers

We’re consider some new framework features to simplify how uploads and downloads are handled. The idea is to use a layered approach that gives direct access to the low-level HTTP request handling mechanism for full control and flexibility and on top of that built-in handler implementations for the most common use cases. The new API would replace Receiver for uploads and StreamResource for downloads.

For uploads, we would invert control so that the primary mode of operation is to read bytes from an InputStream. For downloads, we would give direct control over response headers at the time when the request is handled and provide a listener for download progress and completion.

The full RFC is in a separate document. You can comment directly in that document or in this discussion. As a teaser for what’s in the full document, here’s some imagined usage examples.

Upload to a temp file

new Upload(UploadHandler.toTempFile((File file) -> Notifiation.show("Stored: " + file)));

(It makes no sense to show the server-side file path in a Notification - the point is just to show that you can update the UI from the callback.)

Upload to a byte[] and show progress

new Upload(UploadHandler.inMemory((meta, data) -> {
    Notifiation.show("Got " + data.length + " bytes for " + meta.getFileName());
  }).onProgress(
    (transferredBytes, totalBytes) -> Notification.show("Received "  + transferredBytes),
    32768 // progress interval in bytes
));

Receive a file and count the number of lines directly from the input stream

new Upload(event -> {
  int c = countLines(event.getInputStream());

  event.getUI().access(() -> Notifiation.show(c + " lines in " + event.getFileName()));
});

Image using a class resource

new Image(DownloadHandler.forClassResource(MyView.class, "logo.png"));

Download a File and show a notification when completed (requires @Push)

new Anchor(DownloadHandler.forFile(new File("tos.pdf")
    .whenComplete(success -> Notification.show("Success: " + succcess)),
  "Download terms of service");

Serve a file from the database

String attachmentId = ...;
new Anchor(DownloadHandler.fromStream(event -> {
  try(ResultSet row = fetchAttachmentFromDatabase(attachmentId)) {
    return new DownloadResponse(row.getBlob("data").getOutputStream(), row.getLong("size"),
      row.getString("name"), row.getString("mime"));
  } catch (Exception e) {
    return DownloadReponse.error(500);
  }
}}, "Download attachment");

Stream directly to an OutputStream

new Anchor(event -> {
  event.setFileName("random_bytes");
  OutputStream out = event.getOutputStream();
  while(true) {
    int next = ThreadLocalRandom.nextInt(-1, 256);
    if (next == -1) break;
    out.write(next);
  }
}, "Download random bytes");
3 Likes

Would this allow us to implement scenario’s mentioned here Efficiently serving video files in Java web apps with HTTP range requests | Vaadin, by giving direct access to the request/response ?

Yes, that should be doable. You would still have to implement the Range support manually just as with the current workaround of serving through a separate Servlet. But you would get a little bit of convenience from the built-in security checks and URL handling.

1 Like

Good to see that we finally are at least planning to clean up this mess we currently have regarding uploads and downloads :+1: The document looks like a book, gotta reserve some time to look into it.

I wonder if you checked what is built into Viritin? I haven’t used anything else but those lately. Naming and implementation is not cute (we should also make sure we offer some sane “Element level APIs” for component developers), but I have been quite happy with the actual APIs and functionality.

What I’m suggesting here is structurally similar to what you have in UploadFileHandler with some additions:

  • I’m collecting the callback parameters to a single event object to better allow for future API evolution.
  • That event also gives access to some additional things that are probably only useful in special cases, e.g. the underlying request and response objects.
  • Instead of returning a Command to run after the upload has completed, I’m giving direct access to the UI instance so that you can do event.getUI().access(...) at any time. There are also shorthands for adding a success handler through a simple fluid API.
  • There are shorthand methods for uploading directly to a temp file or to a byte[].

This mechanism is also implemented as a framework level feature that can be used by any component. The core mechanism of assigning a handler as an element attribute value that is translated to a generate URL remains but the idea is to give the add-on developer full control over how that request is handled instead of the highly opinionated StreamReceiver / StreamResource setup that we currently have.

There’s nothing similar to your ByteArrayUploadField here. That’s something I plan to address separately.

I would appreciate anything that allows to start a download from anywhere (i.e. a menu).

DownloadHelper.startDownload({ getStream() }, "my.pdf")

That needs to be implemented separately for each component since it needs some coordination between server-side URL generation and client-side event handling. There’s nothing in the scope of this RFC that could make that easier to do.

There’s an existing ticket for enabling download capabilities for menus, although presented from a different point of view: Allow MenuItem's to contain an Anchor element, to allow download · Issue #3182 · vaadin/flow-components · GitHub. For reference, there’s also an issue for adding a dedicated download API to Button and the same approach could also be applied for menus.

Been using Vaadin since 7, and the last product I built on it had a whole bunch of upload / download support in different areas of the app. It’s been a little while (like a decade) since I wrote the original code, but I remember pain to get things working properly.

Improvements in this area would be very welcome as I build out my next product :grin:.

Thanks to everyone who provided comments here or in the RFC document!

I’ve created a PRD issue based on your input and we’re planning to make the described improvements for Vaadin 24.8 which is scheduled to be released in June.

1 Like

Cool , sometimes asynchronous upload is very helpful to not block the UI.

(nit: there’s a code tag missing starting below Receive a file and count the number of lines directly from the input stream)

1 Like