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.

Files changed (58) hide show
  1. data/Gemfile +1 -1
  2. data/History.md +32 -0
  3. data/README.md +54 -32
  4. data/VERSION +1 -1
  5. data/dragonfly.gemspec +23 -21
  6. data/extra_docs/Configuration.md +0 -8
  7. data/extra_docs/DataStorage.md +13 -5
  8. data/extra_docs/ExampleUseCases.md +4 -1
  9. data/extra_docs/Heroku.md +14 -6
  10. data/extra_docs/Models.md +5 -1
  11. data/extra_docs/Rails3.md +1 -1
  12. data/extra_docs/URLs.md +8 -1
  13. data/lib/dragonfly.rb +7 -8
  14. data/lib/dragonfly/active_model_extensions/attachment.rb +37 -17
  15. data/lib/dragonfly/active_model_extensions/attachment_class_methods.rb +1 -1
  16. data/lib/dragonfly/active_model_extensions/validations.rb +53 -26
  17. data/lib/dragonfly/analyser.rb +1 -1
  18. data/lib/dragonfly/app.rb +1 -1
  19. data/lib/dragonfly/config/heroku.rb +7 -0
  20. data/lib/dragonfly/configurable.rb +19 -20
  21. data/lib/dragonfly/data_storage/couch_data_store.rb +2 -3
  22. data/lib/dragonfly/data_storage/file_data_store.rb +2 -9
  23. data/lib/dragonfly/data_storage/mongo_data_store.rb +1 -1
  24. data/lib/dragonfly/data_storage/s3data_store.rb +17 -10
  25. data/lib/dragonfly/function_manager.rb +4 -8
  26. data/lib/dragonfly/has_filename.rb +24 -0
  27. data/lib/dragonfly/image_magick/analyser.rb +1 -1
  28. data/lib/dragonfly/image_magick/generator.rb +1 -1
  29. data/lib/dragonfly/image_magick/processor.rb +5 -0
  30. data/lib/dragonfly/image_magick/utils.rb +1 -2
  31. data/lib/dragonfly/job.rb +52 -71
  32. data/lib/dragonfly/railtie.rb +1 -1
  33. data/lib/dragonfly/temp_object.rb +32 -14
  34. data/lib/dragonfly/url_attributes.rb +30 -0
  35. data/lib/dragonfly/url_mapper.rb +2 -2
  36. data/samples/DSC02119.JPG +0 -0
  37. data/samples/a.jp2 +0 -0
  38. data/samples/beach.jpg +0 -0
  39. data/spec/dragonfly/active_model_extensions/model_spec.rb +63 -40
  40. data/spec/dragonfly/active_model_extensions/spec_helper.rb +4 -0
  41. data/spec/dragonfly/app_spec.rb +11 -3
  42. data/spec/dragonfly/configurable_spec.rb +38 -20
  43. data/spec/dragonfly/data_storage/file_data_store_spec.rb +24 -36
  44. data/spec/dragonfly/data_storage/s3_data_store_spec.rb +21 -5
  45. data/spec/dragonfly/data_storage/shared_data_store_examples.rb +2 -2
  46. data/spec/dragonfly/has_filename_spec.rb +88 -0
  47. data/spec/dragonfly/image_magick/analyser_spec.rb +10 -0
  48. data/spec/dragonfly/image_magick/processor_spec.rb +11 -0
  49. data/spec/dragonfly/job_spec.rb +173 -167
  50. data/spec/dragonfly/temp_object_spec.rb +82 -10
  51. data/spec/dragonfly/url_attributes.rb +47 -0
  52. data/spec/dragonfly/url_mapper_spec.rb +9 -1
  53. data/spec/functional/model_urls_spec.rb +36 -1
  54. metadata +179 -250
  55. data/lib/dragonfly/core_ext/string.rb +0 -9
  56. data/lib/dragonfly/core_ext/symbol.rb +0 -9
  57. data/spec/dragonfly/core_ext/string_spec.rb +0 -17
  58. data/spec/dragonfly/core_ext/symbol_spec.rb +0 -17
@@ -34,7 +34,7 @@ module Dragonfly
34
34
  content_type = opts[:content_type] || opts[:mime_type] || 'application/octet-stream'
35
35
  temp_object.file do |f|
