# 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: # == THIS IS AN EXPERIMENTAL FEATURE # # Which means that it doesn't yet work on all systems. We're still working on full # compatibility. It's thus not advised to use this unless you've verified it to work # fully on all the systems that is a part of your environment. Consider this an extended # preview. # # To enable this module, add ActionController::Base.enable_upload_progress to your # config/environment.rb file. # # == 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 %>
total_bytes == 0
, started? == false
,
# and finished? == true
.
def upload_progress(upload_id = nil)
upload_id ||= current_upload_id
session[:uploads] && session[:uploads][upload_id] || UploadProgress::Progress.new(0)
end
# == THIS IS AN EXPERIMENTAL FEATURE
#
# Which means that it doesn't yet work on all systems. We're still working on full
# compatibility. It's thus not advised to use this unless you've verified it to work
# fully on all the systems that is a part of your environment. Consider this an extended
# preview.
#
# 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 session.update
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