-
Notifications
You must be signed in to change notification settings - Fork 21.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
- Loading branch information
Showing
6 changed files
with
1,730 additions
and
0 deletions.
There are no files selected for viewing
167 changes: 167 additions & 0 deletions
167
actionpack/lib/action_controller/cgi_ext/multipart_progress.rb
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
Oops, something went wrong.