36
36
  mongo_id = grid.put(f, :content_type => content_type,
37
- :metadata => marshal_encode(opts[:meta] || {}))
37
+ :metadata => marshal_encode(temp_object.meta))
38
38
  mongo_id.to_s
39
39
  end
40
40
  end
@@ -11,15 +11,20 @@ module Dragonfly
11
11
  configurable_attr :bucket_name
12
12
  configurable_attr :access_key_id
13
13
  configurable_attr :secret_access_key
14
- configurable_attr :use_filesystem, true
15
14
  configurable_attr :region
15
+ configurable_attr :use_filesystem, true
16
16
  configurable_attr :storage_headers, {'x-amz-acl' => 'public-read'}
17
+ configurable_attr :url_scheme, 'http'
17
18
 
18
19
  REGIONS = {
19
- 'us-east-1' => 's3.amazonaws.com', #default
20
- 'eu-west-1' => 's3-eu-west-1.amazonaws.com',
20
+ 'us-east-1' => 's3.amazonaws.com', #default
21
+ 'us-west-1' => 's3-us-west-1.amazonaws.com',
22
+ 'us-west-2' => 's3-us-west-2.amazonaws.com',
23
+ 'ap-northeast-1' => 's3-ap-northeast-1.amazonaws.com',
21
24
  'ap-southeast-1' => 's3-ap-southeast-1.amazonaws.com',
22
- 'us-west-1' => 's3-us-west-1.amazonaws.com'
25
+ 'eu-west-1' => 's3-eu-west-1.amazonaws.com',
26
+ 'sa-east-1' => 's3-sa-east-1.amazonaws.com',
27
+ 'sa-east-1' => 's3-sa-east-1.amazonaws.com'
23
28
  }
24
29
 
25
30
  def initialize(opts={})
@@ -33,17 +38,18 @@ module Dragonfly
33
38
  ensure_configured
34
39
  ensure_bucket_initialized
35
40
 
36
- meta = opts[:meta] || {}
37
41
  headers = opts[:headers] || {}
38
- uid = opts[:path] || generate_uid(meta[:name] || temp_object.original_filename || 'file')
42
+ mime_type = opts[:mime_type] || opts[:content_type]
43
+ headers['Content-Type'] = mime_type if mime_type
44
+ uid = opts[:path] || generate_uid(temp_object.name || 'file')
39
45
 
40
46
  rescuing_socket_errors do
41
47
  if use_filesystem
42
48
  temp_object.file do |f|
43
- storage.put_object(bucket_name, uid, f, full_storage_headers(headers, meta))
49
+ storage.put_object(bucket_name, uid, f, full_storage_headers(headers, temp_object.meta))
44
50
  end
45
51
  else
46
- storage.put_object(bucket_name, uid, temp_object.data, full_storage_headers(headers, meta))
52
+ storage.put_object(bucket_name, uid, temp_object.data, full_storage_headers(headers, temp_object.meta))
47
53
  end
48
54
  end
49
55
 
@@ -75,7 +81,8 @@ module Dragonfly
75
81
  storage.get_object_url(bucket_name, uid, opts[:expires])
76
82
  end
77
83
  else
78
- "http://#{bucket_name}.s3.amazonaws.com/#{uid}"
84
+ scheme = opts[:scheme] || url_scheme
85
+ "#{scheme}://#{bucket_name}.s3.amazonaws.com/#{uid}"
79
86
  end
80
87
  end
81
88
 
@@ -93,7 +100,7 @@ module Dragonfly
93
100
  end
94
101
 
95
102
  def bucket_exists?
96
- rescuing_socket_errors{ storage.get_bucket_location(bucket_name) }
103
+ rescuing_socket_errors{ storage.get_bucket(bucket_name) }
97
104
  true
98
105
  rescue Excon::Errors::NotFound => e
99
106
  false
@@ -26,7 +26,7 @@ module Dragonfly
26
26
  obj.use_same_log_as(self) if obj.is_a?(Loggable)
27
27
  obj.use_as_fallback_config(self) if obj.is_a?(Configurable)
28
28
  methods_to_add(obj).each do |meth|
29
- add meth.to_sym, obj.method(meth)
29
+ add meth, obj.method(meth)
30
30
  end
