mirror of
https://github.com/puma/puma.git
synced 2022-11-09 13:48:40 -05:00
Upload progress documentation with examples for Rails (but works with other frameworks).
git-svn-id: svn+ssh://rubyforge.org/var/svn/mongrel/trunk@345 19e92222-5c0b-0410-8929-a290d50e31e9
This commit is contained in:
parent
29df645972
commit
890420fdb5
7 changed files with 319 additions and 6 deletions
|
@ -1,2 +0,0 @@
|
|||
# Configuration file for webgen
|
||||
# Used to set the parameters of the plugins
|
12
doc/site/config.yml
Normal file
12
doc/site/config.yml
Normal file
|
@ -0,0 +1,12 @@
|
|||
# Configuration file for webgen
|
||||
# Used to set the parameters of the plugins
|
||||
FileCopyHandler:
|
||||
paths:
|
||||
- "**/*.css"
|
||||
- "**/*.jpg"
|
||||
- "**/*.png"
|
||||
- "**/*.gif"
|
||||
- "**/*.js"
|
||||
- "**/*.html"
|
||||
- "**/*.rb"
|
||||
|
|
@ -6,7 +6,7 @@ directoryName: Contribute
|
|||
|
||||
h1. Contributing To The Mongrel Canon
|
||||
|
||||
The documentation for Mongrel is typically written by the me (Zed), but other
|
||||
The documentation for Mongrel is typically written by me (Zed), but other
|
||||
people have contributed lots of blog articles about Mongrel, actual site
|
||||
documentation, or reviews and enhancements. It's pretty easy to contribute
|
||||
documentation to the Mongrel canon, although what gets in is strictly
|
||||
|
|
|
@ -6,8 +6,131 @@ directoryName: Upload Progress
|
|||
|
||||
h1. Mongrel Upload Progress Plugin
|
||||
|
||||
There's a plugin for Rails that gives folks a fancy "upload progress" but which really
|
||||
only works with FastCGI. There's plans in the works to create a similar system
|
||||
for Mongrel, but until that's made official you're pretty much on your own.
|
||||
One of the really nice things about Mongrel is its simplicity. It's very easy
|
||||
for someone to take and extend for their own needs. The Mongrel Upload
|
||||
Progress plugin is an example of how I'm able to extend the Mongrel HTTP
|
||||
Request object and provide near-realtime progress updates.
|
||||
|
||||
The reason why this is a challenge, is because web servers usually gather the
|
||||
HTTP request, send it on to the web framework, and wait on a response. This is
|
||||
fine for most requests, because they're too small to cause an issue. For large
|
||||
file uploads this is a usability nightmare. The user is left wondering what
|
||||
whether their upload is going through or not.
|
||||
|
||||
To do this, I've written a Mongrel handler that hooks into some basic Request
|
||||
callbacks. To use it, you need to install the gem, and create a small config
|
||||
file for it:
|
||||
|
||||
<pre><code>gem install mongrel_upload_progress
|
||||
|
||||
# config/mongrel_upload_progress.conf
|
||||
uri "/",
|
||||
:handler => plugin("/handlers/upload", :path_info => '/files/upload'),
|
||||
:in_front => true
|
||||
|
||||
# start mongrel
|
||||
mongrel_rails -d -p 3000 -S config/mongrel_upload_progress.conf
|
||||
</code></pre>
|
||||
|
||||
That config file tells mongrel to load the Upload handler in front of all other
|
||||
handlers. <code>:path_info</code> is passed to it, telling upload_progress to
|
||||
only watch the /files/upload action. There are two more parameters that I'll
|
||||
get into later: <code>:frequency</code> and <code>:drb</code>. I'm using Rails
|
||||
as an example, but this should work with any Ruby framework, such as Camping or
|
||||
Nitro.
|
||||
|
||||
Now that Mongrel is set up, let's create a "basic upload
|
||||
form":/docs/upload_progress_form.html. If you
|
||||
look closely you'll notice a few things:
|
||||
|
||||
* A unique <code>:upload_id</code> parameter must be sent to the upload_progress handler. This is so requests don't get mixed up, and the client page has an ID to query with.
|
||||
* The <form> tag is targetted to an iFrame to do the uploading. Certain browsers (like Safari) won't execute javascript while a request is taken place, so this step is necessary.
|
||||
* There is a little "javascript library":/docs/upload_progress_javascript.js being used. This handles the polling and status bar updates.
|
||||
* Notice the form's action is file/upload, just like the upload_progress handler.
|
||||
|
||||
The "Rails controller
|
||||
actions":/docs/upload_progress_rails.rb for this are very
|
||||
simple. The upload form itself needs no custom code. The upload action only
|
||||
renders javascript to be executed in the iFrame, to modify the contents of the
|
||||
parent page. The progress action is a basic RJS action that updates the
|
||||
current status. Most of the guts of this are implemented in the javascript
|
||||
library.
|
||||
|
||||
Here's what happens when you submit the form:
|
||||
|
||||
* The UploadProgress class creates a PeriodicalExecuter and gets ready to poll.
|
||||
* The browser initiates the upload.
|
||||
* Every 3 seconds, the PeriodicalExecuter calls the RJS #progress action and gets back the current status of the file.
|
||||
* Once finished, the iFrame calls <code>window.parent.UploadProgress.finish()</code>, which removes the status bar and performs any other finishing actions.
|
||||
|
||||
How's this work with a single Mongrel process if "Mongrel synchronizes Rails
|
||||
requests":http://david.planetargon.us/articles/2006/08/08/why-you-need-multiple-mongrel-instances-with-rails?
|
||||
It's actually very careful about locking, synchronizing only the bare minimum.
|
||||
The whole time that Mongrel is receiving the request and updating the progress
|
||||
is spent _in_ Mongrel, so it can happily serve other requests. This is how the
|
||||
RJS action is able poll while it's uploading.
|
||||
|
||||
This is fine and dandy, but not too many sites run on a single Mongrel. You'll
|
||||
quickly run into problems with multiple mongrels since only one Mongrel process
|
||||
knows about the upload. You'll either have to specify a specific mongrel port
|
||||
to communicate with, or set up a dedicated mongrel upload process. The third
|
||||
option, is use DRb.
|
||||
|
||||
<pre><code># config/mongrel_upload_progress.conf
|
||||
uri "/",
|
||||
:handler => plugin("/handlers/upload",
|
||||
:path_info => '/files/upload',
|
||||
:drb => 'druby://0.0.0.0:2999'),
|
||||
:in_front => true
|
||||
|
||||
# lib/upload.rb, the upload drb server
|
||||
require 'rubygems'
|
||||
require 'drb'
|
||||
require 'gem_plugin'
|
||||
GemPlugin::Manager.instance.load 'mongrel' => GemPlugin::INCLUDE
|
||||
DRb.start_service 'druby://0.0.0.0:2999', Mongrel::UploadProgress.new
|
||||
DRb.thread.join</code></pre>
|
||||
|
||||
Now in addition to starting mongrel, you'll need to start the DRb service too:
|
||||
|
||||
<pre><code>ruby lib/upload.rb</code></pre>
|
||||
|
||||
The Rails app should work the same as before, but now it is using a shared DRb
|
||||
instance to store the updates. This gives us one other advantage: a console
|
||||
interface to the current uploads.
|
||||
|
||||
<pre><code># lib/upload_client.rb, a simple upload drb client
|
||||
require 'drb'
|
||||
DRb.start_service
|
||||
|
||||
def get_status
|
||||
DRbObject.new nil, 'druby://0.0.0.0:2999'
|
||||
end
|
||||
|
||||
# typical console session
|
||||
$ irb -r lib/upload_client.rb
|
||||
>> uploads = get_status
|
||||
>> uploads.list
|
||||
=> []
|
||||
# start uploading in the browser
|
||||
>> uploads.list
|
||||
=> ["1157399821"]
|
||||
>> uploads.check "1157399821"
|
||||
=> {:size=>863467686, :received=>0}</code></pre>
|
||||
|
||||
Using DRb gives you a simple way to monitor the status of current uploads in
|
||||
progress. You could also write a simple web frontend for this too, accessing
|
||||
the DRb client with Mongrel::Uploads.
|
||||
|
||||
One final note is the use of the <code>:frequency</code> option. By default,
|
||||
the upload progress is marked every three seconds. This can be modified
|
||||
through the mongrel config file:
|
||||
|
||||
<pre><code>uri "/",
|
||||
:handler => plugin("/handlers/upload",
|
||||
:path_info => '/files/upload',
|
||||
:frequency => 1,
|
||||
:drb => 'druby://0.0.0.0:2999'),
|
||||
:in_front => true</code></pre>
|
||||
|
||||
|
||||
|
|
53
doc/site/src/docs/upload_progress_form.html
Normal file
53
doc/site/src/docs/upload_progress_form.html
Normal file
|
@ -0,0 +1,53 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-type" content="text/html; charset=utf-8" />
|
||||
<title>mongrel test</title>
|
||||
<%= javascript_include_tag :defaults %>
|
||||
<style type="text/css">
|
||||
#progress-bar {
|
||||
width:500px;
|
||||
height:25px;
|
||||
margin:15px;
|
||||
border:solid 1px #000;
|
||||
position:relative;
|
||||
}
|
||||
|
||||
#progress-bar #status-bar {
|
||||
display:block;
|
||||
height:25px;
|
||||
width:0;
|
||||
background-color:#00f;
|
||||
border-right:solid 1px #000;
|
||||
position:absolute;
|
||||
top:0; left:0;
|
||||
}
|
||||
|
||||
#progress-bar #status-text {
|
||||
display:block;
|
||||
padding: 0 15px;
|
||||
line-height:25px;
|
||||
position:absolute;
|
||||
top:0; left:0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<p><%= link_to (@upid = Time.now.to_i.to_s), :action => 'status', :upload_id => @upid %></p>
|
||||
<%= start_form_tag({:action => 'upload', :upload_id => @upid}, {:multipart => true, :target => 'upload',
|
||||
:onsubmit => "UploadProgress.monitor('#{escape_javascript @upid}')"}) %>
|
||||
<div id="file-fields">
|
||||
<p><%= file_field_tag :data %></p>
|
||||
</div>
|
||||
<p><%= link_to_function 'Add File Field', 'UploadProgress.FileField.add()' %>
|
||||
</p>
|
||||
<p><%= submit_tag :Upload %></p>
|
||||
</form>
|
||||
|
||||
<div id="results"></div>
|
||||
<div id="progress-bar"></div>
|
||||
|
||||
<iframe id="upload" name="upload" src="about:blank"></iframe>
|
||||
|
||||
</body>
|
||||
</html>
|
113
doc/site/src/docs/upload_progress_javascript.js
Normal file
113
doc/site/src/docs/upload_progress_javascript.js
Normal file
|
@ -0,0 +1,113 @@
|
|||
var UploadProgress = {
|
||||
uploading: null,
|
||||
monitor: function(upid) {
|
||||
if(!this.periodicExecuter) {
|
||||
this.periodicExecuter = new PeriodicalExecuter(function() {
|
||||
if(!UploadProgress.uploading) return;
|
||||
new Ajax.Request('/files/progress?upload_id=' + upid);
|
||||
}, 3);
|
||||
}
|
||||
|
||||
this.uploading = true;
|
||||
this.StatusBar.create();
|
||||
},
|
||||
|
||||
update: function(total, current) {
|
||||
if(!this.uploading) return;
|
||||
var status = current / total;
|
||||
var statusHTML = status.toPercentage();
|
||||
$('results').innerHTML = statusHTML + "<br /><small>" + current.toHumanSize() + ' of ' + total.toHumanSize() + " uploaded.</small>";
|
||||
this.StatusBar.update(status, statusHTML);
|
||||
},
|
||||
|
||||
finish: function() {
|
||||
this.uploading = false;
|
||||
this.StatusBar.finish();
|
||||
$('results').innerHTML = 'finished!';
|
||||
},
|
||||
|
||||
cancel: function(msg) {
|
||||
if(!this.uploading) return;
|
||||
this.uploading = false;
|
||||
if(this.StatusBar.statusText) this.StatusBar.statusText.innerHTML = msg || 'canceled';
|
||||
},
|
||||
|
||||
StatusBar: {
|
||||
statusBar: null,
|
||||
statusText: null,
|
||||
statusBarWidth: 500,
|
||||
|
||||
create: function() {
|
||||
this.statusBar = this._createStatus('status-bar');
|
||||
this.statusText = this._createStatus('status-text');
|
||||
this.statusText.innerHTML = '0%';
|
||||
this.statusBar.style.width = '0';
|
||||
},
|
||||
|
||||
update: function(status, statusHTML) {
|
||||
this.statusText.innerHTML = statusHTML;
|
||||
this.statusBar.style.width = Math.floor(this.statusBarWidth * status);
|
||||
},
|
||||
|
||||
finish: function() {
|
||||
this.statusText.innerHTML = '100%';
|
||||
this.statusBar.style.width = '100%';
|
||||
},
|
||||
|
||||
_createStatus: function(id) {
|
||||
el = $(id);
|
||||
if(!el) {
|
||||
el = document.createElement('span');
|
||||
el.setAttribute('id', id);
|
||||
$('progress-bar').appendChild(el);
|
||||
}
|
||||
return el;
|
||||
}
|
||||
},
|
||||
|
||||
FileField: {
|
||||
add: function() {
|
||||
new Insertion.Bottom('file-fields', '<p style="display:none"><input id="data" name="data" type="file" /> <a href="#" onclick="UploadProgress.FileField.remove(this);return false;">x</a></p>')
|
||||
$$('#file-fields p').last().visualEffect('blind_down', {duration:0.3});
|
||||
},
|
||||
|
||||
remove: function(anchor) {
|
||||
anchor.parentNode.visualEffect('drop_out', {duration:0.25});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Number.prototype.bytes = function() { return this; };
|
||||
Number.prototype.kilobytes = function() { return this * 1024; };
|
||||
Number.prototype.megabytes = function() { return this * (1024).kilobytes(); };
|
||||
Number.prototype.gigabytes = function() { return this * (1024).megabytes(); };
|
||||
Number.prototype.terabytes = function() { return this * (1024).gigabytes(); };
|
||||
Number.prototype.petabytes = function() { return this * (1024).terabytes(); };
|
||||
Number.prototype.exabytes = function() { return this * (1024).petabytes(); };
|
||||
['byte', 'kilobyte', 'megabyte', 'gigabyte', 'terabyte', 'petabyte', 'exabyte'].each(function(meth) {
|
||||
Number.prototype[meth] = Number.prototype[meth+'s'];
|
||||
});
|
||||
|
||||
Number.prototype.toPrecision = function() {
|
||||
var precision = arguments[0] || 2;
|
||||
var s = Math.round(this * Math.pow(10, precision)).toString();
|
||||
var pos = s.length - precision;
|
||||
var last = s.substr(pos, precision);
|
||||
return s.substr(0, pos) + (last.match("^0{" + precision + "}$") ? '' : '.' + last);
|
||||
}
|
||||
|
||||
// (1/10).toPercentage()
|
||||
// # => '10%'
|
||||
Number.prototype.toPercentage = function() {
|
||||
return (this * 100).toPrecision() + '%';
|
||||
}
|
||||
|
||||
Number.prototype.toHumanSize = function() {
|
||||
if(this < (1).kilobyte()) return this + " Bytes";
|
||||
if(this < (1).megabyte()) return (this / (1).kilobyte()).toPrecision() + ' KB';
|
||||
if(this < (1).gigabytes()) return (this / (1).megabyte()).toPrecision() + ' MB';
|
||||
if(this < (1).terabytes()) return (this / (1).gigabytes()).toPrecision() + ' GB';
|
||||
if(this < (1).petabytes()) return (this / (1).terabytes()).toPrecision() + ' TB';
|
||||
if(this < (1).exabytes()) return (this / (1).petabytes()).toPrecision() + ' PB';
|
||||
return (this / (1).exabytes()).toPrecision() + ' EB';
|
||||
}
|
14
doc/site/src/docs/upload_progress_rails.rb
Normal file
14
doc/site/src/docs/upload_progress_rails.rb
Normal file
|
@ -0,0 +1,14 @@
|
|||
class FilesController < ApplicationController
|
||||
session :off, :only => :progress
|
||||
|
||||
def progress
|
||||
render :update do |page|
|
||||
@status = Mongrel::Uploads.check(params[:upload_id])
|
||||
page.upload_progress.update(@status[:size], @status[:received]) if @status
|
||||
end
|
||||
end
|
||||
|
||||
def upload
|
||||
render :text => %(UPLOADED: #{params.inspect}.<script type="text/javascript">window.parent.UploadProgress.finish();</script>)
|
||||
end
|
||||
end
|
Loading…
Reference in a new issue