dragonfly 0.9.8 → 0.9.9
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/Gemfile +1 -1
- data/History.md +32 -0
- data/README.md +54 -32
- data/VERSION +1 -1
- data/dragonfly.gemspec +23 -21
- data/extra_docs/Configuration.md +0 -8
- data/extra_docs/DataStorage.md +13 -5
- data/extra_docs/ExampleUseCases.md +4 -1
- data/extra_docs/Heroku.md +14 -6
- data/extra_docs/Models.md +5 -1
- data/extra_docs/Rails3.md +1 -1
- data/extra_docs/URLs.md +8 -1
- data/lib/dragonfly.rb +7 -8
- data/lib/dragonfly/active_model_extensions/attachment.rb +37 -17
- data/lib/dragonfly/active_model_extensions/attachment_class_methods.rb +1 -1
- data/lib/dragonfly/active_model_extensions/validations.rb +53 -26
- data/lib/dragonfly/analyser.rb +1 -1
- data/lib/dragonfly/app.rb +1 -1
- data/lib/dragonfly/config/heroku.rb +7 -0
- data/lib/dragonfly/configurable.rb +19 -20
- data/lib/dragonfly/data_storage/couch_data_store.rb +2 -3
- data/lib/dragonfly/data_storage/file_data_store.rb +2 -9
- data/lib/dragonfly/data_storage/mongo_data_store.rb +1 -1
- data/lib/dragonfly/data_storage/s3data_store.rb +17 -10
- data/lib/dragonfly/function_manager.rb +4 -8
- data/lib/dragonfly/has_filename.rb +24 -0
- data/lib/dragonfly/image_magick/analyser.rb +1 -1
- data/lib/dragonfly/image_magick/generator.rb +1 -1
- data/lib/dragonfly/image_magick/processor.rb +5 -0
- data/lib/dragonfly/image_magick/utils.rb +1 -2
- data/lib/dragonfly/job.rb +52 -71
- data/lib/dragonfly/railtie.rb +1 -1
- data/lib/dragonfly/temp_object.rb +32 -14
- data/lib/dragonfly/url_attributes.rb +30 -0
- data/lib/dragonfly/url_mapper.rb +2 -2
- data/samples/DSC02119.JPG +0 -0
- data/samples/a.jp2 +0 -0
- data/samples/beach.jpg +0 -0
- data/spec/dragonfly/active_model_extensions/model_spec.rb +63 -40
- data/spec/dragonfly/active_model_extensions/spec_helper.rb +4 -0
- data/spec/dragonfly/app_spec.rb +11 -3
- data/spec/dragonfly/configurable_spec.rb +38 -20
- data/spec/dragonfly/data_storage/file_data_store_spec.rb +24 -36
- data/spec/dragonfly/data_storage/s3_data_store_spec.rb +21 -5
- data/spec/dragonfly/data_storage/shared_data_store_examples.rb +2 -2
- data/spec/dragonfly/has_filename_spec.rb +88 -0
- data/spec/dragonfly/image_magick/analyser_spec.rb +10 -0
- data/spec/dragonfly/image_magick/processor_spec.rb +11 -0
- data/spec/dragonfly/job_spec.rb +173 -167
- data/spec/dragonfly/temp_object_spec.rb +82 -10
- data/spec/dragonfly/url_attributes.rb +47 -0
- data/spec/dragonfly/url_mapper_spec.rb +9 -1
- data/spec/functional/model_urls_spec.rb +36 -1
- metadata +179 -250
- data/lib/dragonfly/core_ext/string.rb +0 -9
- data/lib/dragonfly/core_ext/symbol.rb +0 -9
- data/spec/dragonfly/core_ext/string_spec.rb +0 -17
- data/spec/dragonfly/core_ext/symbol_spec.rb +0 -17
data/extra_docs/Rails3.md
CHANGED
data/extra_docs/URLs.md
CHANGED
@@ -87,11 +87,16 @@ Downloaded filename
|
|
87
87
|
To specify the filename the browser uses for 'Save As' dialogues:
|
88
88
|
|
89
89
|
app.content_filename = proc{|job, request|
|
90
|
-
|
90
|
+
if job.process_steps.any?
|
91
|
+
"#{job.basename}_#{job.process_steps.first.name}.#{job.format}"
|
92
|
+
else
|
93
|
+
"#{job.basename}.#{job.format}"
|
94
|
+
end
|
91
95
|
}
|
92
96
|
|
93
97
|
This will for example give the following filenames for the following jobs:
|
94
98
|
|
99
|
+
app.fetch('some/tree.png') # -> 'tree.png'
|
95
100
|
app.fetch('some/tree.png').process(:greyscale) # -> 'tree_greyscale.png'
|
96
101
|
app.fetch('some/tree.png').process(:greyscale).gif # -> 'tree_greyscale.gif'
|
97
102
|
|
@@ -141,6 +146,8 @@ Rails 3 (routes.rb):
|
|
141
146
|
|
142
147
|
In each case the url will need to be generated by the router of choice, or manually.
|
143
148
|
|
149
|
+
NOTE: Ruby treats curly braces slightly differently to `do`...`end` so be aware of this when using the above examples.
|
150
|
+
|
144
151
|
Simple Endpoints
|
145
152
|
----------------
|
146
153
|
{Dragonfly::Job Job} objects can also be turned straight into Rack endpoints using `to_app`, e.g. in Rails 3:
|
data/lib/dragonfly.rb
CHANGED
@@ -3,7 +3,7 @@
|
|
3
3
|
# The convention is that dirs are modules
|
4
4
|
# so declare them here and autoload any modules/classes inside them
|
5
5
|
# All paths here are absolute
|
6
|
-
|
6
|
+
camelize = proc do |path|
|
7
7
|
# e.g. 'test/this_one' => Test::ThisOne
|
8
8
|
"#{path}".
|
9
9
|
chomp('/').
|
@@ -12,27 +12,26 @@ def camelize(path)
|
|
12
12
|
gsub('_','').
|
13
13
|
sub(/^(\w)/){ $1.upcase }
|
14
14
|
end
|
15
|
-
|
15
|
+
|
16
|
+
autoload_files_in_dir = proc do |path, namespace|
|
16
17
|
# Define the module
|
17
18
|
eval("module #{namespace}; end")
|
18
19
|
# Autoload modules/classes in that module
|
19
20
|
Dir.glob("#{path}/*.rb").each do |file|
|
20
21
|
file = File.expand_path(file)
|
21
|
-
sub_const_name = camelize
|
22
|
+
sub_const_name = camelize[ File.basename(file, '.rb') ]
|
22
23
|
eval("#{namespace}.autoload('#{sub_const_name}', '#{file}')")
|
23
24
|
end
|
24
25
|
# Recurse on subdirectories
|
25
26
|
Dir.glob("#{path}/*/").each do |dir|
|
26
|
-
sub_namespace = camelize
|
27
|
-
autoload_files_in_dir
|
27
|
+
sub_namespace = camelize[ File.basename(dir) ]
|
28
|
+
autoload_files_in_dir[dir, "#{namespace}::#{sub_namespace}"]
|
28
29
|
end
|
29
30
|
end
|
30
31
|
|
31
|
-
autoload_files_in_dir
|
32
|
+
autoload_files_in_dir["#{File.dirname(__FILE__)}/dragonfly", 'Dragonfly']
|
32
33
|
|
33
34
|
require 'dragonfly/core_ext/object'
|
34
|
-
require 'dragonfly/core_ext/string'
|
35
|
-
require 'dragonfly/core_ext/symbol'
|
36
35
|
require 'dragonfly/core_ext/array'
|
37
36
|
require 'dragonfly/core_ext/hash'
|
38
37
|
|
@@ -14,15 +14,17 @@ module Dragonfly
|
|
14
14
|
:data, :to_file, :file, :tempfile, :path,
|
15
15
|
:process, :encode, :analyse,
|
16
16
|
:meta, :meta=,
|
17
|
-
:name, :
|
17
|
+
:name, :size,
|
18
18
|
:url
|
19
19
|
|
20
|
+
include HasFilename
|
21
|
+
|
20
22
|
alias_method :length, :size
|
21
23
|
|
22
24
|
def initialize(model)
|
23
25
|
@model = model
|
24
26
|
self.uid = model_uid
|
25
|
-
|
27
|
+
set_job_from_uid if uid
|
26
28
|
@should_run_callbacks = true
|
27
29
|
self.class.ensure_uses_cached_magic_attributes
|
28
30
|
end
|
@@ -50,10 +52,11 @@ module Dragonfly
|
|
50
52
|
else app.new_job(value)
|
51
53
|
end
|
52
54
|
set_magic_attributes
|
53
|
-
|
55
|
+
job.url_attrs = all_extra_attributes
|
54
56
|
self.class.run_callbacks(:after_assign, model, self) if should_run_callbacks?
|
55
57
|
retain! if should_retain?
|
56
58
|
end
|
59
|
+
model_uid_will_change!
|
57
60
|
value
|
58
61
|
end
|
59
62
|
|
@@ -79,9 +82,8 @@ module Dragonfly
|
|
79
82
|
end
|
80
83
|
|
81
84
|
def name=(name)
|
82
|
-
job.name = name
|
83
85
|
set_magic_attribute(:name, name) if has_magic_attribute_for?(:name)
|
84
|
-
name
|
86
|
+
job.name = name
|
85
87
|
end
|
86
88
|
|
87
89
|
def process!(*args)
|
@@ -150,7 +152,7 @@ module Dragonfly
|
|
150
152
|
model.send("#{attribute}_#{key}=", value)
|
151
153
|
end
|
152
154
|
sync_with_model
|
153
|
-
|
155
|
+
set_job_from_uid
|
154
156
|
self.retained = true
|
155
157
|
end
|
156
158
|
end
|
@@ -172,6 +174,7 @@ module Dragonfly
|
|
172
174
|
end
|
173
175
|
|
174
176
|
def store_job!
|
177
|
+
meta.merge!(all_extra_attributes)
|
175
178
|
opts = self.class.evaluate_storage_opts(model, self)
|
176
179
|
set_uid_and_model_uid job.store(opts)
|
177
180
|
self.job = job.to_fetched_job(uid)
|
@@ -210,6 +213,11 @@ module Dragonfly
|
|
210
213
|
model.send("#{attribute}_uid")
|
211
214
|
end
|
212
215
|
|
216
|
+
def model_uid_will_change!
|
217
|
+
meth = "#{attribute}_uid_will_change!"
|
218
|
+
model.send(meth) if model.respond_to?(meth)
|
219
|
+
end
|
220
|
+
|
213
221
|
attr_reader :model, :uid
|
214
222
|
attr_writer :job
|
215
223
|
attr_accessor :previous_uid
|
@@ -219,17 +227,6 @@ module Dragonfly
|
|
219
227
|
@uid = uid
|
220
228
|
end
|
221
229
|
|
222
|
-
def update_meta
|
223
|
-
magic_attributes.each{|property| meta[property] = model.send("#{attribute}_#{property}") }
|
224
|
-
meta[:model_class] = model.class.name
|
225
|
-
meta[:model_attachment] = attribute
|
226
|
-
end
|
227
|
-
|
228
|
-
def update_from_uid
|
229
|
-
self.job = app.fetch(uid)
|
230
|
-
update_meta
|
231
|
-
end
|
232
|
-
|
233
230
|
def magic_attributes
|
234
231
|
self.class.magic_attributes
|
235
232
|
end
|
@@ -254,6 +251,29 @@ module Dragonfly
|
|
254
251
|
model.send("#{attribute}_#{property}")
|
255
252
|
end
|
256
253
|
|
254
|
+
def magic_attributes_hash
|
255
|
+
magic_attributes.inject({}) do |attrs, property|
|
256
|
+
attrs[property] = model.send("#{attribute}_#{property}")
|
257
|
+
attrs
|
258
|
+
end
|
259
|
+
end
|
260
|
+
|
261
|
+
def extra_attributes
|
262
|
+
@extra_attributes ||= {
|
263
|
+
:model_class => model.class.name,
|
264
|
+
:model_attachment => attribute
|
265
|
+
}
|
266
|
+
end
|
267
|
+
|
268
|
+
def all_extra_attributes
|
269
|
+
magic_attributes_hash.merge(extra_attributes)
|
270
|
+
end
|
271
|
+
|
272
|
+
def set_job_from_uid
|
273
|
+
self.job = app.fetch(uid)
|
274
|
+
job.url_attrs = all_extra_attributes
|
275
|
+
end
|
276
|
+
|
257
277
|
end
|
258
278
|
end
|
259
279
|
end
|
@@ -2,38 +2,65 @@ module Dragonfly
|
|
2
2
|
module ActiveModelExtensions
|
3
3
|
module Validations
|
4
4
|
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
attrs = opts[:of] or raise ArgumentError, "you need to provide the attribute which has the property, using :of => <attribute_name>"
|
9
|
-
attrs = [attrs].flatten #(make sure it's an array)
|
10
|
-
|
11
|
-
raise ArgumentError, "you must provide either :in => [<value1>, <value2>..] or :as => <value>" unless opts[:in] || opts[:as]
|
12
|
-
allowed_values = opts[:in] || [opts[:as]]
|
13
|
-
|
14
|
-
args = attrs + [opts]
|
15
|
-
validates_each(*args) do |model, attr, attachment|
|
5
|
+
class PropertyValidator < ActiveModel::EachValidator
|
6
|
+
|
7
|
+
def validate_each(model, attribute, attachment)
|
16
8
|
if attachment
|
17
9
|
property = attachment.send(property_name)
|
18
|
-
unless
|
19
|
-
message = opts[:message] ||
|
20
|
-
"#{property_name.to_s.humanize.downcase} is incorrect. "+
|
21
|
-
"It needs to be #{Validations.expected_values_string(allowed_values)}"+
|
22
|
-
(property ? ", but was '#{property}'" : "")
|
23
|
-
message = message.call(property, model) if message.respond_to?(:call)
|
24
|
-
model.errors.add(attr, message)
|
25
|
-
end
|
10
|
+
model.errors.add(attribute, message(property, model)) unless matches?(property)
|
26
11
|
end
|
27
12
|
end
|
28
|
-
|
13
|
+
|
14
|
+
private
|
15
|
+
|
16
|
+
def matches?(property)
|
17
|
+
if case_insensitive?
|
18
|
+
prop = property.to_s.downcase
|
19
|
+
allowed_values.any?{|v| v.to_s.downcase == prop }
|
20
|
+
else
|
21
|
+
allowed_values.include?(property)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def message(property, model)
|
26
|
+
message = options[:message] ||
|
27
|
+
"#{property_name.to_s.humanize.downcase} is incorrect. " +
|
28
|
+
"It needs to be #{expected_values_string}" +
|
29
|
+
(property ? ", but was '#{property}'" : "")
|
30
|
+
message.respond_to?(:call) ? message.call(property, model) : message
|
31
|
+
end
|
32
|
+
|
33
|
+
def check_validity!
|
34
|
+
raise ArgumentError, "you must provide either :in => [<value1>, <value2>..] or :as => <value>" unless options[:in] || options[:as]
|
35
|
+
end
|
36
|
+
|
37
|
+
def property_name
|
38
|
+
options[:property_name]
|
39
|
+
end
|
40
|
+
|
41
|
+
def case_insensitive?
|
42
|
+
options[:case_sensitive] == false
|
43
|
+
end
|
44
|
+
|
45
|
+
def allowed_values
|
46
|
+
@allowed_values ||= options[:in] || [options[:as]]
|
47
|
+
end
|
48
|
+
|
49
|
+
def expected_values_string
|
50
|
+
if allowed_values.is_a?(Range)
|
51
|
+
"between #{allowed_values.first} and #{allowed_values.last}"
|
52
|
+
else
|
53
|
+
allowed_values.length > 1 ? "one of '#{allowed_values.join('\', \'')}'" : "'#{allowed_values.first.to_s}'"
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
29
57
|
end
|
30
58
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
end
|
59
|
+
private
|
60
|
+
|
61
|
+
def validates_property(property_name, options)
|
62
|
+
raise ArgumentError, "you need to provide the attribute which has the property, using :of => <attribute_name>" unless options[:of]
|
63
|
+
validates_with PropertyValidator, options.merge(:attributes => [*options[:of]], :property_name => property_name)
|
37
64
|
end
|
38
65
|
|
39
66
|
end
|
data/lib/dragonfly/analyser.rb
CHANGED
@@ -21,7 +21,7 @@ module Dragonfly
|
|
21
21
|
|
22
22
|
def analyse(temp_object, method, *args)
|
23
23
|
if enable_cache
|
24
|
-
key = [temp_object.
|
24
|
+
key = [temp_object.unique_id, method, *args]
|
25
25
|
cache[key] ||= call_last(method, temp_object, *args)
|
26
26
|
else
|
27
27
|
call_last(method, temp_object, *args)
|
data/lib/dragonfly/app.rb
CHANGED
@@ -86,7 +86,7 @@ module Dragonfly
|
|
86
86
|
end
|
87
87
|
|
88
88
|
def store(object, opts={})
|
89
|
-
temp_object = object.is_a?(TempObject) ? object : TempObject.new(object)
|
89
|
+
temp_object = object.is_a?(TempObject) ? object : TempObject.new(object, opts[:meta] || {})
|
90
90
|
datastore.store(temp_object, opts)
|
91
91
|
end
|
92
92
|
|
@@ -4,6 +4,13 @@ module Dragonfly
|
|
4
4
|
module Heroku
|
5
5
|
|
6
6
|
def self.apply_configuration(app, bucket_name)
|
7
|
+
app.log.warn("""HEROKU CONFIGURATION IS NOW DEPRECATED - you should configure with the S3 Datastore directly, e.g.
|
8
|
+
c.datastore = Dragonfly::DataStorage::S3DataStore.new(
|
9
|
+
:bucket_name => '#{bucket_name}',
|
10
|
+
:access_key_id => 'XXX',
|
11
|
+
:secret_access_key => 'XXX'
|
12
|
+
)
|
13
|
+
""")
|
7
14
|
app.configure do |c|
|
8
15
|
c.datastore = DataStorage::S3DataStore.new
|
9
16
|
c.datastore.configure do |d|
|
@@ -9,13 +9,6 @@ module Dragonfly
|
|
9
9
|
klass.class_eval do
|
10
10
|
include Configurable::InstanceMethods
|
11
11
|
extend Configurable::ClassMethods
|
12
|
-
|
13
|
-
# We should use configured_class rather than self.class
|
14
|
-
# because sometimes this will be the eigenclass of an object
|
15
|
-
# e.g. if we configure a module, etc.
|
16
|
-
define_method :configured_class do
|
17
|
-
klass
|
18
|
-
end
|
19
12
|
end
|
20
13
|
end
|
21
14
|
|
@@ -46,21 +39,17 @@ module Dragonfly
|
|
46
39
|
def has_config_method?(method_name)
|
47
40
|
config_methods.include?(method_name.to_sym)
|
48
41
|
end
|
49
|
-
|
50
|
-
def config_methods
|
51
|
-
@config_methods ||= configured_class.config_methods.dup
|
52
|
-
end
|
53
42
|
|
54
43
|
def configuration
|
55
44
|
@configuration ||= {}
|
56
45
|
end
|
46
|
+
|
47
|
+
def config_methods
|
48
|
+
@config_methods ||= self.class.config_methods.dup
|
49
|
+
end
|
57
50
|
|
58
51
|
def default_configuration
|
59
|
-
|
60
|
-
@default_configuration ||= [self.class, configured_class, *configured_class.ancestors].reverse.inject({}) do |default_config, klass|
|
61
|
-
default_config.merge!(klass.default_configuration) if klass.respond_to? :default_configuration
|
62
|
-
default_config
|
63
|
-
end
|
52
|
+
@default_configuration ||= self.class.default_configuration.dup
|
64
53
|
end
|
65
54
|
|
66
55
|
def set_config_value(key, value)
|
@@ -106,7 +95,7 @@ module Dragonfly
|
|
106
95
|
end
|
107
96
|
|
108
97
|
def saved_configs
|
109
|
-
|
98
|
+
self.class.saved_configs
|
110
99
|
end
|
111
100
|
|
112
101
|
def saved_config_for(symbol)
|
@@ -123,11 +112,17 @@ module Dragonfly
|
|
123
112
|
module ClassMethods
|
124
113
|
|
125
114
|
def default_configuration
|
126
|
-
@default_configuration ||= {}
|
115
|
+
@default_configuration ||= configurable_ancestors.reverse.inject({}) do |default_config, klass|
|
116
|
+
default_config.merge!(klass.default_configuration)
|
117
|
+
default_config
|
118
|
+
end
|
127
119
|
end
|
128
120
|
|
129
121
|
def config_methods
|
130
|
-
@config_methods ||= []
|
122
|
+
@config_methods ||= configurable_ancestors.inject([]) do |conf_methods, klass|
|
123
|
+
conf_methods |= klass.config_methods
|
124
|
+
conf_methods
|
125
|
+
end
|
131
126
|
end
|
132
127
|
|
133
128
|
def nested_configurables
|
@@ -142,6 +137,10 @@ module Dragonfly
|
|
142
137
|
@saved_configs ||= {}
|
143
138
|
end
|
144
139
|
|
140
|
+
def configurable_ancestors
|
141
|
+
@configurable_ancestors ||= ancestors.select{|a| a.included_modules.include?(Configurable) } - [self]
|
142
|
+
end
|
143
|
+
|
145
144
|
private
|
146
145
|
|
147
146
|
def configurable_attr attribute, default=nil, &blk
|
@@ -198,7 +197,7 @@ module Dragonfly
|
|
198
197
|
attr_reader :owner
|
199
198
|
|
200
199
|
def nested_configurable?(method)
|
201
|
-
owner.
|
200
|
+
owner.class.nested_configurables.include?(method.to_sym)
|
202
201
|
end
|
203
202
|
|
204
203
|
end
|
@@ -22,12 +22,11 @@ module Dragonfly
|
|
22
22
|
end
|
23
23
|
|
24
24
|
def store(temp_object, opts={})
|
25
|
-
|
26
|
-
name = meta[:name] || temp_object.original_filename || 'file'
|
25
|
+
name = temp_object.name || 'file'
|
27
26
|
content_type = opts[:content_type] || opts[:mime_type] || 'application/octet-stream'
|
28
27
|
|
29
28
|
temp_object.file do |f|
|
30
|
-
doc = CouchRest::Document.new(:meta => marshal_encode(meta))
|
29
|
+
doc = CouchRest::Document.new(:meta => marshal_encode(temp_object.meta))
|
31
30
|
response = db.save_doc(doc)
|
32
31
|
doc.put_attachment(name, f.dup, :content_type => content_type)
|
33
32
|
form_uid(response['id'], name)
|
@@ -15,11 +15,10 @@ module Dragonfly
|
|
15
15
|
configurable_attr :store_meta, true
|
16
16
|
|
17
17
|
def store(temp_object, opts={})
|
18
|
-
meta = opts[:meta] || {}
|
19
18
|
relative_path = if opts[:path]
|
20
19
|
opts[:path]
|
21
20
|
else
|
22
|
-
filename =
|
21
|
+
filename = temp_object.name || 'file'
|
23
22
|
relative_path = relative_path_for(filename)
|
24
23
|
end
|
25
24
|
|
@@ -28,9 +27,8 @@ module Dragonfly
|
|
28
27
|
until !File.exist?(path)
|
29
28
|
path = disambiguate(path)
|
30
29
|
end
|
31
|
-
prepare_path(path)
|
32
30
|
temp_object.to_file(path).close
|
33
|
-
store_meta_data(path, meta) if store_meta
|
31
|
+
store_meta_data(path, temp_object.meta) if store_meta
|
34
32
|
rescue Errno::EACCES => e
|
35
33
|
raise UnableToStore, e.message
|
36
34
|
end
|
@@ -124,11 +122,6 @@ module Dragonfly
|
|
124
122
|
end
|
125
123
|
end
|
126
124
|
|
127
|
-
def prepare_path(path)
|
128
|
-
dir = File.dirname(path)
|
129
|
-
FileUtils.mkdir_p(dir) unless File.exist?(dir)
|
130
|
-
end
|
131
|
-
|
132
125
|
def purge_empty_directories(path)
|
133
126
|
containing_directory = Pathname.new(path).dirname
|
134
127
|
containing_directory.ascend do |relative_dir|
|