HTTP::Upload::FlowJs - handle resumable multi-part HTTP uploads with flowjs
This synopsis assumes a Plack/PSGI-like environment. There are plugins for Dancer and Mojolicious planned.
use HTTP::Upload::FlowJs;
my @parameter_names = (
'file', # The name of the file
'flowChunkNumber', # The index of the chunk in the current upload.
# First chunk is 1 (no base-0 counting here).
'flowTotalChunks', # The total number of chunks.
'flowCurrentChunkSize', # Current chunk size
'flowChunkSize', # The general chunk size. Using this value and
# flowTotalSize you can calculate the total number of
# chunks. Please note that the size of the data received in
# the HTTP might be lower than flowChunkSize of this for
# the last chunk for a file.
'flowTotalSize', # The total file size.
'flowIdentifier', # A unique identifier for the file contained in the request.
'flowFilename', # The original file name (since a bug in Firefox results in
# the file name not being transmitted in chunk
# multipart posts).
'flowRelativePath', # The file's relative path when selecting a directory
# (defaults to file name in all browsers except Chrome).
);
# In your POST handler for /upload:
sub POST_upload {
my $params = params();
my %info;
@info{ @parameter_names } = @{$params}{@parameter_names};
$info{ localChunkSize } = -s $params{ file };
# or however you get the size of the uploaded chunk
my $uploads = '/tmp/flowjs_uploads/';
my $flowjs = HTTP::Upload::FlowJs->new(
incomingDirectory => $uploads,
allowedContentType => sub { $_[0] =~ m!^image/! },
);
# you might want to set this so users don't clobber each others upload
my $session_id = '';
my @invalid = $flowjs->validateRequest( 'POST', \%info, $session_id );
if( @invalid ) {
warn 'Invalid flow.js upload request:';
warn $_ for @invalid;
return [500,[],["Invalid request"]];
return;
};
if( $flowjs->disallowedContentType( \%info, $session_id )) {
# We can determine the content type, and it's not an image
return [415,[],["File type disallowed"]];
};
my $chunkname = $flowjs->chunkName( \%info, undef );
# Save or copy the uploaded file
upload('file')->copy_to($chunkname);
# Now check if we have received all chunks of the file
if( $flowjs->uploadComplete( \%info, undef )) {
# Combine all chunks to final name
my $digest = Digest::SHA256->new();
my( $content_type, $ext ) = $flowjs->sniffContentType();
my $final_name = "file1.$ext";
open( my $fh, '>', $final_name )
or die $!;
binmode $fh;
my( $ok, @unlink_chunks )
= $flowjs->combineChunks( \%info, undef, $fh, $digest );
unlink @unlink_chunks;
# Notify backend that a file arrived
print sprintf "File '%s' upload complete\n", $final_name;
};
# Signal OK
return [200,[],[]]
};
# This checks whether a file has been received completely or
# needs to be uploaded again
sub GET_upload {
my $params = params();
my %info;
@info{ @parameter_names} = @{$params}{@parameter_names};
my $flowjs = HTTP::Upload::FlowJs->new(
incomingDirectory => $uploads,
allowedContentType => sub { $_[0] =~ m!^image/! },
);
my @invalid = $flowjs->validateRequest( 'GET', \%info, session->{connid} );
if( @invalid ) {
warn 'Invalid flow.js upload request:';
warn $_ for @invalid;
return [500, [], [] ];
} elsif( $flowjs->disallowedContentType( \%info, $session_id)) {
# We can determine the content type, and it's not an image
return [415,[],["File type disallowed"]];
} else {
my( $status, @messages )
= $flowjs->chunkOK( $uploads, \%info, $session_id );
if( $status != 500 ) {
# 200 or 416
return [$status, [], [] ];
} else {
warn $_ for @messages;
return [$status, [], [] ];
};
};
};
flow.js
is a client-side Javascript upload library that uploads
a file in multiple parts. It requires two API points on the server side,
one GET
API point to check whether a part already has been uploaded
completely and one POST
API point to send the data of each partial
upload to. This Perl module implements the backend functionality for
both endpoints. It does not implement the handling of the HTTP requests
themselves, but you likely already use a framework like Mojolicious
or Dancer for that.
my $flowjs = HTTP::Upload::FlowJs->new(
maxChunkCount => 1000,
maxFileSize => 10_000_000,
maxChunkSize => 1024*1024,
simultaneousUploads => 3,
allowedContentType => sub {
my($type) = @_;
$type =~ m!^image/!; # we only allow for cat images
},
);
incomingDirectory - path for the temporary upload parts
Required
maxChunkCount - hard maximum chunks allowed for a single upload
Default 1000
maxFileSize - hard maximum total file size for a single upload
Default 10_000_000
maxChunkSize - hard maximum chunk size for a single chunk
Default 1048576
minChunkSize - hard minimum chunk size for a single chunk
Default 1024
The minimum chunk size is required since the file type detection works on the first chunk. If the first chunk is too small, its file type cannot be checked.
forceChunkSize - force all chunks to be less or equal than
maxChunkSize
Default: true
Otherwise, the last chunk will be greater than or equal to
maxChunkSize
(the last uploaded chunk will be at least this size and up to two the size).Note: when
forceChunkSize
isfalse
it only makechunkSize
value in "jsConfig" equal tomaxChunkSize / 2
.simultaneousUploads - simultaneously allowed uploads per file
Default 3
This is just an indication to the Javascript
flow.js
client if you pass it the configuration from this object. This is not enforced in any way yet.allowedContentType - subroutine to check the MIME type
The default is to allow any kind of file
If you need more advanced checking, do so after you've determined a file upload as complete with
$flowjs->uploadComplete
.fileParameterName - The name of the multipart POST parameter to use for the file chunk
Default file
More interesting limits would be hard maxima for the number of pending
uploads or the number of outstanding chunks per user/session. Checking
these would entail a call to glob
for each check and thus would be
fairly disk-intensive on some systems.
# Perl HASH
my $config = $flowjs->jsConfig(
target => '/upload',
);
# JSON string
my $config = $flowjs->jsConfigStr(
target => '/upload',
);
Create a JSON string that encapsulates the configuration of the Perl object for inclusion with the JS side of the world
my $params = params(); # request params
my @parameter_names = $flowjs->params; # params needed by Flowjs
my %info;
@info{ @parameter_names } = @{$params}{@parameter_names};
$info{ file } = $params{ file };
$info{ localChunkSize } = -s $params{ file };
my @invalid = $flowjs->validateRequest( 'POST', \%info );
Returns needed params for validating request.
my $session_id = '';
my @invalid = $flowjs->validateRequest( 'POST', \%info, $session_id );
if( @invalid ) {
warning 'Invalid flow.js upload request:';
warning $_ for @invalid;
status 500;
return;
};
Does formal validation of the request HTTP parameters. It does not check previously stored information.
Note when POST
there are addition required params localChunkSize
and $self-
{fileParameterName}> (default 'file').
my $expectedSize = $flowJs->expectedChunkSize( $info, $chunkIndex );
Returns the file size we expect for the chunk $chunkIndex
. The index
starts at 1, if it is not passed in or zero, we assume it is for the current
chunk as indicated by $info
.
if( $firstrun or $wipe ) {
$flowJs->resetUploadDirectories( $wipe )
};
Creates the directory for incoming uploads. If $wipe
is passed, it will remove all partial files from the directory.
my $target = $flowjs->chunkName( $info, $sessionid );
Returns the local filename of the chunk described by $info
and
the $sessionid
if given. An optional index can be passed in as
the third parameter to get the filename of another chunk than
the current chunk.
my $target = $flowjs->chunkName( $info, $sessionid, 1 );
# First chunk
my( $status, @messages ) = $flowjs->chunkOK( $info, $sessionPrefix );
if( $status == 500 ) {
warn $_ for @messages;
return [ 500, [], [] ]
} elsif( $status == 200 ) {
# That chunk exists and has the right size
return [ 200, [], [] ]
} else {
# That chunk does not exist and should be uploaded
return [ 416, [],[] ]
}
if( $flowjs->uploadComplete($info, $sessionPrefix) ) {
# do something with the chunks
}
my $fh = $flowjs->chunkFh( $info, $sessionid, $index );
Returns an opened filehandle to the chunk described by $info
. The session
and the index are optional.
my $content = $flowjs->chunkContent( $info, $sessionid, $index );
Returns the content of a chunk described by $info
. The session
and the index are optional.
if( $flowjs->disallowedContentType( $info, $session )) {
return 415, "This type of file is not allowed";
};
Checks that the subroutine validator passed in the constructor allows this MIME type. Unrecognized files will be blocked.
my( $content_type, $image_ext ) = $flowjs->sniffContentType( $info, $session );
if( !defined $content_type ) {
# we need more chunks uploaded to check the content type
} elsif( $content_type eq '' ) {
# We couldn't determine what the content type is?!
return 415, "This type of upload is not allowed";
} elsif( $content_type !~ m!^image/(jpeg|png|gif)$!i ) {
return 415, "This type of upload is not allowed";
} else {
# We allow this upload to continue, as it seems to have
# an appropriate content type
};
This allows for finer-grained checking of the MIME-type. See also
the allowedContentType
argument in the constructor and
->disallowedContentType for a more convenient way to quickly
check the upload type.
if( not $flowjs->uploadComplete($info, $sessionPrefix) ) {
print "Upload not yet completed\n";
return;
};
open my $file, '>', 'user_upload.jpg'
or die "Couldn't create final file 'user_upload.jpg': $!";
binmode $file;
my $digest = Digest::SHA256->new();
my($ok,@unlink) = $flowjs->combineChunks( $info, undef, $file, $digest );
close $file;
if( $ok ) {
print "Received upload OK, removing temporary upload files\n";
unlink @unlink;
print "Checksum: " . $digest->md5hex;
} else {
# whoops
print "Received upload failed, removing target file\n";
unlink 'user_upload.jpg';
};
my $uploading = $flowjs->pendingUploads();
In scalar context, returns the number of pending uploads. In list context, returns the list of filenames that belong to the pending uploads. This list can be larger than the number of pending uploads, as one upload can have more than one chunk.
my @stale_files = $flowjs->staleUploads(3600);
In scalar context, returns the number of stale uploads. In list context, returns the list of filenames that belong to the stale uploads.
An upload is considered stale if no part of it has been written to since
$timeout
seconds ago.
The optional $timeout
parameter is the minimum age of an incomplete upload
before it is considered stale.
The optional $now
parameter is the point of reference for $timeout
.
It defaults to time
.
my @errors = $flowjs->purgeStaleOrInvalid();
Routine to delete all stale uploads and uploads with an invalid file type.
This is mostly a helper routine to run from a cron job.
Note that if you allow uploads of multiple flowJs instances into the same directory, they need to all have the same allowed file types or this method will delete files from another instance.
The public repository of this module is https://github.com/Corion/HTTP-Upload-FlowJs.
The public support forum of this module is https://perlmonks.org/.
Please report bugs in this module via the RT CPAN bug queue at https://rt.cpan.org/Public/Dist/Display.html?Name=HTTP-Upload-FlowJs or via mail to bug-http-upload-flowjs@rt.cpan.org.
Max Maischein corion@cpan.org
Copyright 2009-2017 by Max Maischein corion@cpan.org
.
This module is released under the same terms as Perl itself.