actionpack 1.8.1 → 1.9.0
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of actionpack might be problematic. Click here for more details.
- data/CHANGELOG +309 -16
- data/README +1 -1
- data/lib/action_controller.rb +5 -0
- data/lib/action_controller/assertions.rb +57 -12
- data/lib/action_controller/auto_complete.rb +47 -0
- data/lib/action_controller/base.rb +288 -258
- data/lib/action_controller/benchmarking.rb +8 -3
- data/lib/action_controller/caching.rb +88 -42
- data/lib/action_controller/cgi_ext/cgi_ext.rb +1 -1
- data/lib/action_controller/cgi_ext/cgi_methods.rb +41 -11
- data/lib/action_controller/cgi_ext/multipart_progress.rb +169 -0
- data/lib/action_controller/cgi_ext/raw_post_data_fix.rb +30 -12
- data/lib/action_controller/cgi_process.rb +39 -11
- data/lib/action_controller/code_generation.rb +235 -0
- data/lib/action_controller/cookies.rb +14 -8
- data/lib/action_controller/deprecated_renders_and_redirects.rb +76 -0
- data/lib/action_controller/filters.rb +8 -7
- data/lib/action_controller/helpers.rb +41 -6
- data/lib/action_controller/layout.rb +45 -16
- data/lib/action_controller/request.rb +86 -23
- data/lib/action_controller/rescue.rb +1 -0
- data/lib/action_controller/response.rb +1 -1
- data/lib/action_controller/routing.rb +536 -272
- data/lib/action_controller/scaffolding.rb +30 -25
- data/lib/action_controller/session/active_record_store.rb +251 -50
- data/lib/action_controller/streaming.rb +133 -0
- data/lib/action_controller/templates/rescues/_request_and_response.rhtml +0 -7
- data/lib/action_controller/templates/scaffolds/edit.rhtml +2 -2
- data/lib/action_controller/templates/scaffolds/layout.rhtml +22 -18
- data/lib/action_controller/templates/scaffolds/list.rhtml +3 -3
- data/lib/action_controller/templates/scaffolds/new.rhtml +2 -2
- data/lib/action_controller/templates/scaffolds/show.rhtml +1 -1
- data/lib/action_controller/test_process.rb +68 -47
- data/lib/action_controller/upload_progress.rb +421 -0
- data/lib/action_controller/url_rewriter.rb +8 -11
- data/lib/action_controller/vendor/html-scanner/html/document.rb +6 -5
- data/lib/action_controller/vendor/html-scanner/html/node.rb +70 -14
- data/lib/action_controller/vendor/html-scanner/html/tokenizer.rb +17 -10
- data/lib/action_controller/vendor/html-scanner/html/version.rb +3 -3
- data/lib/action_controller/vendor/xml_simple.rb +1019 -0
- data/lib/action_controller/verification.rb +36 -30
- data/lib/action_view/base.rb +21 -14
- data/lib/action_view/helpers/active_record_helper.rb +15 -13
- data/lib/action_view/helpers/asset_tag_helper.rb +26 -9
- data/lib/action_view/helpers/benchmark_helper.rb +24 -0
- data/lib/action_view/helpers/capture_helper.rb +7 -5
- data/lib/action_view/helpers/date_helper.rb +63 -46
- data/lib/action_view/helpers/form_helper.rb +7 -1
- data/lib/action_view/helpers/form_options_helper.rb +19 -11
- data/lib/action_view/helpers/form_tag_helper.rb +5 -1
- data/lib/action_view/helpers/javascript_helper.rb +403 -35
- data/lib/action_view/helpers/javascripts/controls.js +261 -0
- data/lib/action_view/helpers/javascripts/dragdrop.js +476 -0
- data/lib/action_view/helpers/javascripts/effects.js +570 -0
- data/lib/action_view/helpers/javascripts/prototype.js +633 -371
- data/lib/action_view/helpers/number_helper.rb +11 -13
- data/lib/action_view/helpers/tag_helper.rb +1 -2
- data/lib/action_view/helpers/text_helper.rb +69 -6
- data/lib/action_view/helpers/upload_progress_helper.rb +433 -0
- data/lib/action_view/helpers/url_helper.rb +98 -3
- data/lib/action_view/partials.rb +14 -8
- data/lib/action_view/vendor/builder/xmlmarkup.rb +11 -0
- data/rakefile +13 -5
- data/test/abstract_unit.rb +1 -1
- data/test/controller/action_pack_assertions_test.rb +52 -9
- data/test/controller/active_record_assertions_test.rb +119 -120
- data/test/controller/active_record_store_test.rb +111 -0
- data/test/controller/addresses_render_test.rb +45 -0
- data/test/controller/caching_filestore.rb +92 -0
- data/test/controller/capture_test.rb +39 -0
- data/test/controller/cgi_test.rb +40 -3
- data/test/controller/helper_test.rb +65 -13
- data/test/controller/multipart_progress_testx.rb +365 -0
- data/test/controller/new_render_test.rb +263 -0
- data/test/controller/redirect_test.rb +64 -0
- data/test/controller/render_test.rb +20 -21
- data/test/controller/request_test.rb +83 -3
- data/test/controller/routing_test.rb +702 -0
- data/test/controller/send_file_test.rb +2 -0
- data/test/controller/test_test.rb +44 -8
- data/test/controller/upload_progress_testx.rb +89 -0
- data/test/controller/verification_test.rb +94 -29
- data/test/fixtures/addresses/list.rhtml +1 -0
- data/test/fixtures/test/capturing.rhtml +4 -0
- data/test/fixtures/test/list.rhtml +1 -1
- data/test/fixtures/test/update_element_with_capture.rhtml +9 -0
- data/test/template/active_record_helper_test.rb +30 -15
- data/test/template/asset_tag_helper_test.rb +12 -5
- data/test/template/benchmark_helper_test.rb +72 -0
- data/test/template/date_helper_test.rb +69 -0
- data/test/template/form_helper_test.rb +18 -10
- data/test/template/form_options_helper_test.rb +40 -5
- data/test/template/javascript_helper.rb +149 -2
- data/test/template/number_helper_test.rb +2 -0
- data/test/template/tag_helper_test.rb +4 -0
- data/test/template/text_helper_test.rb +36 -0
- data/test/template/upload_progress_helper_testx.rb +272 -0
- data/test/template/url_helper_test.rb +30 -0
- metadata +30 -6
- data/test/controller/layout_test.rb +0 -49
- data/test/controller/routing_tests.rb +0 -543
@@ -15,18 +15,22 @@ module ActionController #:nodoc:
|
|
15
15
|
}
|
16
16
|
end
|
17
17
|
|
18
|
-
def render_with_benchmark(
|
18
|
+
def render_with_benchmark(options = {}, deprecated_status = nil)
|
19
19
|
if logger.nil?
|
20
|
-
render_without_benchmark(
|
20
|
+
render_without_benchmark(options, deprecated_status)
|
21
21
|
else
|
22
22
|
db_runtime = ActiveRecord::Base.connection.reset_runtime if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
|
23
|
-
|
23
|
+
|
24
|
+
render_output = nil
|
25
|
+
@rendering_runtime = Benchmark::measure{ render_output = render_without_benchmark(options, deprecated_status) }.real
|
24
26
|
|
25
27
|
if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
|
26
28
|
@db_rt_before_render = db_runtime
|
27
29
|
@db_rt_after_render = ActiveRecord::Base.connection.reset_runtime
|
28
30
|
@rendering_runtime -= @db_rt_after_render
|
29
31
|
end
|
32
|
+
|
33
|
+
render_output
|
30
34
|
end
|
31
35
|
end
|
32
36
|
|
@@ -38,6 +42,7 @@ module ActionController #:nodoc:
|
|
38
42
|
log_message = "Completed in #{sprintf("%.5f", runtime)} (#{(1 / runtime).floor} reqs/sec)"
|
39
43
|
log_message << rendering_runtime(runtime) if @rendering_runtime
|
40
44
|
log_message << active_record_runtime(runtime) if Object.const_defined?("ActiveRecord") && ActiveRecord::Base.connected?
|
45
|
+
log_message << " [#{complete_request_uri}]"
|
41
46
|
logger.info(log_message)
|
42
47
|
end
|
43
48
|
end
|
@@ -256,13 +256,8 @@ module ActionController #:nodoc:
|
|
256
256
|
end
|
257
257
|
end
|
258
258
|
|
259
|
-
def cache_base_url
|
260
|
-
@@cache_base_url ||= url_for(:controller => '')
|
261
|
-
end
|
262
|
-
|
263
259
|
def fragment_cache_key(name)
|
264
|
-
|
265
|
-
key.split("://").last
|
260
|
+
name.is_a?(Hash) ? url_for(name).split("://").last : name
|
266
261
|
end
|
267
262
|
|
268
263
|
# Called by CacheHelper#cache
|
@@ -281,6 +276,8 @@ module ActionController #:nodoc:
|
|
281
276
|
end
|
282
277
|
|
283
278
|
def write_fragment(name, content, options = {})
|
279
|
+
return unless perform_caching
|
280
|
+
|
284
281
|
key = fragment_cache_key(name)
|
285
282
|
fragment_cache_store.write(key, content, options)
|
286
283
|
logger.info "Cached fragment: #{key}" unless logger.nil?
|
@@ -288,6 +285,8 @@ module ActionController #:nodoc:
|
|
288
285
|
end
|
289
286
|
|
290
287
|
def read_fragment(name, options = {})
|
288
|
+
return unless perform_caching
|
289
|
+
|
291
290
|
key = fragment_cache_key(name)
|
292
291
|
if cache = fragment_cache_store.read(key, options)
|
293
292
|
logger.info "Fragment hit: #{key}" unless logger.nil?
|
@@ -297,16 +296,27 @@ module ActionController #:nodoc:
|
|
297
296
|
end
|
298
297
|
end
|
299
298
|
|
299
|
+
# Name can take one of three forms:
|
300
|
+
# * String: This would normally take the form of a path like "pages/45/notes"
|
301
|
+
# * Hash: Is treated as an implicit call to url_for, like { :controller => "pages", :action => "notes", :id => 45 }
|
302
|
+
# * Regexp: Will destroy all the matched fragments, example: %r{pages/\d*/notes}
|
300
303
|
def expire_fragment(name, options = {})
|
304
|
+
return unless perform_caching
|
305
|
+
|
301
306
|
key = fragment_cache_key(name)
|
302
|
-
|
303
|
-
|
307
|
+
|
308
|
+
if key.is_a?(Regexp)
|
309
|
+
fragment_cache_store.delete_matched(key, options)
|
310
|
+
logger.info "Expired fragments matching: #{key.source}" unless logger.nil?
|
311
|
+
else
|
312
|
+
fragment_cache_store.delete(key, options)
|
313
|
+
logger.info "Expired fragment: #{key}" unless logger.nil?
|
314
|
+
end
|
304
315
|
end
|
305
316
|
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
logger.info "Expired all fragments matching: #{rp}#{re.source}" unless logger.nil?
|
317
|
+
# Deprecated -- just call expire_fragment with a regular expression
|
318
|
+
def expire_matched_fragments(matcher = /.*/, options = {}) #:nodoc:
|
319
|
+
expire_fragment(matcher, options)
|
310
320
|
end
|
311
321
|
|
312
322
|
class MemoryStore #:nodoc:
|
@@ -326,9 +336,8 @@ module ActionController #:nodoc:
|
|
326
336
|
@mutex.synchronize { @data.delete(name) }
|
327
337
|
end
|
328
338
|
|
329
|
-
def delete_matched(
|
330
|
-
|
331
|
-
@mutex.synchronize { @data.delete_if { |k,v| k =~ re } }
|
339
|
+
def delete_matched(matcher, options) #:nodoc:
|
340
|
+
@mutex.synchronize { @data.delete_if { |k,v| k =~ matcher } }
|
332
341
|
end
|
333
342
|
end
|
334
343
|
|
@@ -361,38 +370,42 @@ module ActionController #:nodoc:
|
|
361
370
|
end
|
362
371
|
|
363
372
|
def delete(name, options) #:nodoc:
|
364
|
-
File.delete(real_file_path(name))
|
373
|
+
File.delete(real_file_path(name))
|
374
|
+
rescue SystemCallError => e
|
375
|
+
Base.logger.info "Couldn't expire cache #{name} (#{e.message})" unless Base.logger.nil?
|
365
376
|
end
|
366
377
|
|
367
|
-
def delete_matched(
|
368
|
-
|
369
|
-
|
370
|
-
|
378
|
+
def delete_matched(matcher, options) #:nodoc:
|
379
|
+
search_dir(@cache_path) do |f|
|
380
|
+
if f =~ matcher
|
381
|
+
begin
|
382
|
+
File.delete(f)
|
383
|
+
rescue Object => e
|
384
|
+
Base.logger.info "Couldn't expire cache: #{f} (#{e.message})" unless Base.logger.nil?
|
385
|
+
end
|
386
|
+
end
|
371
387
|
end
|
372
388
|
end
|
373
389
|
|
374
390
|
private
|
375
391
|
def real_file_path(name)
|
376
|
-
'%s/%s' % [@cache_path, name.gsub('?', '.').gsub(':', '.')]
|
392
|
+
'%s/%s.cache' % [@cache_path, name.gsub('?', '.').gsub(':', '.')]
|
377
393
|
end
|
378
394
|
|
379
395
|
def ensure_cache_path(path)
|
380
396
|
FileUtils.makedirs(path) unless File.exists?(path)
|
381
397
|
end
|
382
398
|
|
383
|
-
def search_dir(dir)
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
files << p.to_s if p.file?
|
392
|
-
files += search_dir(d) if p.directory?
|
399
|
+
def search_dir(dir, &callback)
|
400
|
+
Dir.foreach(dir) do |d|
|
401
|
+
next if d == "." || d == ".."
|
402
|
+
name = File.join(dir, d)
|
403
|
+
if File.directory?(name)
|
404
|
+
search_dir(name, &callback)
|
405
|
+
else
|
406
|
+
callback.call name
|
393
407
|
end
|
394
408
|
end
|
395
|
-
files
|
396
409
|
end
|
397
410
|
end
|
398
411
|
end
|
@@ -400,17 +413,14 @@ module ActionController #:nodoc:
|
|
400
413
|
# Sweepers are the terminators of the caching world and responsible for expiring caches when model objects change.
|
401
414
|
# They do this by being half-observers, half-filters and implementing callbacks for both roles. A Sweeper example:
|
402
415
|
#
|
403
|
-
# class ListSweeper <
|
416
|
+
# class ListSweeper < ActionController::Caching::Sweeper
|
404
417
|
# observe List, Item
|
405
418
|
#
|
406
419
|
# def after_save(record)
|
407
|
-
#
|
408
|
-
#
|
409
|
-
#
|
410
|
-
#
|
411
|
-
# controller.expire_page(:controller => "lists", :action => %w( show public feed ), :id => @list.id)
|
412
|
-
# controller.expire_action(:controller => "lists", :action => "all")
|
413
|
-
# @list.shares.each { |share| controller.expire_page(:controller => "lists", :action => "show", :id => share.url_key) }
|
420
|
+
# list = record.is_a?(List) ? record : record.list
|
421
|
+
# expire_page(:controller => "lists", :action => %w( show public feed ), :id => list.id)
|
422
|
+
# expire_action(:controller => "lists", :action => "all")
|
423
|
+
# list.shares.each { |share| expire_page(:controller => "lists", :action => "show", :id => share.url_key) }
|
414
424
|
# end
|
415
425
|
# end
|
416
426
|
#
|
@@ -432,12 +442,48 @@ module ActionController #:nodoc:
|
|
432
442
|
def cache_sweeper(*sweepers)
|
433
443
|
return unless perform_caching
|
434
444
|
configuration = sweepers.last.is_a?(Hash) ? sweepers.pop : {}
|
435
|
-
sweepers.each do |sweeper|
|
445
|
+
sweepers.each do |sweeper|
|
436
446
|
observer(sweeper)
|
437
|
-
|
447
|
+
|
448
|
+
sweeper_instance = Object.const_get(Inflector.classify(sweeper)).instance
|
449
|
+
|
450
|
+
if sweeper_instance.is_a?(Sweeper)
|
451
|
+
around_filter(sweeper_instance, :only => configuration[:only])
|
452
|
+
else
|
453
|
+
after_filter(sweeper_instance, :only => configuration[:only])
|
454
|
+
end
|
438
455
|
end
|
439
456
|
end
|
440
457
|
end
|
441
458
|
end
|
459
|
+
|
460
|
+
if defined?(ActiveRecord::Observer)
|
461
|
+
class Sweeper < ActiveRecord::Observer #:nodoc:
|
462
|
+
attr_accessor :controller
|
463
|
+
|
464
|
+
def before(controller)
|
465
|
+
self.controller = controller
|
466
|
+
callback(:before)
|
467
|
+
end
|
468
|
+
|
469
|
+
def after(controller)
|
470
|
+
callback(:after)
|
471
|
+
end
|
472
|
+
|
473
|
+
private
|
474
|
+
def callback(timing)
|
475
|
+
controller_callback_method_name = "#{timing}_#{controller.controller_name.underscore}"
|
476
|
+
action_callback_method_name = "#{controller_callback_method_name}_#{controller.action_name}"
|
477
|
+
|
478
|
+
send(controller_callback_method_name) if respond_to?(controller_callback_method_name)
|
479
|
+
send(action_callback_method_name) if respond_to?(action_callback_method_name)
|
480
|
+
end
|
481
|
+
|
482
|
+
def method_missing(method, *arguments)
|
483
|
+
return if @controller.nil?
|
484
|
+
@controller.send(method, *arguments)
|
485
|
+
end
|
486
|
+
end
|
487
|
+
end
|
442
488
|
end
|
443
489
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
require 'cgi'
|
2
|
+
require 'action_controller/vendor/xml_simple'
|
2
3
|
|
3
4
|
# Static methods for parsing the query and request parameters that can be used in
|
4
5
|
# a CGI extension class or testing in isolation.
|
@@ -10,25 +11,25 @@ class CGIMethods #:nodoc:
|
|
10
11
|
parsed_params = {}
|
11
12
|
|
12
13
|
query_string.split(/[&;]/).each { |p|
|
13
|
-
k, v = p.split('=')
|
14
|
+
k, v = p.split('=',2)
|
15
|
+
v = nil if (!v.nil? && v.empty?)
|
14
16
|
|
15
17
|
k = CGI.unescape(k) unless k.nil?
|
16
18
|
v = CGI.unescape(v) unless v.nil?
|
17
19
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
else
|
25
|
-
parsed_params[k] = v.nil? ? nil : v
|
20
|
+
keys = split_key(k)
|
21
|
+
last_key = keys.pop
|
22
|
+
last_key = keys.pop if (use_array = last_key.empty?)
|
23
|
+
parent = keys.inject(parsed_params) {|h, k| h[k] ||= {}}
|
24
|
+
|
25
|
+
if use_array then (parent[last_key] ||= []) << v
|
26
|
+
else parent[last_key] = v
|
26
27
|
end
|
27
28
|
}
|
28
29
|
|
29
30
|
return parsed_params
|
30
31
|
end
|
31
|
-
|
32
|
+
|
32
33
|
# Returns the request (POST/GET) parameters in a parsed form where pairs such as "customer[address][street]" /
|
33
34
|
# "Somewhere cool!" are translated into a full hash hierarchy, like
|
34
35
|
# { "customer" => { "address" => { "street" => "Somewhere cool!" } } }
|
@@ -47,7 +48,36 @@ class CGIMethods #:nodoc:
|
|
47
48
|
return parsed_params
|
48
49
|
end
|
49
50
|
|
51
|
+
def self.parse_formatted_request_parameters(format, raw_post_data)
|
52
|
+
case format
|
53
|
+
when :xml
|
54
|
+
return XmlSimple.xml_in(raw_post_data, 'ForceArray' => false)
|
55
|
+
when :yaml
|
56
|
+
return YAML.load(raw_post_data)
|
57
|
+
end
|
58
|
+
rescue Object => e
|
59
|
+
{ "exception" => "#{e.message} (#{e.class})", "backtrace" => e.backtrace,
|
60
|
+
"raw_post_data" => raw_post_data, "format" => format }
|
61
|
+
end
|
62
|
+
|
50
63
|
private
|
64
|
+
|
65
|
+
# Splits the given key into several pieces. Example keys are 'name', 'person[name]',
|
66
|
+
# 'person[name][first]', and 'people[]'. In each instance, an Array instance is returned.
|
67
|
+
# 'person[name][first]' produces ['person', 'name', 'first']; 'people[]' produces ['people', '']
|
68
|
+
def CGIMethods.split_key(key)
|
69
|
+
if /^([^\[]+)((?:\[[^\]]*\])+)$/ =~ key
|
70
|
+
keys = [$1]
|
71
|
+
|
72
|
+
keys.concat($2[1..-2].split(']['))
|
73
|
+
keys << '' if key[-2..-1] == '[]' # Have to add it since split will drop empty strings
|
74
|
+
|
75
|
+
return keys
|
76
|
+
else
|
77
|
+
return [key]
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
51
81
|
def CGIMethods.get_typed_value(value)
|
52
82
|
if value.respond_to?(:content_type) && !value.content_type.empty?
|
53
83
|
# Uploaded file
|
@@ -56,7 +86,7 @@ class CGIMethods #:nodoc:
|
|
56
86
|
# Value as part of a multipart request
|
57
87
|
value.read
|
58
88
|
elsif value.class == Array
|
59
|
-
|
89
|
+
value.collect { |v| CGIMethods.get_typed_value(v) }
|
60
90
|
else
|
61
91
|
# Standard value (not a multipart request)
|
62
92
|
value.to_s
|
@@ -0,0 +1,169 @@
|
|
1
|
+
# == Overview
|
2
|
+
#
|
3
|
+
# This module will extend the CGI module with methods to track the upload
|
4
|
+
# progress for multipart forms for use with progress meters. The progress is
|
5
|
+
# saved in the session to be used from any request from any server with the
|
6
|
+
# same session. In other words, this module will work across application
|
7
|
+
# instances.
|
8
|
+
#
|
9
|
+
# === Usage
|
10
|
+
#
|
11
|
+
# Just do your file-uploads as you normally would, but include an upload_id in
|
12
|
+
# the query string of your form action. Your form post action should look
|
13
|
+
# like:
|
14
|
+
#
|
15
|
+
# <form method="post" enctype="multipart/form-data" action="postaction?upload_id=SOMEIDYOUSET">
|
16
|
+
# <input type="file" name="client_file"/>
|
17
|
+
# </form>
|
18
|
+
#
|
19
|
+
# Query the upload state in a progress by reading the progress from the session
|
20
|
+
#
|
21
|
+
# class UploadController < ApplicationController
|
22
|
+
# def upload_status
|
23
|
+
# render :text => "Percent complete: " + @session[:uploads]['SOMEIDYOUSET'].completed_percent"
|
24
|
+
# end
|
25
|
+
# end
|
26
|
+
#
|
27
|
+
# === Session options
|
28
|
+
#
|
29
|
+
# Upload progress uses the session options defined in
|
30
|
+
# ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS. If you are passing
|
31
|
+
# custom session options to your dispatcher then please follow the
|
32
|
+
# "recommended way to change session options":http://wiki.rubyonrails.com/rails/show/HowtoChangeSessionOptions
|
33
|
+
#
|
34
|
+
# === Update frequency
|
35
|
+
#
|
36
|
+
# During an upload, the progress will be written to the session every 2
|
37
|
+
# seconds. This prevents excessive writes yet maintains a decent picture of
|
38
|
+
# the upload progress for larger files.
|
39
|
+
#
|
40
|
+
# User interfaces that update more often that every 2 seconds will display the same results.
|
41
|
+
# Consider this update frequency when designing your progress polling.
|
42
|
+
#
|
43
|
+
|
44
|
+
require 'cgi'
|
45
|
+
|
46
|
+
# For integration with ActionPack
|
47
|
+
require 'action_controller/base'
|
48
|
+
require 'action_controller/cgi_process'
|
49
|
+
require 'action_controller/upload_progress'
|
50
|
+
|
51
|
+
class CGI #:nodoc:
|
52
|
+
class ProgressIO < SimpleDelegator #:nodoc:
|
53
|
+
MIN_SAVE_INTERVAL = 1.0 # Number of seconds between session saves
|
54
|
+
|
55
|
+
attr_reader :progress, :session
|
56
|
+
|
57
|
+
def initialize(orig_io, progress, session)
|
58
|
+
@session = session
|
59
|
+
@progress = progress
|
60
|
+
|
61
|
+
@start_time = Time.now
|
62
|
+
@last_save_time = @start_time
|
63
|
+
save_progress
|
64
|
+
|
65
|
+
super(orig_io)
|
66
|
+
end
|
67
|
+
|
68
|
+
def read(*args)
|
69
|
+
data = __getobj__.read(*args)
|
70
|
+
|
71
|
+
if data and data.size > 0
|
72
|
+
now = Time.now
|
73
|
+
elapsed = now - @start_time
|
74
|
+
progress.update!(data.size, elapsed)
|
75
|
+
|
76
|
+
if now - @last_save_time > MIN_SAVE_INTERVAL
|
77
|
+
save_progress
|
78
|
+
@last_save_time = now
|
79
|
+
end
|
80
|
+
else
|
81
|
+
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")
|
82
|
+
|
83
|
+
progress.reset!
|
84
|
+
save_progress
|
85
|
+
end
|
86
|
+
|
87
|
+
data
|
88
|
+
end
|
89
|
+
|
90
|
+
def save_progress
|
91
|
+
@session.update
|
92
|
+
end
|
93
|
+
|
94
|
+
def finish
|
95
|
+
@session.update
|
96
|
+
ActionController::Base.logger.debug("Finished processing multipart upload in #{@progress.elapsed_seconds.to_s}s")
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
module QueryExtension #:nodoc:
|
101
|
+
# Need to do lazy aliasing on the instance that we are extending because of the way QueryExtension
|
102
|
+
# gets included for each instance of the CGI object rather than on a module level. This method is a
|
103
|
+
# bit obtrusive because we are overriding CGI::QueryExtension::extended which could be used in the
|
104
|
+
# future. Need to research a better method
|
105
|
+
def self.extended(obj)
|
106
|
+
obj.instance_eval do
|
107
|
+
# unless defined? will prevent clobbering the progress IO on multiple extensions
|
108
|
+
alias :stdinput_without_progress :stdinput unless defined? stdinput_without_progress
|
109
|
+
alias :stdinput :stdinput_with_progress
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def stdinput_with_progress
|
114
|
+
@stdin_with_progress or stdinput_without_progress
|
115
|
+
end
|
116
|
+
|
117
|
+
private
|
118
|
+
# Bootstrapped on ActionController::UploadProgress::upload_status_for
|
119
|
+
def read_multipart_with_progress(boundary, content_length)
|
120
|
+
begin
|
121
|
+
begin
|
122
|
+
# Session disabled if the default session options have been set to 'false'
|
123
|
+
options = ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS
|
124
|
+
raise RuntimeError.new("Multipart upload progress disabled, no session options") unless options
|
125
|
+
|
126
|
+
options = options.stringify_keys
|
127
|
+
|
128
|
+
# Pull in the application controller to satisfy any dependencies on class definitions
|
129
|
+
# of instances stored in the session.
|
130
|
+
Controllers.const_load!(:ApplicationController, "application") unless Controllers.const_defined?(:ApplicationController)
|
131
|
+
|
132
|
+
# Assumes that @cookies has already been setup
|
133
|
+
# Raises nomethod if upload_id is not defined
|
134
|
+
@params = CGI::parse(read_params_from_query)
|
135
|
+
upload_id = @params[(options['upload_key'] || 'upload_id')].first
|
136
|
+
raise RuntimeError.new("Multipart upload progress disabled, no upload id in query string") unless upload_id
|
137
|
+
|
138
|
+
upload_progress = ActionController::UploadProgress::Progress.new(content_length)
|
139
|
+
|
140
|
+
session = Session.new(self, options)
|
141
|
+
session[:uploads] = {} unless session[:uploads]
|
142
|
+
session[:uploads].delete(upload_id) # in case the same upload id is used twice
|
143
|
+
session[:uploads][upload_id] = upload_progress
|
144
|
+
|
145
|
+
@stdin_with_progress = CGI::ProgressIO.new(stdinput_without_progress, upload_progress, session)
|
146
|
+
ActionController::Base.logger.debug("Multipart upload with progress (id: #{upload_id}, size: #{content_length})")
|
147
|
+
rescue
|
148
|
+
ActionController::Base.logger.debug("Exception during setup of read_multipart_with_progress: #{$!}")
|
149
|
+
end
|
150
|
+
ensure
|
151
|
+
begin
|
152
|
+
params = read_multipart_without_progress(boundary, content_length)
|
153
|
+
@stdin_with_progress.finish if @stdin_with_progress.respond_to? :finish
|
154
|
+
ensure
|
155
|
+
@stdin_with_progress = nil
|
156
|
+
session.close if session
|
157
|
+
end
|
158
|
+
end
|
159
|
+
params
|
160
|
+
end
|
161
|
+
|
162
|
+
# Prevent redefinition of aliases on multiple includes
|
163
|
+
unless private_instance_methods.include?("read_multipart_without_progress")
|
164
|
+
alias_method :read_multipart_without_progress, :read_multipart
|
165
|
+
alias_method :read_multipart, :read_multipart_with_progress
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
end
|