dragonfly 0.1.0

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of dragonfly might be problematic. Click here for more details.

Files changed (75) hide show
  1. data/.gitignore +7 -0
  2. data/LICENSE +20 -0
  3. data/README.markdown +95 -0
  4. data/Rakefile +60 -0
  5. data/VERSION +1 -0
  6. data/config.rb +7 -0
  7. data/config.ru +10 -0
  8. data/dragonfly-rails.gemspec +41 -0
  9. data/dragonfly.gemspec +137 -0
  10. data/features/dragonfly.feature +38 -0
  11. data/features/steps/common_steps.rb +8 -0
  12. data/features/steps/dragonfly_steps.rb +39 -0
  13. data/features/support/env.rb +25 -0
  14. data/features/support/image_helpers.rb +9 -0
  15. data/generators/dragonfly_app/USAGE +16 -0
  16. data/generators/dragonfly_app/dragonfly_app_generator.rb +54 -0
  17. data/generators/dragonfly_app/templates/custom_processing.erb +13 -0
  18. data/generators/dragonfly_app/templates/initializer.erb +7 -0
  19. data/generators/dragonfly_app/templates/metal_file.erb +32 -0
  20. data/irbrc.rb +20 -0
  21. data/lib/dragonfly/active_record_extensions/attachment.rb +117 -0
  22. data/lib/dragonfly/active_record_extensions/class_methods.rb +41 -0
  23. data/lib/dragonfly/active_record_extensions/instance_methods.rb +28 -0
  24. data/lib/dragonfly/active_record_extensions/validations.rb +19 -0
  25. data/lib/dragonfly/active_record_extensions.rb +12 -0
  26. data/lib/dragonfly/analysis/analyser.rb +45 -0
  27. data/lib/dragonfly/analysis/base.rb +10 -0
  28. data/lib/dragonfly/analysis/r_magick_analyser.rb +40 -0
  29. data/lib/dragonfly/app.rb +85 -0
  30. data/lib/dragonfly/app_configuration.rb +9 -0
  31. data/lib/dragonfly/configurable.rb +113 -0
  32. data/lib/dragonfly/core_ext/object.rb +8 -0
  33. data/lib/dragonfly/data_storage/base.rb +19 -0
  34. data/lib/dragonfly/data_storage/file_data_store.rb +72 -0
  35. data/lib/dragonfly/data_storage.rb +9 -0
  36. data/lib/dragonfly/encoding/base.rb +13 -0
  37. data/lib/dragonfly/encoding/r_magick_encoder.rb +17 -0
  38. data/lib/dragonfly/extended_temp_object.rb +94 -0
  39. data/lib/dragonfly/middleware.rb +27 -0
  40. data/lib/dragonfly/parameters.rb +152 -0
  41. data/lib/dragonfly/processing/processor.rb +14 -0
  42. data/lib/dragonfly/processing/r_magick_processor.rb +71 -0
  43. data/lib/dragonfly/r_magick_configuration.rb +47 -0
  44. data/lib/dragonfly/rails/images.rb +20 -0
  45. data/lib/dragonfly/temp_object.rb +118 -0
  46. data/lib/dragonfly/url_handler.rb +148 -0
  47. data/lib/dragonfly.rb +33 -0
  48. data/samples/beach.png +0 -0
  49. data/samples/egg.png +0 -0
  50. data/samples/round.gif +0 -0
  51. data/samples/taj.jpg +0 -0
  52. data/spec/argument_matchers.rb +29 -0
  53. data/spec/dragonfly/active_record_extensions/attachment_spec.rb +8 -0
  54. data/spec/dragonfly/active_record_extensions/initializer.rb +1 -0
  55. data/spec/dragonfly/active_record_extensions/migration.rb +21 -0
  56. data/spec/dragonfly/active_record_extensions/model_spec.rb +400 -0
  57. data/spec/dragonfly/active_record_extensions/models.rb +2 -0
  58. data/spec/dragonfly/active_record_extensions/spec_helper.rb +23 -0
  59. data/spec/dragonfly/analysis/analyser_spec.rb +85 -0
  60. data/spec/dragonfly/analysis/r_magick_analyser_spec.rb +35 -0
  61. data/spec/dragonfly/app_spec.rb +69 -0
  62. data/spec/dragonfly/configurable_spec.rb +193 -0
  63. data/spec/dragonfly/data_storage/data_store_spec.rb +47 -0
  64. data/spec/dragonfly/data_storage/file_data_store_spec.rb +93 -0
  65. data/spec/dragonfly/extended_temp_object_spec.rb +67 -0
  66. data/spec/dragonfly/middleware_spec.rb +44 -0
  67. data/spec/dragonfly/parameters_spec.rb +293 -0
  68. data/spec/dragonfly/processing/rmagick_processor_spec.rb +148 -0
  69. data/spec/dragonfly/temp_object_spec.rb +233 -0
  70. data/spec/dragonfly/url_handler_spec.rb +246 -0
  71. data/spec/dragonfly_spec.rb +4 -0
  72. data/spec/image_matchers.rb +31 -0
  73. data/spec/simple_matchers.rb +14 -0
  74. data/spec/spec_helper.rb +19 -0
  75. metadata +160 -0
