diff options
author | David Heinemeier Hansson <david@loudthinking.com> | 2005-06-28 17:44:16 +0000 |
---|---|---|
committer | David Heinemeier Hansson <david@loudthinking.com> | 2005-06-28 17:44:16 +0000 |
commit | beffb77e05bf3c192e798587812105458146df32 (patch) | |
tree | e51ff71df80264ad08447719d842929ef1e9fd8e /actionpack/lib/action_controller/cgi_ext/multipart_progress.rb | |
parent | 62ed6950c9119c12e6b17a68c79c45c77a7b1ca4 (diff) | |
download | rails-beffb77e05bf3c192e798587812105458146df32.tar.gz rails-beffb77e05bf3c192e798587812105458146df32.tar.bz2 rails-beffb77e05bf3c192e798587812105458146df32.zip |
Added support for upload progress indicators in Apache and lighttpd 1.4.x (won't work in WEBrick or lighttpd 1.3.x) #1475 [Sean Treadway]
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@1553 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
Diffstat (limited to 'actionpack/lib/action_controller/cgi_ext/multipart_progress.rb')
-rw-r--r-- | actionpack/lib/action_controller/cgi_ext/multipart_progress.rb | 167 |
1 files changed, 167 insertions, 0 deletions
diff --git a/actionpack/lib/action_controller/cgi_ext/multipart_progress.rb b/actionpack/lib/action_controller/cgi_ext/multipart_progress.rb new file mode 100644 index 0000000000..d297c6f86e --- /dev/null +++ b/actionpack/lib/action_controller/cgi_ext/multipart_progress.rb @@ -0,0 +1,167 @@ +# == Overview +# +# This module will extend the CGI module with methods to track the upload +# progress for multipart forms for use with progress meters. The progress is +# saved in the session to be used from any request from any server with the +# same session. In other words, this module will work across application +# instances. +# +# === Usage +# +# Just do your file-uploads as you normally would, but include an upload_id in +# the query string of your form action. Your form post action should look +# like: +# +# <form method="post" enctype="multipart/form-data" action="postaction?upload_id=SOMEIDYOUSET"> +# <input type="file" name="client_file"/> +# </form> +# +# Query the upload state in a progress by reading the progress from the session +# +# class UploadController < ApplicationController +# def upload_status +# render :text => "Percent complete: " + @session[:uploads]['SOMEIDYOUSET'].completed_percent" +# end +# end +# +# === Session options +# +# Upload progress uses the session options defined in +# ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS. If you are passing +# custom session options to your dispatcher then please follow the +# "recommended way to change session options":http://wiki.rubyonrails.com/rails/show/HowtoChangeSessionOptions +# +# === Update frequency +# +# During an upload, the progress will be written to the session every 2 +# seconds. This prevents excessive writes yet maintains a decent picture of +# the upload progress for larger files. +# +# User interfaces that update more often that every 2 seconds will display the same results. +# Consider this update frequency when designing your progress polling. +# + +require 'cgi' + +# For integration with ActionPack +require 'action_controller/base' +require 'action_controller/cgi_process' +require 'action_controller/upload_progress' + +class CGI #:nodoc: + class ProgressIO < SimpleDelegator #:nodoc: + MIN_SAVE_INTERVAL = 1.0 # Number of seconds between session saves + + attr_reader :progress, :session + + def initialize(orig_io, progress, session) + @session = session + @progress = progress + + @start_time = Time.now + @last_save_time = @start_time + save_progress + + super(orig_io) + end + + def read(*args) + data = __getobj__.read(*args) + + if data and data.size > 0 + now = Time.now + elapsed = now - @start_time + progress.update!(data.size, elapsed) + + if now - @last_save_time > MIN_SAVE_INTERVAL + save_progress + @last_save_time = now + end + else + ActionController::Base.logger.debug("CGI::ProgressIO#read returns nothing when it should return nil if IO is finished: [#{args.inspect}], a cancelled upload or old FCGI bindings. Resetting the upload progress") + + progress.reset! + save_progress + end + + data + end + + def save_progress + @session.update + end + + def finish + @session.update + ActionController::Base.logger.debug("Finished processing multipart upload in #{@progress.elapsed_seconds.to_s}s") + end + end + + module QueryExtension #:nodoc: + # Need to do lazy aliasing on the instance that we are extending because of the way QueryExtension + # gets included for each instance of the CGI object rather than on a module level. This method is a + # bit obtrusive because we are overriding CGI::QueryExtension::extended which could be used in the + # future. Need to research a better method + def self.extended(obj) + obj.instance_eval do + # unless defined? will prevent clobbering the progress IO on multiple extensions + alias :stdinput_without_progress :stdinput unless defined? stdinput_without_progress + alias :stdinput :stdinput_with_progress + end + end + + def stdinput_with_progress + @stdin_with_progress or stdinput_without_progress + end + + private + # Bootstrapped on ActionController::UploadProgress::upload_status_for + def read_multipart_with_progress(boundary, content_length) + begin + begin + # Session disabled if the default session options have been set to 'false' + options = ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS + raise RuntimeError.new("Multipart upload progress disabled, no session options") unless options + + options = options.stringify_keys + + #Controllers.const_load!(:ApplicationController, "application") unless Controllers.const_defined?(:ApplicationController) + + # Assumes that @cookies has already been setup + # Raises nomethod if upload_id is not defined + @params = CGI::parse(read_params_from_query) + upload_id = @params[(options['upload_key'] || 'upload_id')].first + raise RuntimeError.new("Multipart upload progress disabled, no upload id in query string") unless upload_id + + upload_progress = ActionController::UploadProgress::Progress.new(content_length) + + session = Session.new(self, options) + session[:uploads] = {} unless session[:uploads] + session[:uploads].delete(upload_id) # in case the same upload id is used twice + session[:uploads][upload_id] = upload_progress + + @stdin_with_progress = CGI::ProgressIO.new(stdinput_without_progress, upload_progress, session) + ActionController::Base.logger.debug("Multipart upload with progress (id: #{upload_id}, size: #{content_length})") + rescue + ActionController::Base.logger.debug("Exception during setup of read_multipart_with_progress: #{$!}") + end + ensure + begin + params = read_multipart_without_progress(boundary, content_length) + @stdin_with_progress.finish if @stdin_with_progress.respond_to? :finish + ensure + @stdin_with_progress = nil + session.close if session + end + end + params + end + + # Prevent redefinition of aliases on multiple includes + unless private_instance_methods.include?("read_multipart_without_progress") + alias_method :read_multipart_without_progress, :read_multipart + alias_method :read_multipart, :read_multipart_with_progress + end + + end +end |