Projects‎ > ‎

XForms - Incremental Upload

  • parallel uploads
    • server-side
      • don't modify XForms state
      • just store a temp file
      • return server-events to send through Ajax
    • client-side
      • allows parallel uploads + Ajax requests
  • change mechanism to trigger uploads
    • xf:upload is always incremental: client starts upload automatically
    • upon file selection
      • upload starts right away
      • Ajax xxforms-upload-start sent right away
      • while the file is being sent, users see a visual indication the upload in progress
    • uploads don't impact the loading indicator used for Ajax requests
    • server can keep track of these events manually and/or engine provides status through functions:
      • xxf:uploads-pending() as xs:boolean (or as xs:integer)
  • Bugs
    • [ #315705 ] New upload: JavaScript exception
    • [ #315732 ] Upload field on IE: JavaScript error on file selection

2011-01-04 Status update: upload queue functional, indicator for upload, new xxforms-upload-start

The upload queue is now functional: when users select a file A, the upload of A starts right away; if during the upload, through another upload control, users select a file B, the upload of B will start as soon as uploading A is finished. While files are uploaded,  Ajax requests can happen in parallel, so users can continue to interact with the form.

How should uploads impact the display of the regular loading indicator? One approach is to show the loading indicator if there is an Ajax request or a upload in progress. With this approach, while long-running uploads happen in the background, the loading indicator is displayed for prolonged periods of time, and there is no way for users to know if the page is waiting to be refreshed after an action they took that results in an Ajax request. Since the loading indicator will have its own progress indicator, it will be clear enough that an upload is in progress, and we resolved to keep the loading indicator only for Ajax requests and page replacements.

For now, we don't have a real progress bar, but as soon as the upload starts, the file selection control is replaced by a spinner (animated GIF set from CSS); this prevents users from selecting another file until the upload is finished (fully uploaded or canceled), and informs users that the upload is in progress.

This concludes phase 1 of the upload implementation. The next step is to allow users to cancel an upload currently in progress (part of phase 2), and next to show a proper progress indicator, not just just an indicator that an upload is currently happening (part of phase 3).

Phase 2

  • upload cancellation
    • users can cancel an upload in progress
    • Ajax xxforms-upload-cancel
    • change implementation to send only one file per POST
  • retry mechanism to work properly with uploads, see bug [ #315398 ]
  • programmatic mechanism to indicate whether uploads are in progress
    • e.g. xf:bind readonly="xxf:uploads-pending()"

2011-01-05 Status update: cancel implemented, change of implementation, thinking of progress

The ability for users to cancel uploads is now implemented. While the upload is in progress, a "Cancel" link is shown next to the spinning icon indicating that an upload is in progress. It works quite well, with one caveat. The upload starts in the background right after a file is selected. So imagine that you have a form with multiple upload fields: select a file in A, and that file uploads. If you hit cancel, then this upload is cancelled. Good. Now while A is uploaded, select files in B and C. Those are added to the upload queue, and will be sent as soon we're done uploading A. OK, now A is finished, and B+C are sent. What happens if users cancel B? In the current implementation, since B and C are sent together, we are "smart" and cancel both B and C. Logical? Yes. Expected? Mmmh… not sure.

Even if uploading B+C in one shot seemed like a good idea, we're going to change this and really upload just one file at a time (B, then C). Uploading one file at a time will also help with the implementation of the progress bar. The files are sent in the body of a multipart/form-data POST. So the body starts with something like:

Content-Type: multipart/form-data; boundary=---------------------------241791819029657
Content-Length: 3956246
...

We have here the total size of whatever is sent. If B+C were sent, the server would be unable to tell what the progress of B or C is independently of each other.

2011-01-05 Status update: uploading file by file, next: progress indicator

The plan outlined yesterday, where we upload files one by one to make the cancel behavior more user-friendly (i.e. correct!), and facilitate the implementation of a progress bar is now implemented. Next, we want to implement the progress bar. Since we only have one upload per page (i.e. per UUID), the client can just ask the server what the current progress is. We can reuse the <xxforms:event> element here, and specify a source-control-id to be coherent with the way it is used in other places, all the server cares is that it is asked for the progress update for a certain UUID.

<xxforms:event-request xmlns:xxforms="http://orbeon.org/oxf/xml/xforms">
    <xxforms:uuid>797C7ABC-226E-3C42-0BE8-F4966C37E404</xxforms:uuid>
    <xxforms:sequence>6</xxforms:sequence>
    <xxforms:action>
        <xxforms:event name="xxforms-upload-progress" source-control-id="my-upload"/>
    </xxforms:action>
</xxforms:event-request>

In the server response, the control id from the request is repeated, just for consistency. (Again, the upload progress is not tied to a control on the server, but only to the UUID.) (An Ajax request will contain at most one query for the upload progress, but it can contain other events as well.) The server tells the client the total number of bytes it expects (from the Content-Length in the POST body), and how many it received so far. The sizes won't exactly correspond to the size of the file, as the POST body contains additional information, but this is enough for the client to show a progress in percent, and maybe also good enough to show a size rounded in KB. If the server doesn't yet know the progress is, because it hasn't processed the Content-Length in the request body yet, it can just ignore the upload progress query; the client will ask again later.

<xxf:event-response xmlns:xxf="http://orbeon.org/oxf/xml/xforms">
    <xxf:action>
        <xxf:control-values>
            <xxf:control id="my-upload" progress-received="1249129" progress-expected="3954614"/>
        </xxf:control-values>
    </xxf:action>
</xxf:event-response>

Another topic is the proper handling of interrupted connections. If an Ajax request fails because of a connection problem, we resent it. The same should happen for uploads. For this to work, we need to the YUI Connection Manager to notify us when an upload failed, but at this point it doesn't seem to do so. We are trying to resolve this with the YUI team.

2011-01-19 Erik's note on server-side aspects of progress indicator

Currently, a file upload sent as multipart/form-data is processed by commons-fileupload as soon as "somebody" (typically oxf:request) asks for request parameters.

In order to track progress, we must switch to the fileupload streaming API. This provides a streaming InputStream so we can store progress information as we are reading the stream from the browser.

Proposal:
  • upgrade to streaming API
  • if XForms document UUID parameter AND session are present, regularly store progress into session
    • single progress number (as there is only one upload in parallel for one UUID for now)
    • UUID -> (read, total) where total is obtained from the Content-Length header for the item or the whole body if missing
  • implement xxforms-upload-progress event in XFormsServer
  • detect interrupted uploads
    • server
    • client

Phase 3

  • progress indicator, similar to the one used by Gmail
  • generate unique ids for controls in iterations to handle controls moved while upload is pending


Concurrency issues

The new architecture allows uploads to go on in the background in parallel with Ajax requests. This can cause issues:

replace="all" -> 2 main cases:

  • If mode was incremental AND upload values have already been stored into instances, or if mode is NOT incremental, things are fine and we can submit the form right away as is done now.
  • If there are still pending uploads, we have a difficulty:
    • Instance serialization must wait as data is not complete and it makes sense to wait from user perspective (canceling uploads upon user demand could be an option).
    • But if we don't do anything special, then user can still use interface and modify instance to submit through Ajax.
    • After a while, form will automatically submit, which may confuse the user.

Case of replace="instance|none":

  • We have a similar issue as with replace="all" (user can modify page and instances as uploads are processed), except the page won't be replaced under the user's feet.

Proposal:

  • For now, let Ajax requests modify as uploads are processed even if not quite correct.
  • With replace="all", don't do 2nd phase of submission right away when done with uploads, but retry 1st phase so we have a chance to return validation errors through Ajax.
  • With replace="instance|none", can go through 2nd phase as we do this through Ajax.
  • Should we provide option to block the UI while the remaining uploads are in progress? And maybe provide a "cancel current uploads" option in this case?

Later:

  • Put instance to serialize "on the side" (temp file)
  • Fill it up with incoming uploads.
  • Resume submission with that temporary instance once all uploads have arrived.

NOTE: XForms 1.1 calls for mode="synchronous|asynchronous". Currently we don't really implement either, but something in between. With 1.1, *after* serialization is complete:

  • In synchronous mode, we should block the UI while the submission is in progress.
  • In asynchronous mode (default), we should let the submission go freely in the background as in another thread.

Additional feature: progress monitoring

This is a feature of its own.

  • Server must be able to store upload progress information "somewhere" (session), i.e. uuid -> current bytes or %
    • This probably requires modifying oxf:request.
    • At a given point in time, a given page (uuid) can only have one upload in progress.
  • Client then regularly queries the server about progress and updates progress bar.
  • Other than that, server processes uploads as usual.

Plan

  1. Support for parallel upload/Ajax events without messing up state
    1. Server processes one or more uploaded files received through a form post, returns server events, and does not modify dynamic state
    2. Client receives sends Ajax request with the server events
    3. Server handles the server events and storing values in instance
  2. Enhanced features
    1. "Real" progress bar
    2. Possibly: option to block client input (with cancel option)
    3. Possibly: canceling ongoing uploads
    4. Possibly: improved upload semantics with temporary instance
    5. Possibly: synchronous / asynchronous submissions
Comments