Skip to content

Instantly share code, notes, and snippets.

@jhollinger
Created July 27, 2012 05:27
Show Gist options
  • Star 15 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save jhollinger/3186316 to your computer and use it in GitHub Desktop.
Save jhollinger/3186316 to your computer and use it in GitHub Desktop.
Dropbox - async, ajax uploads with HTML5 in Sinatra
#dropbox {
position: relative;
}
#dropbox > input {
position: absolute;
top: 0px;
right: 0px;
bottom: 0px;
left: 0px;
width: 100%;
height: 100%;
opacity: 0;
}
#dropbox > input:hover, #dropbox:hover {
cursor: pointer;
}
function Dropbox(dropbox, url) {
this.box = dropbox;
this.url = url;
this.queue = [];
this.num_current_uploads = 0;
// Add clickable file picker
this.picker = document.createElement('input');
this.picker.setAttribute('type', 'file');
this.box.appendChild(this.picker);
// Add progress bar container
this.uploads = document.createElement('div');
this.box.parentNode.insertBefore(this.uploads, this.box.nextSibling);
// Optional defaults
this.max_concurrent = 2; // Queue uploads after n concurrent
this.max_size = null; // Max size in MB
this.success = null; // Callback on successful upload, passed the response body
this.error = null; // Callback on upload failure, passed the error text
this.mime_types = null; // A regex to match file mime types
var _this = this;
// Init drag and drop handlers
this.box.addEventListener("dragenter", function(e) { _this.drag(e, true) }, false);
this.box.addEventListener("dragleave", function(e) { _this.drag(e, false) }, false);
this.box.addEventListener("dragover", noop, false);
this.box.addEventListener("drop", function(e) { _this.drop(e) }, false);
this.picker.addEventListener("change", function(e) { _this.drop(e) })
}
Dropbox.prototype.drop = function(e) {
this.drag(e, false);
var files = (e.dataTransfer || e.target).files;
for ( var i=0; i<files.length; i++ )
this.handle_file(files[i]);
};
Dropbox.prototype.drag = function(e, active) {
noop(e);
this.box.className = active ? this.box.className += ' active' : this.box.className.replace(/ ?active/, '');
};
Dropbox.prototype.handle_file = function(file) {
// Bad file type
if ( this.mime_types && !file.type.match(this.mime_types) ) {
alert(file.name + ' is not an allowed file type');
// Too large
} else if ( this.max_size && (file.size / 1024 / 1024) > this.max_size ) {
alert(file.name + ' is too large; maximum is ' + this.max_size.toFixed(2) + ' MB');
} else {
file.label = this.add_label(file);
// Enqueue it
if ( this.max_concurrent > -1 && this.num_current_uploads >= this.max_concurrent )
this.queue.push(file);
// Upload it
else this.process_file(file);
}
};
Dropbox.prototype.process_file = function(file) {
this.num_current_uploads += 1;
var _this = this;
var reader = new FileReader();
reader.onload = function(e) {
var buffer = e.target.result;
var buffer_view = new Uint8Array(buffer);
_this.upload_file(file, buffer_view);
}
reader.readAsArrayBuffer(file);
};
Dropbox.prototype.upload_file = function(file, file_contents) {
var query_params = [['filename', file.name], ['mimetype', file.type], ['size', file.size]];
query_params.push(['preventCache', rand(30)]); // Chrome fix - it caches regardless of Cache-Control and Pragma headers
var query_string = query_params.map(function(p) { return p[0] + '=' + encodeURIComponent(p[1]) }).join('&');
var upload_url = this.url + '?' + query_string;
var _this = this;
var xhr = new XMLHttpRequest();
if ( xhr.upload ) xhr.upload.addEventListener('progress', function(e) { _this.handle_upload_progress(e, file.label) }, false)
xhr.open('POST', upload_url);
xhr.setRequestHeader('Content-Type', 'text/plain'); // Safari fix - it always sends application/x-www-form-urlencoded
xhr.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
xhr.setRequestHeader('Cache-Control', 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
xhr.setRequestHeader('Pragma', 'no-cache');
xhr.onreadystatechange = function(e) {
if ( xhr.readyState === 4 ) {
// Success
if (xhr.status === 200) {
file.label.getElementsByClassName('percent')[0].innerHTML = '100%';
if ( _this.success ) _this.success(file, xhr.responseText);
// Error
} else {
file.label.getElementsByClassName('percent')[0].innerHTML = 'Error';
if ( _this.error ) _this.error(file, xhr.statusText, xhr.responseText);
}
// Pop next upload off the queue
_this.num_current_uploads -= 1;
if ( _this.queue.length > 0 ) _this.process_file(_this.queue.shift())
// Remove the label in 1 second
setTimeout(function() { _this.uploads.removeChild(file.label) }, 1000);
}
}
xhr.send(file_contents)
};
Dropbox.prototype.handle_upload_progress = function(e, label) {
if ( e.lengthComputable ) {
var progress = label.getElementsByTagName('progress')[0];
progress.setAttribute('value', e.loaded);
progress.setAttribute('max', e.total);
label.getElementsByClassName('percent')[0].innerHTML = ((e.loaded / e.total) * 100).toFixed(0) + '%';
}
};
Dropbox.prototype.add_label = function(file) {
var size = (file.size / 1024 / 1024).toFixed(2);
var label = document.createElement('div');
label.setAttribute('class', 'upload-progress');
label.innerHTML = '<progress value="0" max="100"></progress> <span class="desc">' + file.name + ' - <span class="percent">0%</span> (' + size + ' MB)</span>';
this.uploads.insertBefore(label, null);
return label;
};
function rand(n) {
var str = '', possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for ( var i = 0; i < n; i++ ) {
str += possible.charAt(Math.floor(Math.random() * possible.length));
}
return str;
}
function noop(e) {
e.stopPropagation();
e.preventDefault();
}
require 'sinatra'
use Rack::Static, :urls => %w[/dropbox.js /dropbox.css /theme.css]
get '/' do
erb :dropbox
end
post '/drop' do
File.open("/tmp/upload-#{params[:filename]}", 'w') do |f|
f.binmode
while buffer = request.body.read(51200) # read in 50 kB chunks
f << buffer
end
end
'huzzah!'
end
__END__
@@ layout
<!doctype html>
<html>
<head>
<title>Dropbox</title>
<link href="/dropbox.css" media="screen" rel="stylesheet" type="text/css" />
<link href="/theme.css" media="screen" rel="stylesheet" type="text/css" />
<script src="/dropbox.js" type="text/javascript"></script>
</head>
<body>
<%= yield %>
</body>
</html>
@@ dropbox
<div id="dropbox">Drag and drop files, or click here...</div>
<div id="log"></div>
<script>
var box = document.getElementById('dropbox')
var log = document.getElementById('log')
var dropbox = new Dropbox(box, '/drop')
// Optional settings and callbacks
dropbox.max_size = 10 // MB
dropbox.max_concurrent = 2 // Do not upload more than two files at a time
dropbox.mime_types = /(png)|(jpe?g)|(gif)/i // Only allow pngs, jpgs, and gifs
dropbox.success = function(file, response) {
log.innerHTML += '<p>' + file.name + ' was successfully uploaded: ' + response + '</p>'
}
dropbox.error = function(file, status, response) {
log.innerHTML += '<p>' + file.name + ' failed to upload: ' + error + '</p>'
}
</script>
#dropbox {
padding: 15px 0px;
border: 2px solid #ddd;
text-align: center;
color: #999;
font-size: 1.3em;
font-family: Arial, sans-serif;
transition: background-color 0.15s;
}
#dropbox.active {
border-color: #3a3;
color: #777;
background-color: #dfd;
}
.upload-progress {
margin-bottom: 10px;
}
.upload-progress .desc {
font-size: 0.85em;
color: #555;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment