mirror of
https://github.com/rails/rails.git
synced 2022-11-09 12:12:34 -05:00
beffb77e05
git-svn-id: http://svn-commit.rubyonrails.org/rails/trunk@1553 5ecf4fe2-1ee6-0310-87b1-e25e094e27de
410 lines
15 KiB
Ruby
410 lines
15 KiB
Ruby
# Unfortunately we need to require multipart_progress here and not in
|
|
# uplaod_status_for because if the upload happens to hit a fresh FCGI instance
|
|
# the upload_status_for method will be called after the CGI object is created
|
|
# Requiring here means that multipart progress will be enabled for all multipart
|
|
# postings.
|
|
require 'action_controller/cgi_ext/multipart_progress'
|
|
|
|
module ActionController #:nodoc:
|
|
# == Action Pack Upload Progress for multipart uploads
|
|
#
|
|
# The UploadProgress module aids in the process of viewing an Ajax driven
|
|
# upload status when working with multipart forms. It offers a macro that
|
|
# will prepare an action for handling the cleanup of the Ajax updating including
|
|
# passing the redirect URL and custom parameters to the Javascript finish handler.
|
|
#
|
|
# UploadProgress is available for all multipart uploads when the +upload_status_for+
|
|
# macro is called in one of your controllers.
|
|
#
|
|
# The progress is stored as an UploadProgress::Progress object in the session and
|
|
# is accessible in the controller and view with the +upload_progress+ method.
|
|
#
|
|
# For help rendering the UploadProgress enabled form and supported elements, see
|
|
# ActionView::Helpers::UploadProgressHelper.
|
|
#
|
|
# === Automatic updating on upload actions
|
|
#
|
|
# class DocumentController < ApplicationController
|
|
# upload_status_for :create
|
|
#
|
|
# def create
|
|
# # ... Your document creation action
|
|
# end
|
|
# end
|
|
#
|
|
# The +upload_status_for+ macro will override the rendering of the action passed
|
|
# if +upload_id+ is found in the query string. This allows for default
|
|
# behavior if Javascript is disabled. If you are tracking the upload progress
|
|
# then +create+ will now return the cleanup scripts that will terminate the polling
|
|
# of the upload status.
|
|
#
|
|
# === Customized status rendering
|
|
#
|
|
# class DocumentController < ApplicationController
|
|
# upload_status_for :create, :status => :custom_status
|
|
#
|
|
# def create
|
|
# # ... Your document creation action
|
|
# end
|
|
#
|
|
# def custom_status
|
|
# # ... Override this action to return content to be replaced in
|
|
# # the status container
|
|
# render :inline => "<%= upload_progress.completed_percent rescue 0 %> % complete", :layout => false
|
|
# end
|
|
#
|
|
# The default status action is +upload_status+. The results of this action
|
|
# are added used to replace the contents of the HTML elements defined in
|
|
# +upload_status_tag+. Within +upload_status+, you can load the Progress
|
|
# object from the session with the +upload_progress+ method and display your own
|
|
# results.
|
|
#
|
|
# Completion of the upload status updating occurs automatically with an +after_filter+ call to
|
|
# +finish_upload_status+. Because the upload must be posted into a hidden IFRAME to enable
|
|
# Ajax updates during the upload, +finish_upload_status+ overwrites the results of any previous
|
|
# +render+ or +redirect_to+ so it can render the necessary Javascript that will properly terminate
|
|
# the status updating loop, trigger the completion callback or redirect to the appropriate URL.
|
|
#
|
|
# ==== Basic Example (View):
|
|
#
|
|
# <%= form_tag_with_upload_progress({:action => 'create'}, {:finish => 'alert("Document Uploaded")'}) %>
|
|
# <%= upload_status_tag %>
|
|
# <%= file_field 'document', 'file' %>
|
|
# <%= end_form_tag %>
|
|
#
|
|
# ==== Basic Example (Controller):
|
|
#
|
|
# class DocumentController < ApplicationController
|
|
# upload_status_for :create
|
|
#
|
|
# def create
|
|
# @document = Document.create(params[:document])
|
|
# end
|
|
# end
|
|
#
|
|
# ==== Extended Example (View):
|
|
#
|
|
# <%= form_tag_with_upload_progress({:action => 'create'}, {}, {:action => :custom_status}) %>
|
|
# <%= upload_status_tag %>
|
|
# <%= file_field 'document', 'file' %>
|
|
# <%= submit_tag "Upload" %>
|
|
# <%= end_form_tag %>
|
|
#
|
|
# <%= form_tag_with_upload_progress({:action => 'add_preview'}, {:finish => 'alert(arguments[0])'}, {:action => :custom_status}) %>
|
|
# <%= upload_status_tag %>
|
|
# <%= submit_tag "Upload" %>
|
|
# <%= file_field 'preview', 'file' %>
|
|
# <%= end_form_tag %>
|
|
#
|
|
# ==== Extended Example (Controller):
|
|
#
|
|
# class DocumentController < ApplicationController
|
|
# upload_status_for :add_preview, :create, {:status => :custom_status}
|
|
#
|
|
# def add_preview
|
|
# @document = Document.find(params[:id])
|
|
# @document.preview = Preview.create(params[:preview])
|
|
# if @document.save
|
|
# finish_upload_status "'Preview added'"
|
|
# else
|
|
# finish_upload_status "'Preview not added'"
|
|
# end
|
|
# end
|
|
#
|
|
# def create
|
|
# @document = Document.new(params[:document])
|
|
#
|
|
# upload_progress.message = "Processing document..."
|
|
# session.update
|
|
#
|
|
# @document.save
|
|
# redirect_to :action => 'show', :id => @document.id
|
|
# end
|
|
#
|
|
# def custom_status
|
|
# render :inline => '<%= upload_progress_status %> <div>Updated at <%= Time.now %></div>', :layout => false
|
|
# end
|
|
#
|
|
#
|
|
module UploadProgress
|
|
|
|
def self.append_features(base) #:nodoc:
|
|
super
|
|
base.extend(ClassMethods)
|
|
base.helper_method :upload_progress, :next_upload_id, :last_upload_id, :current_upload_id
|
|
end
|
|
|
|
module ClassMethods
|
|
# Creates an +after_filter+ which will call +finish_upload_status+
|
|
# creating the document that will be loaded into the hidden IFRAME, terminating
|
|
# the status polling forms created with +form_with_upload_progress+.
|
|
#
|
|
# Also defines an action +upload_status+ or a action name passed as
|
|
# the <tt>:status</tt> option. This status action must match the one expected
|
|
# in the +form_tag_with_upload_progress+ helper.
|
|
#
|
|
def upload_status_for(*actions)
|
|
after_filter :finish_upload_status, :only => actions
|
|
|
|
define_method(actions.last.is_a?(Hash) && actions.last[:status] || :upload_status) do
|
|
render(:inline => '<%= upload_progress_status %>', :layout => false)
|
|
end
|
|
end
|
|
end
|
|
|
|
# Overwrites the body rendered if the upload comes from a form that tracks
|
|
# the progress of the upload. After clearing the body and any redirects, this
|
|
# method then renders the helper +finish_upload_status+
|
|
#
|
|
# This method only needs to be called if you wish to pass a
|
|
# javascript parameter to your finish event handler that you optionally
|
|
# define in +form_with_upload_progress+
|
|
#
|
|
# === Parameter:
|
|
#
|
|
# client_js_argument:: a string containing a Javascript expression that will
|
|
# be evaluated and passed to your +finish+ handler of
|
|
# +form_tag_with_upload_progress+.
|
|
#
|
|
# You can pass a String, Number or Boolean.
|
|
#
|
|
# === Strings
|
|
#
|
|
# Strings contain Javascript code that will be evaluated on the client. If you
|
|
# wish to pass a string to the client finish callback, you will need to include
|
|
# quotes in the +client_js_argument+ you pass to this method.
|
|
#
|
|
# ==== Example
|
|
#
|
|
# finish_upload_status("\"Finished\"")
|
|
# finish_upload_status("'Finished #{@document.title}'")
|
|
# finish_upload_status("{success: true, message: 'Done!'}")
|
|
# finish_upload_status("function() { alert('Uploaded!'); }")
|
|
#
|
|
# === Numbers / Booleans
|
|
#
|
|
# Numbers and Booleans can either be passed as Number objects or string versions
|
|
# of number objects as they are evaluated by Javascript the same way as in Ruby.
|
|
#
|
|
# ==== Example
|
|
#
|
|
# finish_upload_status(0)
|
|
# finish_upload_status(@document.file.size)
|
|
# finish_upload_status("10")
|
|
#
|
|
# === Nil
|
|
#
|
|
# To pass +nil+ to the finish callback, use a string "undefined"
|
|
#
|
|
# ==== Example
|
|
#
|
|
# finish_upload_status(@message || "undefined")
|
|
#
|
|
# == Redirection
|
|
#
|
|
# If you action performs a redirection then +finish_upload_status+ will recognize
|
|
# the redirection and properly create the Javascript to perform the redirection in
|
|
# the proper location.
|
|
#
|
|
# It is possible to redirect and pass a parameter to the finish callback.
|
|
#
|
|
# ==== Example
|
|
#
|
|
# redirect_to :action => 'show', :id => @document.id
|
|
# finish_upload_status("'Redirecting you to your new file'")
|
|
#
|
|
#
|
|
def finish_upload_status(client_js_argument='')
|
|
if not @rendered_finish_upload_status and params[:upload_id]
|
|
@rendered_finish_upload_status = true
|
|
|
|
erase_render_results
|
|
location = erase_redirect_results || ''
|
|
|
|
## TODO determine if #inspect is the appropriate way to marshall values
|
|
## in inline templates
|
|
|
|
template = "<%= finish_upload_status({"
|
|
template << ":client_js_argument => #{client_js_argument.inspect}, "
|
|
template << ":redirect_to => #{location.to_s.inspect}, "
|
|
template << "}) %>"
|
|
|
|
render({ :inline => template, :layout => false })
|
|
end
|
|
end
|
|
|
|
# Returns and saves the next unique +upload_id+ in the instance variable
|
|
# <tt>@upload_id</tt>
|
|
def next_upload_id
|
|
@upload_id = last_upload_id.succ
|
|
end
|
|
|
|
# Either returns the last saved +upload_id+ or looks in the session
|
|
# for the last used +upload_id+ and saves it as the intance variable
|
|
# <tt>@upload_id</tt>
|
|
def last_upload_id
|
|
@upload_id ||= ((session[:uploads] || {}).keys.map{|k| k.to_i}.sort.last || 0).to_s
|
|
end
|
|
|
|
# Returns the +upload_id+ from the query parameters or if it cannot be found
|
|
# in the query parameters, then return the +last_upload_id+
|
|
def current_upload_id
|
|
params[:upload_id] or last_upload_id
|
|
end
|
|
|
|
# Get the UploadProgress::Progress object for the supplied +upload_id+ from the
|
|
# session. If no +upload_id+ is given, then use the +current_upload_id+
|
|
#
|
|
# If an UploadProgress::Progress object cannot be found, a new instance will be
|
|
# returned with <code>total_bytes == 0</code>, <code>started? == false</code>,
|
|
# and <code>finished? == true</code>.
|
|
def upload_progress(upload_id = nil)
|
|
upload_id ||= current_upload_id
|
|
session[:uploads] && session[:uploads][upload_id] || UploadProgress::Progress.new(0)
|
|
end
|
|
|
|
# Upload Progress abstracts the progress of an upload. It's used by the
|
|
# multipart progress IO that keeps track of the upload progress and creating
|
|
# the application depends on. It contians methods to update the progress
|
|
# during an upload and read the statistics such as +received_bytes+,
|
|
# +total_bytes+, +completed_percent+, +bitrate+, and
|
|
# +remaining_seconds+
|
|
#
|
|
# You can get the current +Progress+ object by calling +upload_progress+ instance
|
|
# method in your controller or view.
|
|
#
|
|
class Progress
|
|
unless const_defined? :MIN_SAMPLE_TIME
|
|
# Number of seconds between bitrate samples. Updates that occur more
|
|
# frequently than +MIN_SAMPLE_TIME+ will not be queued until this
|
|
# time passes. This behavior gives a good balance of accuracy and load
|
|
# for both fast and slow transfers.
|
|
MIN_SAMPLE_TIME = 0.150
|
|
|
|
# Number of seconds between updates before giving up to try and calculate
|
|
# bitrate anymore
|
|
MIN_STALL_TIME = 10.0
|
|
|
|
# Number of samples used to calculate bitrate
|
|
MAX_SAMPLES = 20
|
|
end
|
|
|
|
# Number bytes received from the multipart post
|
|
attr_reader :received_bytes
|
|
|
|
# Total number of bytes expected from the mutlipart post
|
|
attr_reader :total_bytes
|
|
|
|
# The last time the upload history was updated
|
|
attr_reader :last_update_time
|
|
|
|
# A message you can set from your controller or view to be rendered in the
|
|
# +upload_status_text+ helper method. If you set a messagein a controller
|
|
# then call <code>session.update</code> to make that message available to
|
|
# your +upload_status+ action.
|
|
attr_accessor :message
|
|
|
|
# Create a new Progress object passing the expected number of bytes to receive
|
|
def initialize(total)
|
|
@total_bytes = total
|
|
reset!
|
|
end
|
|
|
|
# Resets the received_bytes, last_update_time, message and bitrate, but
|
|
# but maintains the total expected bytes
|
|
def reset!
|
|
@received_bytes, @last_update_time, @stalled, @message = 0, 0, false, ''
|
|
reset_history
|
|
end
|
|
|
|
# Number of bytes left for this upload
|
|
def remaining_bytes
|
|
@total_bytes - @received_bytes
|
|
end
|
|
|
|
# Completed percent in integer form from 0..100
|
|
def completed_percent
|
|
(@received_bytes * 100 / @total_bytes).to_i rescue 0
|
|
end
|
|
|
|
# Updates this UploadProgress object with the number of bytes received
|
|
# since last update time and the absolute number of seconds since the
|
|
# beginning of the upload.
|
|
#
|
|
# This method is used by the +MultipartProgress+ module and should
|
|
# not be called directly.
|
|
def update!(bytes, elapsed_seconds)#:nodoc:
|
|
if @received_bytes + bytes > @total_bytes
|
|
#warn "Progress#update received bytes exceeds expected bytes"
|
|
bytes = @total_bytes - @received_bytes
|
|
end
|
|
|
|
@received_bytes += bytes
|
|
|
|
# Age is the duration of time since the last update to the history
|
|
age = elapsed_seconds - @last_update_time
|
|
|
|
# Record the bytes received in the first element of the history
|
|
# in case the sample rate is exceeded and we shouldn't record at this
|
|
# time
|
|
@history.first[0] += bytes
|
|
@history.first[1] += age
|
|
|
|
history_age = @history.first[1]
|
|
|
|
@history.pop while @history.size > MAX_SAMPLES
|
|
@history.unshift([0,0]) if history_age > MIN_SAMPLE_TIME
|
|
|
|
if history_age > MIN_STALL_TIME
|
|
@stalled = true
|
|
reset_history
|
|
else
|
|
@stalled = false
|
|
end
|
|
|
|
@last_update_time = elapsed_seconds
|
|
|
|
self
|
|
end
|
|
|
|
# Calculates the bitrate in bytes/second. If the transfer is stalled or
|
|
# just started, the bitrate will be 0
|
|
def bitrate
|
|
history_bytes, history_time = @history.transpose.map { |vals| vals.inject { |sum, v| sum + v } }
|
|
history_bytes / history_time rescue 0
|
|
end
|
|
|
|
# Number of seconds elapsed since the start of the upload
|
|
def elapsed_seconds
|
|
@last_update_time
|
|
end
|
|
|
|
# Calculate the seconds remaining based on the current bitrate. Returns
|
|
# O seconds if stalled or if no bytes have been received
|
|
def remaining_seconds
|
|
remaining_bytes / bitrate rescue 0
|
|
end
|
|
|
|
# Returns true if there are bytes pending otherwise returns false
|
|
def finished?
|
|
remaining_bytes <= 0
|
|
end
|
|
|
|
# Returns true if some bytes have been received
|
|
def started?
|
|
@received_bytes > 0
|
|
end
|
|
|
|
# Returns true if there has been a delay in receiving bytes. The delay
|
|
# is set by the constant MIN_STALL_TIME
|
|
def stalled?
|
|
@stalled
|
|
end
|
|
|
|
private
|
|
def reset_history
|
|
@history = [[0,0]]
|
|
end
|
|
end
|
|
end
|
|
end
|