NAME

   HTTP::Upload::FlowJs - handle resumable multi-part HTTP uploads with
   flowjs

SYNOPSIS

   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.
       '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, [], [] ];
           };
       };
     };

OVERVIEW

   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.

METHODS

HTTP::Upload::FlowJs->new

     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
         },
     );

     maxChunkCount - hard maximum chunks allowed for a single upload

     maxFileSize - hard maximum total file size for a single upload

     maxChunkSize - hard maximum chunk size for a single chunk

     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.

     simultaneousUploads - simultaneously allowed uploads per file

     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.

   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.

$flowjs->jsConfig

     my $config = $flowjs->jsConfig(
         target => '/upload',
     );

   Create a JSON string that encapsulates the configuration of the Perl
   object for inclusion with the JS side of the world

$flowjs->validateRequest

       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.

$flowJs->expectedChunkSize

       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.

$flowjs->resetUploadDirectories

       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.

$flowjs->chunkName

       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

$flowjs->chunkOK

       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, [],[] ]
       }

$flowjs->uploadComplete( $info, $sessionPrefix=undef )

     if( $flowjs->uploadComplete($info, $sessionPrefix) ) {
         # do something with the chunks
     }

$flowjs->chunkFh

     my $fh = $flowjs->chunkFh( $info, $sessionid, $index );

   Returns an opened filehandle to the chunk described by $info. The
   session and the index are optional.

$flowjs->chunkContent

     my $content = $flowjs->chunkContent( $info, $sessionid, $index );

   Returns the content of a chunk described by $info. The session and the
   index are optional.

$flowjs->disallowedContentType( $info, $session )

       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.

$flowjs->sniffContentType( $info, $session )

       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.

$flowjs->combineChunks $info, $sessionPrefix, $target_fh, $digest )

     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';
     };

$flowjs->pendingUploads

     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.

$flowjs->staleUploads( $timeout, $now )

     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.

$flowjs->purgeStaleOrInvalid( $timeout, $now )

       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.

REPOSITORY

   The public repository of this module is
   https://github.com/Corion/HTTP-Upload-FlowJs.

SUPPORT

   The public support forum of this module is http://perlmonks.org/.

BUG TRACKER

   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 [email protected].

AUTHOR

   Max Maischein [email protected]

COPYRIGHT (c)

   Copyright 2009-2017 by Max Maischein [email protected].

LICENSE

   This module is released under the same terms as Perl itself.