31
31
  objects << obj
32
32
  obj
@@ -58,13 +58,9 @@ module Dragonfly
58
58
  private
59
59
 
60
60
  def methods_to_add(obj)
61
- if obj.is_a?(Configurable)
62
- obj.public_methods(false) -
63
- obj.config_methods.map{|meth| meth.to_method_name} -
64
- [:configured_class.to_method_name] # Hacky - there must be a better way...
65
- else
66
- obj.public_methods(false)
67
- end
61
+ methods = obj.public_methods(false).map{|m| m.to_sym }
62
+ methods -= obj.config_methods if obj.is_a?(Configurable)
63
+ methods
68
64
  end
69
65
 
70
66
  end
@@ -0,0 +1,24 @@
1
+ module Dragonfly
2
+ # Convenience methods for setting basename and extension
3
+ # Including class needs to define a 'name' accessor
4
+ # which is assumed to hold a filename-style string
5
+ module HasFilename
6
+
7
+ def basename
8
+ File.basename(name, '.*') if name
9
+ end
10
+
11
+ def basename=(basename)
12
+ self.name = [basename, ext].compact.join('.')
13
+ end
14
+
15
+ def ext
16
+ File.extname(name)[/\.(.*)/, 1] if name
17
+ end
18
+
19
+ def ext=(ext)
20
+ self.name = [(basename || 'file'), ext].join('.')
21
+ end
22
+
23
+ end
24
+ end
@@ -2,8 +2,8 @@ module Dragonfly
2
2
  module ImageMagick
3
3
  class Analyser
4
4
 
5
- include Utils
6
5
  include Configurable
6
+ include Utils
7
7
 
8
8
  def width(temp_object)
9
9
  identify(temp_object)[:width]
@@ -36,8 +36,8 @@ module Dragonfly
36
36
  '900' => 900
37
37
  }
38
38
 
39
- include Utils
40
39
  include Configurable
40
+ include Utils
41
41
 
42
42
  def plain(width, height, colour, opts={})
43
43
  format = opts[:format] || 'png'
@@ -20,12 +20,17 @@ module Dragonfly
20
20
  CROP_GEOMETRY = /^(\d+)x(\d+)([+-]\d+)?([+-]\d+)?(\w{1,2})?$/ # e.g. '30x30+10+10'
21
21
  THUMB_GEOMETRY = Regexp.union RESIZE_GEOMETRY, CROPPED_RESIZE_GEOMETRY, CROP_GEOMETRY
22
22
 
23
+ include Configurable
23
24
  include Utils
24
25
 
25
26
  def resize(temp_object, geometry)
26
27
  convert(temp_object, "-resize #{geometry}")
27
28
  end
28
29
 
30
+ def auto_orient(temp_object)
31
+ convert(temp_object, "-auto-orient")
32
+ end
33
+
29
34
  def crop(temp_object, opts={})
30
35
  width = opts[:width]
31
36
  height = opts[:height]
@@ -21,8 +21,7 @@ module Dragonfly
21
21
  def identify(temp_object)
22
22
  # example of details string:
23
23
  # myimage.png PNG 200x100 200x100+0+0 8-bit DirectClass 31.2kb
