Guide Home
POD Home

Multipart Uploads

Uploading from from an HTTP client is done via POST requests; Sloop::Client includes several methods (readPost() and simplePost()) for dealing with raw POST data.

In a web context, however, uploads are often structured using the "multipart/form-data" media type defined in RFC 2388 (there is also "chunked" encoding for data of initially indeterminate length, but web clients will not do this; you may encounter it with Sloop::Other connections to HTTP servers). This includes, for example, file uploads via HTML forms.

Such uploads may involve multiple discrete pieces of data (hence, "multipart"). If you create an HTML form with the enctype set to multipart/form-data, you must have a corresponding handler to deal with it. This is the purpose of Sloop::Multipart.

my $handler = sub {
    my $client = shift;
    my $len = $client->{request}->header('content-length');

    my $multipart = Sloop::Multipart->new (
        $client->{request},
        $len,
        '/opt/sloop/tmp/'
    );

    $client->readPost (
        sub {
            my $status = shift;
            return if $status == -1;

        # Add data to be processed.
            if ($multipart->process (
                \$client->{request}->{body}
            ) == -1) {
                $client->err_log("Upload failed: ".$multipart->{error});
                $client->generic(400);
                return;
            }

        # Deal with the upload when finished.
            if ($status) {
                foreach (@{$multipart->{parts}}) {
                # These are Sloop::Multipart::Data objects.
                    $_->placeFile (
                        '/opt/sloop/uploads/'.$_->filename
                    )
                }
                $client->reply(\"Upload complete!");
            }
        }, $len
    )
}

The reason to use $client->readPost() here instead of simplePost() is that POST data is normally read into memory. This is probably not desirable with file uploads, so rather than wait for the entire thing to be completed, we take each chunk as it is received and feed it to the Sloop::Multipart object, which stores it in a temp file until we decide what to do.

Here's a breakdown of what this handler does:

  1. Create a Sloop::Multipart object using /opt/sloop/tmp/ for temporary storage.

  2. Read in the upload as it arrives. The server's max_recv setting helps to ensure no one read will use an excessive amount of memory.

  3. When the upload is complete, copy each part received into a file and reply to the client. The individual parts correspond to individual <input> tags from the HTML form.

In practise, a form may contain various different kinds of inputs, not all of which are files. In this case, you can sort them out based on $part->{name}, which corresponds to the name attribute of the input. However, even in that case, you likely want to confirm something is what you think it is, in order to prevent errors from malicious or incorrect requests. For example, in the handler above, if a part is actually not a file, $part->filename will return undef, and we will get a warning for "Use of uninitialized value in concatenation".

For short non-file fields, you can double check the length with $part->size and then access it via $part->getContent. These parts are still temporarily stored in a file. While you can create a Multipart object which uses memory alone, you presumably then want to delimit the total possible length (the second arg to the constructor). You can't decide some parts will go to disk and some won't. Although this may seem silly, there's a good reason for it: if the total length is delimited to include a possibly large file which you want saved to disk, but you want other parts placed in memory, a malicious/incorrect request may use an inappropriate portion of the total length in one of the in-memory fields.