forked from Corion/HTTP-Upload-FlowJs
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathREADME
More file actions
391 lines (284 loc) · 13.5 KB
/
README
File metadata and controls
391 lines (284 loc) · 13.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
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 bug-http-upload-flowjs@rt.cpan.org.
AUTHOR
Max Maischein corion@cpan.org
COPYRIGHT (c)
Copyright 2009-2017 by Max Maischein corion@cpan.org.
LICENSE
This module is released under the same terms as Perl itself.