24
- format, geometry, depth = raw_identify(temp_object).scan(/([A-Z]+) (.+) .+ (\d+)-bit/)[0]
25
- width, height = geometry.split('x')
24
+ format, width, height, depth = raw_identify(temp_object).scan(/([A-Z0-9]+) (\d+)x(\d+) .+ (\d+)-bit/)[0]
26
25
  {
27
26
  :format => format.downcase.to_sym,
28
27
  :width => width.to_i,
data/lib/dragonfly/job.rb CHANGED
@@ -10,17 +10,19 @@ module Dragonfly
10
10
  # Exceptions
11
11
  class AppDoesNotMatch < StandardError; end
12
12
  class JobAlreadyApplied < StandardError; end
13
+ class NoContent < StandardError; end
13
14
  class NothingToProcess < StandardError; end
14
15
  class NothingToEncode < StandardError; end
15
- class NothingToAnalyse < StandardError; end
16
16
  class InvalidArray < StandardError; end
17
17
  class NoSHAGiven < StandardError; end
18
18
  class IncorrectSHA < StandardError; end
19
19
 
20
20
  extend Forwardable
21
21
  def_delegators :result,
22
- :data, :file, :tempfile, :path, :to_file, :size, :each
23
- def_delegator :app, :server
22
+ :data, :file, :tempfile, :path, :to_file, :size, :each,
23
+ :meta, :meta=, :name, :name=, :basename, :basename=, :ext, :ext=
24
+ def_delegator :app,
25
+ :server
24
26
 
25
27
  class Step
26
28
 
@@ -81,7 +83,7 @@ module Dragonfly
81
83
 
82
84
  class Encode < Step
83
85
  def init
84
- job.meta[:format] = format
86
+ job.url_attrs[:format] = format
85
87
  end
86
88
  def format
87
89
  args.first
@@ -92,8 +94,7 @@ module Dragonfly
92
94
  def apply
93
95
  raise NothingToEncode, "Can't encode because temp object has not been initialized. Need to fetch first?" unless job.temp_object
94
96
  content, meta = job.app.encoder.encode(job.temp_object, format, *arguments)
95
- job.update(content, meta)
96
- job.meta[:format] = format
97
+ job.update(content, (meta || {}).merge(:format => format))
97
98
  end
98
99
  end
99
100
 
@@ -106,19 +107,22 @@ module Dragonfly
106
107
 
107
108
  class FetchFile < Step
108
109
  def init
109
- job.name = File.basename(path)
110
+ job.url_attrs[:name] = filename
110
111
  end
111
112
  def path
112
- File.expand_path(args.first)
113
+ @path ||= File.expand_path(args.first)
114
+ end
115
+ def filename
116
+ @filename ||= File.basename(path)
113
117
  end
114
118
  def apply
115
- job.temp_object = TempObject.new(Pathname.new(path))
119
+ job.update(Pathname.new(path), :name => filename)
116
120
  end
117
121
  end
118
122
 
119
123
  class FetchUrl < Step
120
124
  def init
121
- job.name = File.basename(path) if path[/[^\/]$/]
125
+ job.url_attrs[:name] = filename
122
126
  end
123
127
  def url
124
128
  @url ||= (args.first[%r<^\w+://>] ? args.first : "http://#{args.first}")
@@ -126,9 +130,12 @@ module Dragonfly
126
130
  def path
127
131
  @path ||= URI.parse(url).path
128
132
  end
133
+ def filename
134
+ @filename ||= File.basename(path) if path[/[^\/]$/]
135
+ end
129
136
  def apply
130
137
  open(url) do |f|
131
- job.temp_object = TempObject.new(f.read)
138
+ job.update(f.read, :name => filename)
132
139
  end
133
140
  end
134
141
  end
@@ -183,7 +190,7 @@ module Dragonfly
183
190
 
184
191
  def format
185
192
  apply
186
- format_from_meta || analyse(:format)
193
+ meta[:format] || (ext.to_sym if ext && app.trust_file_extensions) || analyse(:format)
187
194
  end
188
195
 
189
196
  def mime_type
@@ -196,13 +203,13 @@ module Dragonfly
196
203
 
197
204
  end
198
205
 
199
- def initialize(app, content=nil, meta={})
206
+ def initialize(app, content=nil, meta={}, url_attrs={})
200
207
  @app = app
201
208
  @steps = []
202
209
  @next_step_index = 0
203
- @meta = {}
204
210
  @previous_temp_objects = []
205
- update(content, meta)
211
+ update(content, meta) if content
212
+ self.url_attrs = url_attrs
206
213
  end
207
214
 
208
215
  # Used by 'dup' and 'clone'
@@ -237,9 +244,6 @@ module Dragonfly
237
244
  end
238
245
 
239
246
  def analyse(method, *args)
240
- unless result
241
- raise NothingToAnalyse, "Can't analyse because temp object has not been initialized. Need to fetch first?"
242
- end
243
247
  analyser.analyse(result, method, *args)
244
248
  end
245
249
 
@@ -255,11 +259,6 @@ module Dragonfly
255
259
  next_step_index == steps.length
256
260
  end
257
261
 
258
- def result
259
- apply
260
- temp_object
261
- end
262
-
263
262
  def applied_steps
264
263
  steps[0...next_step_index]
265
264
  end
@@ -309,6 +308,12 @@ module Dragonfly
309
308
  app.url_for(self, attributes_for_url.merge(opts)) unless steps.empty?
310
309
  end
311
310
 
311
+ def url_attrs=(hash)
312
+ @url_attrs = UrlAttributes[hash]
313
+ end
314
+
315
+ attr_reader :url_attrs
316
+
312
317
  def b64_data
313
318
  "data:#{mime_type};base64,#{Base64.encode64(data)}"
314
319
  end
@@ -324,7 +329,7 @@ module Dragonfly
324
329
  end
325
330
 
326
331
  def to_fetched_job(uid)
327
- new_job = self.class.new(app, temp_object, meta)
332
+ new_job = self.class.new(app, temp_object, meta, url_attrs)
328
333
  new_job.fetch!(uid)
329
334
  new_job.next_step_index = 1
330
335
  new_job
@@ -368,56 +373,20 @@ module Dragonfly
368
373
  # Misc
369
374
 
370
375
  def store(opts={})
371
- app.store(result, opts.merge(:meta => meta))
376
+ temp_object = result
377
+ app.store(temp_object, opts_for_store.merge(opts).merge(:meta => temp_object.meta))
372
378
  end
373
379
 
374
380
  def inspect
375
381
  to_s.sub(/>$/, " app=#{app}, steps=#{steps.inspect}, temp_object=#{temp_object.inspect}, steps applied:#{applied_steps.length}/#{steps.length} >")
376
382
  end
377
-
378
- # Name and stuff
379
-
380
- attr_reader :meta
381
-
382
- def meta=(hash)
383
- raise ArgumentError, "meta must be a hash, you tried setting it as #{hash.inspect}" unless hash.is_a?(Hash)
384
- @meta = hash
385
- end
386
-
387
- def name
388
- meta[:name]
389
- end
390
-
391
- def name=(name)
392
- meta[:name] = name
393
- end
394
383
 
395
- def basename
396
- meta[:basename] || (File.basename(name, '.*') if name)
397
- end
398
-
399
- def ext
400
- meta[:ext] || (File.extname(name)[/\.(.*)/, 1] if name)
401
- end
402
-
403
- def attributes_for_url
404
- attrs = meta.reject{|k, v| !server.params_in_url.include?(k.to_s) }
405
- attrs[:basename] ||= basename if server.params_in_url.include?('basename')
406
- attrs[:ext] ||= ext if server.params_in_url.include?('ext')
407
- attrs[:format] = (attrs[:format] || format_from_meta).to_s if server.params_in_url.include?('format')
408
- attrs.delete_if{|k, v| v.blank? }
409
- attrs
410
- end
411
-
412
- def update(content, meta)
413
- if meta
414
- meta.merge!(meta.delete(:meta)) if meta[:meta] # legacy data etc. may have nested meta hash - deprecate gracefully here
415
- self.meta.merge!(meta)
416
- end
417
- if content
418
- self.temp_object = TempObject.new(content)
419
- self.name = temp_object.original_filename if name.nil? && temp_object.original_filename
384
+ def update(content, new_meta)
385
+ if new_meta
386
+ new_meta.merge!(new_meta.delete(:meta)) if new_meta[:meta] # legacy data etc. may have nested meta hash - deprecate gracefully here
420
387
  end
388
+ old_meta = temp_object ? temp_object.meta : {}
389
+ self.temp_object = TempObject.new(content, old_meta.merge(new_meta || {}))
421
390
  end
422
391
 
423
392
  def close
@@ -432,15 +401,27 @@ module Dragonfly
432
401
 
433
402
  private
434
403
 
435
- attr_reader :previous_temp_objects
436
-
437
- def format_from_meta
438
- meta[:format] || (ext.to_sym if ext && app.trust_file_extensions)
404
+ def result
405
+ apply
406
+ temp_object || raise(NoContent, "Job has not been initialized with content. Need to fetch first?")
407
+ end
408
+
409
+ def attributes_for_url
410
+ attrs = url_attrs.slice(*server.params_in_url)
411
+ attrs[:format] = (attrs[:format] || (url_attrs.ext if app.trust_file_extensions)).to_s if server.params_in_url.include?('format')
412
+ attrs.delete_if{|k, v| v.blank? }
413
+ attrs
439
414
  end
415
+
416
+ attr_reader :previous_temp_objects
440
417
 
441
418
  def last_step_of_type(type)
442
419
  steps.select{|s| s.is_a?(type) }.last
443
420
  end
421
+
422
+ def opts_for_store
423
+ {:mime_type => mime_type}
424
+ end
444
425
 
445
426
  end
446
427
  end
@@ -4,7 +4,7 @@ require 'rails'
4
4
  module Dragonfly
5
5
  class Railtie < ::Rails::Railtie
6
6
  initializer "dragonfly.railtie.initializer" do |app|
7
- app.middleware.insert 1, Dragonfly::CookieMonster
7
+ app.middleware.insert_before 'ActionDispatch::Cookies', Dragonfly::CookieMonster
8
8
  end
9
9
  end
10
10
  end
@@ -32,20 +32,14 @@ module Dragonfly
32
32
  #
33
33
  class TempObject
34
34
 
35
+ include HasFilename
36
+
35
37
  # Exceptions
36
38
  class Closed < RuntimeError; end
37
39
 
38
- # Class configuration
39
- class << self
40
-
41
- include Configurable
42
- configurable_attr :block_size, 8192
43
-
44
- end
45
-
46
40
  # Instance Methods
47
41
 
48
- def initialize(obj)
42
+ def initialize(obj, meta={})
49
43
  if obj.is_a? TempObject
50
44
  @data = obj.get_data
51
45
  @tempfile = obj.get_tempfile
@@ -74,9 +68,22 @@ module Dragonfly
74
68
  elsif @pathname
75
69
  @pathname.basename.to_s
76
70
  end
71
+
72
+ # Meta
73
+ @meta = meta
74
+ @meta[:name] ||= @original_filename if @original_filename
77
75
  end
78
76
 
79
77
  attr_reader :original_filename
78
+ attr_accessor :meta
79
+
80
+ def name
81
+ meta[:name]
82
+ end
83
+
84
+ def name=(name)
85
+ meta[:name] = name
86
+ end
80
87
 
81
88
  def data
82
89
  raise Closed, "can't read data as TempObject has been closed" if closed?
@@ -124,12 +131,14 @@ module Dragonfly
124
131
  end
125
132
  end
126
133
 
127
- def to_file(path)
134
+ def to_file(path, opts={})
135
+ mode = opts[:mode] || 0644
136
+ prepare_path(path) unless opts[:mkdirs] == false
128
137
  if @data
129
- File.open(path, 'wb'){|f| f.write(@data) }
138
+ File.open(path, 'wb', mode){|f| f.write(@data) }
130
139
  else
131
140
  FileUtils.cp(self.path, path)
132
- File.chmod(0644, path)
141
+ File.chmod(mode, path)
133
142
  end
134
143
  File.new(path, 'rb')
135
144
  end
@@ -159,6 +168,10 @@ module Dragonfly
159
168
  to_s.sub(/>$/, " #{content_string} >")
160
169
  end
161
170
 
171
+ def unique_id
172
+ @unique_id ||= "#{object_id}#{rand(1000000)}"
173
+ end
174
+
162
175
  protected
163
176
 
164
177
  # We don't use normal accessors here because #data etc. do more than just return the instance var
@@ -177,7 +190,7 @@ module Dragonfly
177
190
  private
178
191
 
179
192
  def block_size
180
- self.class.block_size
193
+ 8192
181
194
  end
182
195
 
183
196
  def copy_to_tempfile(path)
@@ -187,12 +200,17 @@ module Dragonfly
187
200
  end
188
201
 
189
202
  def new_tempfile(content=nil)
190
- tempfile = Tempfile.new('dragonfly')
203
+ tempfile = ext ? Tempfile.new(['dragonfly', ".#{ext}"]) : Tempfile.new('dragonfly')
191
204
  tempfile.binmode
192
205
  tempfile.write(content) if content
193
206
  tempfile.close
194
207
  tempfile
195
208
  end
196
209
 
210
+ def prepare_path(path)
211
+ dir = File.dirname(path)
212
+ FileUtils.mkdir_p(dir) unless File.exist?(dir)
213
+ end
214
+
197
215
  end
198
216
  end