public
Last active

mojolicious file uploads via websocket

  • Download Gist
upload.pl
Perl
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
#!/usr/bin/env perl
 
use Mojolicious::Lite;
use Mojo::JSON 'j';
use Mojo::Asset::Memory;
use File::Spec;
 
helper send_ready_signal => sub {
my $self = shift;
my $payload = { ready => \1 };
$payload->{chunksize} = shift if @_;
$self->send({ text => j($payload) });
};
 
helper send_error_signal => sub {
my $self = shift;
my $message = shift;
my $payload = {
error => $message,
fatal => $_[0] ? \1 : \0,
};
$self->send({ text => j($payload) });
};
 
helper send_close_signal => sub {
my $self = shift;
$self->send({ text => j({ close => \1 }) });
};
 
helper receive_file => sub {
my $self = shift;
 
# setup text/binary handlers
# create file_start/file_chunk/file_finish events
{
my $unsafe_keys = eval { ref $_[-1] eq 'ARRAY' } ? pop : [qw/directory/];
my $meta = shift || {};
my $file = Mojo::Asset::Memory->new;
 
$self->on( text => sub {
my ($ws, $text) = @_;
 
# receive file metadata
my %got = %{ j($text) };
 
# prevent client-side abuse
my %unsafe;
@unsafe{@$unsafe_keys} = delete @got{@$unsafe_keys};
%$meta = (%got, %$meta);
 
# finished
if ( $got{finished} ) {
$ws->tx->emit( file_finish => $file, $meta );
return;
}
 
# inform the sender to send the file
$ws->tx->emit( file_start => $file, $meta, \%unsafe );
});
 
$self->on( binary => sub {
my ($ws, $bytes) = @_;
 
$file->add_chunk( $bytes );
$ws->tx->emit( file_chunk => $file, $meta );
});
}
 
# connect default handlers for new file_* events
 
# begin file receipt
$self->on( file_start => sub { $_[0]->send_ready_signal } );
 
# log progress
$self->on( file_chunk => sub {
my ($ws, $file, $meta) = @_;
state $old_size = 0;
my $new_size = $file->size;
my $message = sprintf q{Upload: '%s' - %d | %d | %d}, $meta->{name}, ($new_size - $old_size), $new_size, $meta->{size};
$ws->app->log->debug( $message );
$old_size = $new_size;
});
 
# inform the sender to send the next chunk
$self->on( file_chunk => sub { $_[0]->send_ready_signal } );
 
# save file
$self->on( file_finish => sub {
my ($ws, $file, $meta) = @_;
my $target = $meta->{name} || 'unknown';
if ( -d $meta->{directory} ) {
$target = File::Spec->catfile( $meta->{directory}, $target );
}
$file->move_to($target);
my $message = sprintf q{Upload: '%s' - Saved to '%s'}, $meta->{name}, $target;
$ws->app->log->debug( $message );
$ws->send_close_signal;
});
 
};
 
any '/' => 'index';
 
websocket '/upload' => sub {
my $self = shift;
my $dir = File::Spec->rel2abs('upload');
mkdir $dir unless -d $dir;
$self->receive_file({directory => $dir});
};
 
app->start;
 
__DATA__
 
@@ index.html.ep
 
<!DOCTYPE html>
<html>
<head>
<title>Testing</title>
<link href="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/css/bootstrap-combined.min.css" rel="stylesheet">
<script src="//netdna.bootstrapcdn.com/twitter-bootstrap/2.3.0/js/bootstrap.min.js"></script>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.9.0/jquery.min.js"></script>
%= javascript 'upload.js'
%= javascript begin
function sendfile () {
//var file = document.getElementById('file').files[0];
var update = function(ratio) {
var percent = Math.ceil( 100 * ratio );
$('#progress .bar').css('width', percent + '%');
};
var success = function() {
$('#progress').removeClass('progress-striped active');
$('#progress .bar').addClass('bar-success');
};
var failure = function (messages) {
$('#progress').removeClass('progress-striped active');
$('#progress .bar').addClass('bar-danger');
console.log(messages);
};
sendFileViaWS({
url: '<%= url_for('upload')->to_abs %>',
file: $('#file').get(0).files[0],
onchunk: update,
onsuccess: success,
onfailure: failure
});
 
}
% end
</head>
<body>
<div class="container">
<input id="file" type="file">
<button onclick="sendfile()">Send</button>
<div id="progress" class="progress progress-striped active">
<div class="bar" style="width: 0%;"></div>
</div>
</div>
</body>
</html>
 
