-
-
Save cghiban/1d4df80a98f98e160343 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/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 ... |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment