1
0
Fork 0
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:
zedshaw 2006-09-04 23:18:26 +00:00
parent 29df645972
commit 890420fdb5
7 changed files with 319 additions and 6 deletions

View file

@ -1,2 +0,0 @@
# Configuration file for webgen
# Used to set the parameters of the plugins

12
doc/site/config.yml Normal file
View 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"

View file

@ -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

View file

@ -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 &lt;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>

View 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>

View 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';
}

View 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