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.
- data/.gitignore +7 -0
- data/LICENSE +20 -0
- data/README.markdown +95 -0
- data/Rakefile +60 -0
- data/VERSION +1 -0
- data/config.rb +7 -0
- data/config.ru +10 -0
- data/dragonfly-rails.gemspec +41 -0
- data/dragonfly.gemspec +137 -0
- data/features/dragonfly.feature +38 -0
- data/features/steps/common_steps.rb +8 -0
- data/features/steps/dragonfly_steps.rb +39 -0
- data/features/support/env.rb +25 -0
- data/features/support/image_helpers.rb +9 -0
- data/generators/dragonfly_app/USAGE +16 -0
- data/generators/dragonfly_app/dragonfly_app_generator.rb +54 -0
- data/generators/dragonfly_app/templates/custom_processing.erb +13 -0
- data/generators/dragonfly_app/templates/initializer.erb +7 -0
- data/generators/dragonfly_app/templates/metal_file.erb +32 -0
- data/irbrc.rb +20 -0
- data/lib/dragonfly/active_record_extensions/attachment.rb +117 -0
- data/lib/dragonfly/active_record_extensions/class_methods.rb +41 -0
- data/lib/dragonfly/active_record_extensions/instance_methods.rb +28 -0
- data/lib/dragonfly/active_record_extensions/validations.rb +19 -0
- data/lib/dragonfly/active_record_extensions.rb +12 -0
- data/lib/dragonfly/analysis/analyser.rb +45 -0
- data/lib/dragonfly/analysis/base.rb +10 -0
- data/lib/dragonfly/analysis/r_magick_analyser.rb +40 -0
- data/lib/dragonfly/app.rb +85 -0
- data/lib/dragonfly/app_configuration.rb +9 -0
- data/lib/dragonfly/configurable.rb +113 -0
- data/lib/dragonfly/core_ext/object.rb +8 -0
- data/lib/dragonfly/data_storage/base.rb +19 -0
- data/lib/dragonfly/data_storage/file_data_store.rb +72 -0
- data/lib/dragonfly/data_storage.rb +9 -0
- data/lib/dragonfly/encoding/base.rb +13 -0
- data/lib/dragonfly/encoding/r_magick_encoder.rb +17 -0
- data/lib/dragonfly/extended_temp_object.rb +94 -0
- data/lib/dragonfly/middleware.rb +27 -0
- data/lib/dragonfly/parameters.rb +152 -0
- data/lib/dragonfly/processing/processor.rb +14 -0
- data/lib/dragonfly/processing/r_magick_processor.rb +71 -0
- data/lib/dragonfly/r_magick_configuration.rb +47 -0
- data/lib/dragonfly/rails/images.rb +20 -0
- data/lib/dragonfly/temp_object.rb +118 -0
- data/lib/dragonfly/url_handler.rb +148 -0
- data/lib/dragonfly.rb +33 -0
- data/samples/beach.png +0 -0
- data/samples/egg.png +0 -0
- data/samples/round.gif +0 -0
- data/samples/taj.jpg +0 -0
- data/spec/argument_matchers.rb +29 -0
- data/spec/dragonfly/active_record_extensions/attachment_spec.rb +8 -0
- data/spec/dragonfly/active_record_extensions/initializer.rb +1 -0
- data/spec/dragonfly/active_record_extensions/migration.rb +21 -0
- data/spec/dragonfly/active_record_extensions/model_spec.rb +400 -0
- data/spec/dragonfly/active_record_extensions/models.rb +2 -0
- data/spec/dragonfly/active_record_extensions/spec_helper.rb +23 -0
- data/spec/dragonfly/analysis/analyser_spec.rb +85 -0
- data/spec/dragonfly/analysis/r_magick_analyser_spec.rb +35 -0
- data/spec/dragonfly/app_spec.rb +69 -0
- data/spec/dragonfly/configurable_spec.rb +193 -0
- data/spec/dragonfly/data_storage/data_store_spec.rb +47 -0
- data/spec/dragonfly/data_storage/file_data_store_spec.rb +93 -0
- data/spec/dragonfly/extended_temp_object_spec.rb +67 -0
- data/spec/dragonfly/middleware_spec.rb +44 -0
- data/spec/dragonfly/parameters_spec.rb +293 -0
- data/spec/dragonfly/processing/rmagick_processor_spec.rb +148 -0
- data/spec/dragonfly/temp_object_spec.rb +233 -0
- data/spec/dragonfly/url_handler_spec.rb +246 -0
- data/spec/dragonfly_spec.rb +4 -0
- data/spec/image_matchers.rb +31 -0
- data/spec/simple_matchers.rb +14 -0
- data/spec/spec_helper.rb +19 -0
- 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,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
|