@@ upload.js
 
function sendFileViaWS (param) {
var ws = new WebSocket(param.url);
var file = param.file;
var filedata = { name : file.name, size : file.size };
 
var chunksize = param.chunksize || 250000;
var slice_start = 0;
var end = filedata.size;
var finished = false;
var success = false; // set to true on completion
var error_messages = [];
 
ws.onopen = function(){ ws.send(JSON.stringify(filedata)) };
 
ws.onmessage = function(e){
var status = JSON.parse(e.data);
 
// got close signal
if ( status.close ) {
if ( finished ) {
success = true;
}
ws.close();
return;
}
 
// server reports error
if ( status.error ) {
if ( param.onerror ) {
param.onerror( status );
}
error_messages.push( status );
if ( status.fatal ) {
ws.close();
}
return;
}
 
// anything else but ready signal is ignored
if ( ! status.ready ) {
return;
}
 
// upload already successful, inform server
if ( finished ) {
ws.send(JSON.stringify({ finished : true }));
return;
}
 
// server is ready for next chunk
var slice_end = slice_start + ( status.chunksize || chunksize );
if ( slice_end >= end ) {
slice_end = end;
finished = true;
}
ws.send( file.slice(slice_start,slice_end) );
if ( param.onchunk ) {
param.onchunk( slice_end / end ); // send ratio completed
}
slice_start = slice_end;
return;
};
 
ws.onclose = function () {
if ( success ) {
if ( param.onsuccess ) {
param.onsuccess();
}
return;
}
 
if (error_messages.length == 0) {
error_messages[0] = { error : 'Unknown upload error' };
}
 
if ( param.onfailure ) {
param.onfailure( error_messages );
} else {
console.log( error_messages );
}
}
}
 
__END__
 
# Protocol Documentation
 
* All signals/metadata are simply JSON formatted strings sent with via
websocket with TEXT opcode.
 
* All file data is sent via websocket with BINARY opcode
 
## Client side (javascript)
 
* Client starts by connecting and sending file meta-data.
 
{ name : filename, size : size_in_bytes }
 
Clien then waits for ready signal.
 
* On receipt of ready signal reply with chunk of file (on BINARY channel),
fire the `onchunck` handler (called with the ratio of sent data to total data)
then wait for ready signal. Repeat until file is finished, or another signal
causes other action to be taken.
 
* On receipt of ready signal when the file has finished transmitting, reply
with finished signal.
 
{ finished : true }
 
An optional `hash` key is planned, which if present would convey the hash type
and the file's hash result for comparison to the received file.
 
{ finished : true, hash : { type : sha1, value : hash_result } }
 
Servers should not necessarily interpret the lack of a hash parameter as a
reason for failure, as the browser may not support it.
 
* On receipt of error signal, store the error signal and fire the `onerror`
handler (with argument being the error signal contents). If fatal, close the
connection, which will then fire the `onfailure` handler.
 
* On receipt of close signal close connection. If all filedata has been sent,
mark as successful (`onsuccess` will fire rather than `onfailure`).
 
* In any case, the `onclose` handler will fire either the `onsuccess` (no
arguments) handler or the `onfailure` handler (called with an array of received
error signals).
 
*TODO: Transport error (ws.onerror handler)*
 
## Server side (generic)
 
* On reciept of file metadata and when ready for file chunks send ready signal.
 
{ ready : true [, chunksize : size_in_bytes ] }
 
Optionally a `chunksize` key may be sent telling the client the maximum number
of bytes the next chunk may be; if this number is zero, the default will be
used.
 
* On any error send error signal with optional `fatal` boolean flag. All other
keys are assumed to be for the handler.
 
{ error : truthy_value [, fatal : boolean ] }
 
* On receipt of finish signal, reply with either a standard error signal or the
close signal.
 
{ close : true }
 
Note that a lack of a final close signal will indicate a failure (will fire
`onfailure` handler) when the websocket finally closes due to timeout. Note
also that closing early will fire the `onfailure` handler immediately, sending
an error message with the close will not do what you mean; in this case use the
error signal with the `fatal` flag.
 
## Server side (Perl/Mojolicious)
 
coming soon ...

This code has now been modularized and is available at https://github.com/jberger/GalileoSend

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.