1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
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
|