aboutsummaryrefslogblamecommitdiffstats
path: root/actionpack/lib/action_controller/cgi_ext/multipart_progress.rb
blob: d297c6f86ee92753083ed88a18605c0eb6497596 (plain) (tree)






































































































































































                                                                                                                                                                                                                           
# == 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