# == 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: # #
# #
# # 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' 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 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 # Pull in the application controller to satisfy any dependencies on class definitions # of instances stored in the session. # Be sure to stay compatible with Rails 1.0/const_load! if Object.const_defined?(:Controllers) and Controllers.respond_to?(:const_load!) Controllers.const_load!(:ApplicationController, "application") unless Controllers.const_defined?(:ApplicationController) else require_dependency('application.rb') unless Object.const_defined?(:ApplicationController) end # 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 = 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