@@ -0,0 +1,94 @@
1
+ module Dragonfly
2
+ class ExtendedTempObject < TempObject
3
+
4
+ # Exceptions
5
+ class NotConfiguredError < RuntimeError; end
6
+
7
+ class << self
8
+ attr_accessor :app
9
+ end
10
+
11
+ def process(processing_method, *args)
12
+ self.class.new(processor.send(processing_method, self, *args))
13
+ end
14
+
15
+ def process!(processing_method, *args)
16
+ modify_self!(processor.send(processing_method, self, *args))
17
+ end
18
+
19
+ def encode(*args)
20
+ self.class.new(encoder.encode(self, *args))
21
+ end
22
+
23
+ def encode!(*args)
24
+ modify_self!(encoder.encode(self, *args))
25
+ end
26
+
27
+ def transform(*args)
28
+ args.any? ? dup.transform!(*args) : self
29
+ end
30
+
31
+ def transform!(*args)
32
+ parameters = parameters_class.from_args(*args)
33
+ process!(parameters.processing_method, parameters.processing_options) unless parameters.processing_method.nil?
34
+ encode!(parameters.format, parameters.encoding) unless parameters.format.nil?
35
+ self
36
+ end
37
+
38
+ # Modify methods, public_methods and respond_to?, because method_missing
39
+ # allows methods from the analyser
40
+
41
+ def methods(*args)
42
+ (super + analyser.analysis_methods).uniq
43
+ end
44
+
45
+ def public_methods(*args)
46
+ (super + analyser.analysis_methods).uniq
47
+ end
48
+
49
+ def respond_to?(method)
50
+ super || analyser.has_analysis_method?(method)
51
+ end
52
+
53
+ private
54
+
55
+ def method_missing(method, *args, &block)
56
+ if analyser.has_analysis_method?(method)
57
+ # Define the method so we don't use method_missing next time
58
+ instance_var = "@#{method}"
59
+ self.class.class_eval do
60
+ define_method method do
61
+ # Lazy reader, like
62
+ # @width ||= analyser.width(self)
63
+ instance_variable_set(instance_var, instance_variable_get(instance_var) || analyser.send(method, self))
64
+ end
65
+ end
66
+ # Now that it's defined (for next time)
67
+ send(method)
68
+ else
69
+ super
70
+ end
71
+ end
72
+
73
+ def app
74
+ self.class.app ? self.class.app : raise(NotConfiguredError, "#{self.class} has no app set")
75
+ end
76
+
77
+ def analyser
78
+ app.analyser
79
+ end
80
+
81
+ def processor
82
+ app.processor
83
+ end
84
+
85
+ def encoder
86
+ app.encoder
87
+ end
88
+
89
+ def parameters_class
90
+ app.parameters_class
91
+ end
92
+
93
+ end
94
+ end
@@ -0,0 +1,27 @@
1
+ module Dragonfly
2
+
3
+ class Middleware
4
+
5
+ def initialize(app, dragonfly_app_name)
6
+ @app = app
7
+ @dragonfly_app_name = dragonfly_app_name
8
+ end
9
+
10
+ def call(env)
11
+ response = dragonfly_app.call(env)
12
+ if response[0] == 404
13
+ @app.call(env)
14
+ else
15
+ response
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def dragonfly_app
22
+ App[@dragonfly_app_name]
23
+ end
24
+
25
+ end
26
+
27
+ end
@@ -0,0 +1,152 @@
1
+ require 'rack'
2
+
3
+ module Dragonfly
4
+ class Parameters
5
+
6
+ # Exceptions
7
+ class InvalidParameters < RuntimeError; end
8
+ class InvalidShortcut < RuntimeError; end
9
+
10
+ # Class methods
11
+ class << self
12
+
13
+ include Configurable
14
+
15
+ configurable_attr :default_processing_method
16
+ configurable_attr :default_processing_options, {}
17
+ configurable_attr :default_format
18
+ configurable_attr :default_encoding, {}
19
+
20
+ def add_shortcut(*args, &block)
21
+ if block
22
+ block_shortcuts_of_length(args.length) << [args, block]
23
+ else
24
+ shortcut_name, attributes = args
25
+ simple_shortcuts[shortcut_name] = attributes
26
+ end
27
+ end
28
+ configuration_method :add_shortcut
29
+
30
+ def from_shortcut(*args)
31
+ if attributes = matching_simple_shortcut(args)
32
+ new(attributes)
33
+ elsif attributes = matching_block_shortcut(args)
34
+ new(attributes)
35
+ else
36
+ raise InvalidShortcut, "No shortcut was found matching (#{args.map{|a| a.inspect }.join(', ')})"
37
+ end
38
+ end
39
+
40
+ def from_args(*args)
41
+ if args.empty? then new
42
+ elsif args.length == 1 && args.first.is_a?(Hash) then new(args.first)
43
+ elsif args.length == 1 && args.first.is_a?(Parameters) then args.first.dup
44
+ else from_shortcut(*args)
45
+ end
46
+ end
47
+
48
+ private
49
+
50
+ def simple_shortcuts
51
+ @simple_shortcuts ||= {}
52
+ end
53
+
54
+ # block_shortcuts is actually a hash (keyed on the number of
55
+ # arguments) of arrays (of argument lists)
56
+ def block_shortcuts
57
+ @block_shortcuts ||= {}
58
+ end
59
+
60
+ def block_shortcuts_of_length(arg_length)
61
+ block_shortcuts[arg_length] ||= []
62
+ end
63
+
64
+ def matching_simple_shortcut(args)
65
+ if args.length == 1 && args.first.is_a?(Symbol)
66
+ simple_shortcuts[args.first]
67
+ end
68
+ end
69
+
70
+ def matching_block_shortcut(args)
71
+ block_shortcuts_of_length(args.length).each do |(args_to_match, block)|
72
+ if all_args_match?(args, args_to_match)
73
+ # If the block shortcut arg is a single regexp, then also yield the match data
74
+ if args_to_match.length == 1 && args_to_match.first.is_a?(Regexp)
75
+ match_data = args_to_match.first.match(args.first)
76
+ return block.call(args.first, match_data)
77
+ # ...otherwise just yield the args
78
+ else
79
+ return block.call(*args)
80
+ end
81
+ end
82
+ end
83
+ nil
84
+ end
85
+
86
+ def all_args_match?(args, args_to_match)
87
+ (0...args.length).inject(true){|current_result, i| current_result &&= args_to_match[i] === args[i] }
88
+ end
89
+
90
+ end
91
+
92
+ # Instance methods
93
+
94
+ attr_accessor :uid, :processing_method, :processing_options, :format, :encoding
95
+
96
+ def initialize(attributes={})
97
+ attributes = attributes.dup
98
+ %w(processing_method processing_options format encoding).each do |attribute|
99
+ instance_variable_set "@#{attribute}", (attributes.delete(attribute.to_sym) || self.class.send("default_#{attribute}"))
100
+ end
101
+ @uid = attributes.delete(:uid)
102
+ raise ArgumentError, "Parameters doesn't recognise the following parameters: #{attributes.keys.join(', ')}" if attributes.any?
103
+ end
104
+
105
+ def [](attribute)
106
+ send(attribute)
107
+ end
108
+
109
+ def []=(attribute, value)
110
+ send("#{attribute}=", value)
111
+ end
112
+
113
+ def ==(other_parameters)
114
+ self.to_hash == other_parameters.to_hash
115
+ end
116
+
117
+ def generate_sha(salt, sha_length)
118
+ Digest::SHA1.hexdigest("#{to_sorted_array}#{salt}")[0...sha_length]
119
+ end
120
+
121
+ def unique_signature
122
+ generate_sha('I like cheese', 10)
123
+ end
124
+
125
+ def to_hash
126
+ {
127
+ :uid => uid,
128
+ :processing_method => processing_method,
129
+ :processing_options => processing_options,
130
+ :format => format,
131
+ :encoding => encoding
132
+ }
133
+ end
134
+
135
+ def validate!
136
+ raise InvalidParameters, "Parameters requires that at least the uid and the format are set" if uid.nil? || format.nil?
137
+ end
138
+
139
+ private
140
+
141
+ def to_sorted_array
142
+ [
143
+ uid,
144
+ format,
145
+ processing_method,
146
+ processing_options.sort{|a,b| a[1].to_s <=> b[1].to_s },
147
+ encoding.sort{|a,b| a[1].to_s <=> b[1].to_s }
148
+ ]
149
+ end
150
+
151
+ end
152
+ end
@@ -0,0 +1,14 @@
1
+ module Dragonfly
2
+ module Processing
3
+ class Processor
4
+
5
+ include Configurable
6
+
7
+ def register(mod)
8
+ self.extend(mod)
9
+ end
10
+ configuration_method :register
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,71 @@
1
+ require 'rmagick'
2
+
3
+ module Dragonfly
4
+ module Processing
5
+
6
+ module RMagickProcessor
7
+
8
+ GRAVITY_MAPPINGS = {
9
+ 'nw' => Magick::NorthWestGravity,
10
+ 'n' => Magick::NorthGravity,
11
+ 'ne' => Magick::NorthEastGravity,
12
+ 'w' => Magick::WestGravity,
13
+ 'c' => Magick::CenterGravity,
14
+ 'e' => Magick::EastGravity,
15
+ 'sw' => Magick::SouthWestGravity,
16
+ 's' => Magick::SouthGravity,
17
+ 'se' => Magick::SouthEastGravity
18
+ }
19
+
20
+ def crop(temp_object, opts={})
21
+ x = opts[:x].to_i
22
+ y = opts[:y].to_i
23
+ gravity = GRAVITY_MAPPINGS[opts[:gravity]] || Magick::ForgetGravity
24
+ width = opts[:width].to_i
25
+ height = opts[:height].to_i
26
+
27
+ image = rmagick_image(temp_object)
28
+
29
+ # RMagick throws an error if the cropping area is bigger than the image,
30
+ # when the gravity is something other than nw
31
+ width = image.columns - x if x + width > image.columns
32
+ height = image.rows - y if y + height > image.rows
33
+
34
+ image.crop(gravity, x, y, width, height).to_blob
35
+ end
36
+
37
+ def resize(temp_object, opts={})
38
+ rmagick_image(temp_object).change_geometry!(opts[:geometry]) do |cols, rows, img|
39
+ img.resize!(cols, rows)
40
+ end.to_blob
41
+ end
42
+
43
+ def resize_and_crop(temp_object, opts={})
44
+ image = rmagick_image(temp_object)
45
+
46
+ width = opts[:width] ? opts[:width].to_i : image.columns
47
+ height = opts[:height] ? opts[:height].to_i : image.rows
48
+ gravity = GRAVITY_MAPPINGS[opts[:gravity]] || Magick::CenterGravity
49
+
50
+ image.resize_to_fill(width, height, gravity).to_blob
51
+ end
52
+
53
+ def vignette(temp_object, opts={})
54
+ x = opts[:x].to_f || temp_object.width * 0.1
55
+ y = opts[:y].to_f || temp_object.height * 0.1
56
+ radius = opts[:radius].to_f || 0.0
57
+ sigma = opts[:sigma].to_f || 10.0
58
+
59
+ rmagick_image(temp_object).vignette(x, y, radius, sigma).to_blob
60
+ end
61
+
62
+ private
63
+
64
+ def rmagick_image(temp_object)
65
+ Magick::Image.from_blob(temp_object.data).first
66
+ end
67
+
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,47 @@
1
+ module Dragonfly
2
+
3
+ RMagickConfiguration = AppConfiguration.new
4
+
5
+ def RMagickConfiguration.apply_configuration(app)
6
+ app.configure do |c|
7
+ c.analyser do |a|
8
+ a.register(Analysis::RMagickAnalyser.new)
9
+ end
10
+ c.processor do |p|
11
+ p.register(Processing::RMagickProcessor)
12
+ end
13
+ c.encoder = Encoding::RMagickEncoder.new
14
+ c.parameters do |p|
15
+ p.default_format = :jpg
16
+ # Standard resizing like '30x40!', etc.
17
+ p.add_shortcut(/^\d*x\d*[><%^!]?$|^\d+@$/) do |geometry, match_data|
18
+ {
19
+ :processing_method => :resize,
20
+ :processing_options => {:geometry => geometry}
21
+ }
22
+ end
23
+ # Cropped resizing like '20x50#ne'
24
+ p.add_shortcut(/^(\d+)x(\d+)#(\w{1,2})?/) do |geometry, match_data|
25
+ {
26
+ :processing_method => :resize_and_crop,
27
+ :processing_options => {:width => match_data[1], :height => match_data[2], :gravity => match_data[3]}
28
+ }
29
+ end
30
+ # Cropping like '30x30+10+10ne'
31
+ p.add_shortcut(/^(\d+)x(\d+)([+-]\d+)([+-]\d+)(\w{1,2})?/) do |geometry, match_data|
32
+ {
33
+ :processing_method => :crop,
34
+ :processing_options => {
35
+ :width => match_data[1],
36
+ :height => match_data[2],
37
+ :x => match_data[3],
38
+ :y => match_data[4],
39
+ :gravity => match_data[5]
40
+ }
41
+ }
42
+ end
43
+ end
44
+ end
45
+ end
46
+
47
+ end
@@ -0,0 +1,20 @@
1
+ require 'dragonfly'
2
+
3
+ ### The dragonfly app ###
4
+
5
+ app = Dragonfly::App[:images]
6
+ app.configure_with(Dragonfly::RMagickConfiguration)
7
+ app.configure do |c|
8
+ c.log = RAILS_DEFAULT_LOGGER
9
+ c.datastore.configure do |d|
10
+ d.root_path = "#{Rails.root}/public/system/dragonfly/#{Rails.env}"
11
+ end
12
+ c.url_handler do |u|
13
+ u.protect_from_dos_attacks = false
14
+ u.path_prefix = '/images'
15
+ end
16
+ end
17
+
18
+ ### Extend active record ###
19
+ ActiveRecord::Base.extend Dragonfly::ActiveRecordExtensions
20
+ ActiveRecord::Base.register_dragonfly_app(:image, app)
@@ -0,0 +1,118 @@
1
+ require 'tempfile'
2
+
3
+ module Dragonfly
4
+ class TempObject
5
+
6
+ # Class configuration
7
+ class << self
8
+
9
+ include Configurable
10
+ configurable_attr :block_size, 8192
11
+
12
+ end
13
+
14
+ # Instance Methods
15
+
16
+ def initialize(obj)
17
+ initialize_from_object!(obj)
18
+ end
19
+
20
+ attr_accessor :name
21
+
22
+ def modify_self!(obj)
23
+ reset!
24
+ initialize_from_object!(obj)
25
+ self
26
+ end
27
+
28
+ def data
29
+ @data ||= initialized_data || file.open.read
30
+ end
31
+
32
+ def tempfile
33
+ if @tempfile
34
+ @tempfile
35
+ elsif initialized_tempfile
36
+ @tempfile = initialized_tempfile
37
+ elsif initialized_data
38
+ tempfile = Tempfile.new('dragonfly')
39
+ tempfile.write(initialized_data)
40
+ tempfile.close
41
+ @tempfile = tempfile
42
+ elsif initialized_file
43
+ @tempfile = copy_to_tempfile(initialized_file)
44
+ end
45
+ end
46
+
47
+ alias_method :file, :tempfile
48
+
49
+ def path
50
+ tempfile.path
51
+ end
52
+
53
+ def size
54
+ if initialized_data
55
+ initialized_data.size
56
+ else
57
+ File.size(path)
58
+ end
59
+ end
60
+
61
+ def each(&block)
62
+ if initialized_data
63
+ string_io = StringIO.new(initialized_data)
64
+ while part = string_io.read(block_size)
65
+ yield part
66
+ end
67
+ else
68
+ tempfile.open
69
+ while part = tempfile.read(block_size)
70
+ yield part
71
+ end
72
+ tempfile.close
73
+ end
74
+ end
75
+
76
+ protected
77
+
78
+ attr_accessor :initialized_data, :initialized_tempfile, :initialized_file
79
+
80
+ private
81
+
82
+ def reset!
83
+ instance_variables.each do |var|
84
+ instance_variable_set(var, nil)
85
+ end
86
+ end
87
+
88
+ def initialize_from_object!(obj)
89
+ case obj
90
+ when TempObject
91
+ @initialized_data = obj.initialized_data
92
+ @initialized_tempfile = copy_to_tempfile(obj.initialized_tempfile) if obj.initialized_tempfile
93
+ @initialized_file = obj.initialized_file
94
+ when String
95
+ @initialized_data = obj
96
+ when Tempfile
97
+ @initialized_tempfile = obj
98
+ when File
99
+ @initialized_file = obj
100
+ else
101
+ raise ArgumentError, "#{self.class.name} must be initialized with a String, a File or a Tempfile"
102
+ end
103
+ self.name = obj.original_filename if obj.respond_to?(:original_filename)
104
+ end
105
+
106
+ def copy_to_tempfile(file)
107
+ tempfile = Tempfile.new('dragonfly')
108
+ tempfile.close
109
+ FileUtils.cp File.expand_path(file.path), tempfile.path
110
+ tempfile
111
+ end
112
+
113
+ def block_size
114
+ self.class.block_size
115
+ end
116
+
117
+ end
118
+ end
@@ -0,0 +1,148 @@
1
+ require 'digest/sha1'
2
+ require 'rack'
3
+
4
+ module Dragonfly
5
+ class UrlHandler
6
+
7
+ # Exceptions
8
+ class IncorrectSHA < RuntimeError; end
9
+ class SHANotGiven < RuntimeError; end
10
+ class UnknownUrl < RuntimeError; end
11
+
12
+ include Rack::Utils
13
+ include Configurable
14
+
15
+ MAPPINGS = {
16
+ :processing_method => 'm',
17
+ :processing_options => 'o',
18
+ :encoding => 'e',
19
+ :sha => 's'
20
+ }
21
+
22
+ configurable_attr :protect_from_dos_attacks, true
23
+ configurable_attr :secret, 'This is a secret!'
24
+ configurable_attr :sha_length, 16
25
+ configurable_attr :path_prefix, ''
26
+
27
+ def initialize(parameters_class = Parameters)
28
+ @parameters_class = parameters_class
29
+ end
30
+
31
+ def url_for(uid, *args)
32
+ parameters = parameters_class.from_args(*args)
33
+ parameters.uid = uid
34
+ parameters_to_url(parameters)
35
+ end
36
+
37
+ def url_to_parameters(path, query_string)
38
+ validate_format!(path)
39
+ path = remove_path_prefix(path)
40
+ query = parse_nested_query(query_string)
41
+ attributes = {
42
+ :uid => extract_uid(path, query),
43
+ :processing_method => extract_processing_method(path, query),
44
+ :processing_options => extract_processing_options(path, query),
45
+ :format => extract_format(path, query),
46
+ :encoding => extract_encoding(path, query)
47
+ }.reject{|k,v| v.nil? }
48
+ parameters = parameters_class.new(attributes)
49
+ validate_parameters(parameters, query)
50
+ parameters
51
+ end
52
+
53
+ def parameters_to_url(parameters)
54
+ parameters.validate!
55
+ query_string = [:processing_method, :processing_options, :encoding].map do |attribute|
56
+ build_query(MAPPINGS[attribute] => parameters[attribute]) unless parameters[attribute].blank?
57
+ end.compact.join('&')
58
+ sha_string = "&#{MAPPINGS[:sha]}=#{sha_from_parameters(parameters)}" if protect_from_dos_attacks?
59
+ url = "#{path_prefix}/#{escape_except_for_slashes(parameters.uid)}.#{parameters.format}?#{query_string}#{sha_string}"
60
+ url.sub!(/\?$/,'')
61
+ url
62
+ end
63
+
64
+ private
65
+
66
+ def remove_path_prefix(path)
67
+ path.sub(path_prefix, '')
68
+ end
69
+
70
+ def extract_uid(path, query)
71
+ path.sub(/^\//,'').sub(/\.[^.]+$/, '')
72
+ end
73
+
74
+ def extract_processing_method(path, query)
75
+ query[MAPPINGS[:processing_method]]
76
+ end
77
+
78
+ def extract_processing_options(path, query)
79
+ processing_options = query[MAPPINGS[:processing_options]]
80
+ symbolize_keys(processing_options) if processing_options
81
+ end
82
+
83
+ def extract_format(path, query)
84
+ path.sub(/^\//,'').split('.').last
85
+ end
86
+
87
+ def extract_encoding(path, query)
88
+ encoding = query[MAPPINGS[:encoding]]
89
+ symbolize_keys(encoding) if encoding
90
+ end
91
+
92
+ attr_reader :parameters_class
93
+
94
+ def symbolize_keys(hash)
95
+ hash = hash.dup
96
+ hash.each do |key, value|
97
+ hash[key.to_sym] = hash.delete(key)
98
+ end
99
+ hash
100
+ end
101
+
102
+ def validate_parameters(parameters, query)
103
+ if protect_from_dos_attacks?
104
+ sha = query[MAPPINGS[:sha]]
105
+ raise SHANotGiven, "You need to give a SHA" if sha.nil?
106
+ raise IncorrectSHA, "The SHA parameter you gave is incorrect" if sha_from_parameters(parameters) != sha
107
+ end
108
+ end
109
+
110
+ def protect_from_dos_attacks?
111
+ protect_from_dos_attacks
112
+ end
113
+
114
+ def sha_from_parameters(parameters)
115
+ parameters.generate_sha(secret, sha_length)
116
+ end
117
+
118
+
119
+ # Annoyingly, the 'build_query' in Rack::Utils doesn't seem to work
120
+ # properly for nested parameters/arrays
121
+ # Taken from http://github.com/sinatra/sinatra/commit/52658061d1205753a8afd2801845a910a6c01ffd
122
+ def build_query(value, prefix = nil)
123
+ case value
124
+ when Array
125
+ value.map { |v|
126
+ build_query(v, "#{prefix}[]")
127
+ } * "&"
128
+ when Hash
129
+ value.map { |k, v|
130
+ build_query(v, prefix ? "#{prefix}[#{escape(k)}]" : escape(k))
131
+ } * "&"
132
+ else
133
+ "#{prefix}=#{escape(value)}"
134
+ end
135
+ end
136
+
137
+ def escape_except_for_slashes(string)
138
+ string.split('/').map{|s| escape(s) }.join('/')
139
+ end
140
+
141
+ def validate_format!(path)
142
+ if path !~ /^#{path_prefix}/ || path !~ /^.*[^\/].*\..*[^\/].*$/
143
+ raise UnknownUrl, "path '#{path}' not found"
144
+ end
145
+ end
146
+
147
+ end
148
+ end