dragonfly 0.9.0 → 0.9.1

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/History.md CHANGED
@@ -1,3 +1,14 @@
1
+ 0.9.1 (2011-05-11)
2
+ ==================
3
+ Features
4
+ --------
5
+ - Added reflection methods `app.processor_methods`, `app.generator_methods` and `app.job_methods`
6
+
7
+ Fixes
8
+ -----
9
+ - Improved performance of `resize_and_crop` method, using imagemagick built-in '^' operator
10
+ - Improved server security validations
11
+
1
12
  0.9.0 (2011-04-27)
2
13
  ==================
3
14
  Features
data/README.md CHANGED
@@ -12,7 +12,7 @@ For the lazy Rails user...
12
12
  **Gemfile**:
13
13
 
14
14
  gem 'rack-cache', :require => 'rack/cache'
15
- gem 'dragonfly', '~>0.9.0'
15
+ gem 'dragonfly', '~>0.9.1'
16
16
 
17
17
  **Initializer** (e.g. config/initializers/dragonfly.rb):
18
18
 
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.9.0
1
+ 0.9.1
data/dragonfly.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{dragonfly}
8
- s.version = "0.9.0"
8
+ s.version = "0.9.1"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = ["Mark Evans"]
12
- s.date = %q{2011-04-27}
12
+ s.date = %q{2011-05-11}
13
13
  s.description = %q{Dragonfly is a framework that enables on-the-fly processing for any content type.
14
14
  It is especially suited to image handling. Its uses range from image thumbnails to standard attachments to on-demand text generation.}
15
15
  s.email = %q{mark@new-bamboo.co.uk}
@@ -172,6 +172,7 @@ Gem::Specification.new do |s|
172
172
  "spec/support/argument_matchers.rb",
173
173
  "spec/support/image_matchers.rb",
174
174
  "spec/support/simple_matchers.rb",
175
+ "spec/test_imagemagick.ru",
175
176
  "yard/handlers/configurable_attr_handler.rb",
176
177
  "yard/setup.rb",
177
178
  "yard/templates/default/fulldoc/html/css/common.css",
@@ -81,6 +81,11 @@ Where is configuration done?
81
81
  In Rails, it should be done in an initializer, e.g. 'config/initializers/dragonfly.rb'.
82
82
  Otherwise it should be done anywhere where general setup is done, early on.
83
83
 
84
+ Reflecting on configuration
85
+ ---------------------------
86
+ There are a few methods you can call on the `app` to see what processors etc. are registered: `processor_methods`, `generator_methods`
87
+ and `job_methods`.
88
+
84
89
  Saved configurations
85
90
  ====================
86
91
  Saved configurations are useful if you often configure the app the same way.
@@ -83,7 +83,7 @@ To configure with the {Dragonfly::DataStorage::S3DataStore S3DataStore}:
83
83
 
84
84
  app.datastore = Dragonfly::DataStorage::S3DataStore.new
85
85
 
86
- app.datastore.configure do |d|
86
+ app.datastore.configure do |c|
87
87
  c.bucket_name = 'my_bucket'
88
88
  c.access_key_id = 'salfjasd34u23'
89
89
  c.secret_access_key = '8u2u3rhkhfo23...'
data/extra_docs/Heroku.md CHANGED
@@ -6,7 +6,7 @@ The default configuration won't work out of the box for Heroku, because
6
6
  - Heroku doesn't allow saving files to the filesystem (although it does use tempfiles)
7
7
  - We won't need {http://tomayko.com/src/rack-cache/ Rack::Cache} on Heroku because it already uses the caching proxy {http://varnish.projects.linpro.no/ Varnish}, which we can make use of
8
8
 
9
- Instead of the normal {Dragonfly::DataStorage::FileDataStore FileDataStore}, we can use the {Dragonfly::DataStorage::S3DataStore S3DataStore}.
9
+ Instead of the normal {file:DataStorage#File\_datastore FileDataStore}, we can use the {file:DataStorage#S3\_datastore S3DataStore}.
10
10
 
11
11
  Assuming you have an S3 account set up...
12
12
 
@@ -45,5 +45,5 @@ Now you can benefit from super-fast images served straight from Heroku's cache!
45
45
 
46
46
  **NOTE**: HEROKU'S CACHE IS CLEARED EVERY TIME YOU DEPLOY!!!
47
47
 
48
- If this is an issue, you may want to look into storing thumbnails on S3 (see {file:ExampleUseCases}), or maybe an after-deploy hook for hitting specific Dragonfly urls you want to cache, etc.
48
+ If this is an issue, you may want to look into storing thumbnails on S3 (see {file:ServingRemotely}), or maybe an after-deploy hook for hitting specific Dragonfly urls you want to cache, etc.
49
49
  It won't be a problem for most sites though.
@@ -124,3 +124,13 @@ You can use `padding-top`, `padding-left`, etc., as well as the standard css sho
124
124
 
125
125
  An alternative for `:font_family` is `:font` (see {http://www.imagemagick.org/script/command-line-options.php#font}), which could be a complete filename.
126
126
  Available fonts are those available on your system.
127
+
128
+ Configuration
129
+ -------------
130
+ There are some options that can be set, e.g. if the imagemagick convert command can't be found:
131
+
132
+ app.configure do |c|
133
+ c.convert_command = "/opt/local/bin/convert" # defaults to "convert"
134
+ c.identify_command = "/opt/local/bin/convert" # defaults to "convert"
135
+ c.log_commands = true # defaults to false
136
+ end
data/extra_docs/Models.md CHANGED
@@ -208,6 +208,11 @@ Validations
208
208
  The property argument of `validates_property` will generally be one of the registered analyser properties as described in {file:Analysers.md Analysers}.
209
209
  However it would actually work for arbitrary properties, including those of non-dragonfly model attributes.
210
210
 
211
+ `validates_property` can also take a proc for the message, yielding the actual value and the model
212
+
213
+ validates_property :width, :of => :cover_image, :in => (0..400),
214
+ :message => proc{|actual, model| "Unlucky #{model.title} - was #{actual}" }
215
+
211
216
  Name and extension
212
217
  ------------------
213
218
  If the object assigned is a file, or responds to `original_filename` (as is the case with file uploads in Rails, etc.), then `name` will be set.
data/extra_docs/Rails2.md CHANGED
@@ -25,11 +25,11 @@ config/initializers/dragonfly.rb:
25
25
 
26
26
  environment.rb:
27
27
 
28
- config.middleware.insert_after 'Rack::Lock', 'Dragonfly::Middleware', :images, '/media'
28
+ config.middleware.insert 0, 'Dragonfly::Middleware', :images, '/media'
29
29
  config.middleware.insert_before 'Dragonfly::Middleware', 'Rack::Cache', {
30
30
  :verbose => true,
31
- :metastore => "file:#{Rails.root}/tmp/dragonfly/cache/meta",
32
- :entitystore => "file:#{Rails.root}/tmp/dragonfly/cache/body"
31
+ :metastore => URI.encode("file:#{Rails.root}/tmp/dragonfly/cache/meta"),
32
+ :entitystore => URI.encode("file:#{Rails.root}/tmp/dragonfly/cache/body")
33
33
  }
34
34
 
35
35
  Gems
data/extra_docs/Rails3.md CHANGED
@@ -26,14 +26,14 @@ application.rb:
26
26
  config.middleware.insert 0, 'Dragonfly::Middleware', :images
27
27
  config.middleware.insert_before 'Dragonfly::Middleware', 'Rack::Cache', {
28
28
  :verbose => true,
29
- :metastore => "file:#{Rails.root}/tmp/dragonfly/cache/meta",
30
- :entitystore => "file:#{Rails.root}/tmp/dragonfly/cache/body"
29
+ :metastore => URI.encode("file:#{Rails.root}/tmp/dragonfly/cache/meta"),
30
+ :entitystore => URI.encode("file:#{Rails.root}/tmp/dragonfly/cache/body")
31
31
  }
32
32
 
33
33
  Gemfile
34
34
  -------
35
35
 
36
- gem 'dragonfly', '~>0.9.0'
36
+ gem 'dragonfly', '~>0.9.1'
37
37
  gem 'rack-cache', :require => 'rack/cache'
38
38
 
39
39
  Capistrano
data/lib/dragonfly/app.rb CHANGED
@@ -145,6 +145,19 @@ module Dragonfly
145
145
  end
146
146
  end
147
147
 
148
+ # Reflection
149
+ def processor_methods
150
+ processor.functions.keys
151
+ end
152
+
153
+ def generator_methods
154
+ generator.functions.keys
155
+ end
156
+
157
+ def job_methods
158
+ job_definitions.definition_names
159
+ end
160
+
148
161
  # Deprecated methods
149
162
  def url_path_prefix=(thing)
150
163
  raise NoMethodError, "url_path_prefix is deprecated - please use url_format, e.g. url_format = '/media/:job/:basename.:format' - see docs for more details"
@@ -2,8 +2,9 @@ module Dragonfly
2
2
  module DataStorage
3
3
 
4
4
  # Exceptions
5
- class DataNotFound < StandardError; end
6
- class UnableToStore < StandardError; end
5
+ class BadUID < RuntimeError; end
6
+ class DataNotFound < RuntimeError; end
7
+ class UnableToStore < RuntimeError; end
7
8
 
8
9
  end
9
10
  end
@@ -6,7 +6,6 @@ module Dragonfly
6
6
  class FileDataStore
7
7
 
8
8
  # Exceptions
9
- class BadUID < RuntimeError; end
10
9
  class UnableToFormUrl < RuntimeError; end
11
10
 
12
11
  include Configurable
@@ -139,7 +138,7 @@ module Dragonfly
139
138
  end
140
139
 
141
140
  def validate_uid!(uid)
142
- raise BadUID, "tried to fetch uid #{uid.inspect} - perhaps due to a malicious user" if uid['..']
141
+ raise BadUID, "tried to retrieve uid #{uid.inspect}" if uid.blank? || uid['../']
143
142
  end
144
143
 
145
144
  end
@@ -37,12 +37,14 @@ module Dragonfly
37
37
  headers = opts[:headers] || {}
38
38
  uid = opts[:path] || generate_uid(meta[:name] || temp_object.original_filename || 'file')
39
39
 
40
- if use_filesystem
41
- temp_object.file do |f|
42
- storage.put_object(bucket_name, uid, f, full_storage_headers(headers, meta))
40
+ rescuing_socket_errors do
41
+ if use_filesystem
42
+ temp_object.file do |f|
43
+ storage.put_object(bucket_name, uid, f, full_storage_headers(headers, meta))
44
+ end
45
+ else
46
+ storage.put_object(bucket_name, uid, temp_object.data, full_storage_headers(headers, meta))
43
47
  end
44
- else
45
- storage.put_object(bucket_name, uid, temp_object.data, full_storage_headers(headers, meta))
46
48
  end
47
49
 
48
50
  uid
@@ -50,7 +52,7 @@ module Dragonfly
50
52
 
51
53
  def retrieve(uid)
52
54
  ensure_configured
53
- response = storage.get_object(bucket_name, uid)
55
+ response = rescuing_socket_errors{ storage.get_object(bucket_name, uid) }
54
56
  [
55
57
  response.body,
56
58
  parse_s3_metadata(response.headers)
@@ -60,7 +62,7 @@ module Dragonfly
60
62
  end
61
63
 
62
64
  def destroy(uid)
63
- storage.delete_object(bucket_name, uid)
65
+ rescuing_socket_errors{ storage.delete_object(bucket_name, uid) }
64
66
  rescue Excon::Errors::NotFound => e
65
67
  raise DataNotFound, "#{e} - #{uid}"
66
68
  end
@@ -87,7 +89,7 @@ module Dragonfly
87
89
  end
88
90
 
89
91
  def bucket_exists?
90
- storage.get_bucket_location(bucket_name)
92
+ rescuing_socket_errors{ storage.get_bucket_location(bucket_name) }
91
93
  true
92
94
  rescue Excon::Errors::NotFound => e
93
95
  false
@@ -106,7 +108,7 @@ module Dragonfly
106
108
 
107
109
  def ensure_bucket_initialized
108
110
  unless @bucket_initialized
109
- storage.put_bucket(bucket_name, 'LocationConstraint' => region) unless bucket_exists?
111
+ rescuing_socket_errors{ storage.put_bucket(bucket_name, 'LocationConstraint' => region) } unless bucket_exists?
110
112
  @bucket_initialized = true
111
113
  end
112
114
  end
@@ -134,6 +136,13 @@ module Dragonfly
134
136
  REGIONS.keys
135
137
  end
136
138
 
139
+ def rescuing_socket_errors(&block)
140
+ yield
141
+ rescue Excon::Errors::SocketError => e
142
+ storage.reload
143
+ yield
144
+ end
145
+
137
146
  end
138
147
 
139
148
  end
@@ -35,8 +35,9 @@ module Dragonfly
35
35
  y = "#{opts[:y] || 0}"
36
36
  y = '+' + y unless y[/^[+-]/]
37
37
  repage = opts[:repage] == false ? '' : '+repage'
38
+ resize = opts[:resize]
38
39
 
39
- convert(temp_object, "-crop #{width}x#{height}#{x}#{y}#{" -gravity #{gravity}" if gravity} #{repage}")
40
+ convert(temp_object, "#{"-resize #{resize} " if resize}#{"-gravity #{gravity} " if gravity}-crop #{width}x#{height}#{x}#{y} #{repage}")
40
41
  end
41
42
 
42
43
  def flip(temp_object)
@@ -53,20 +54,18 @@ module Dragonfly
53
54
  alias grayscale greyscale
54
55
 
55
56
  def resize_and_crop(temp_object, opts={})
56
- attrs = identify(temp_object)
57
- current_width = attrs[:width].to_i
58
- current_height = attrs[:height].to_i
59
-
60
- width = opts[:width] ? opts[:width].to_i : current_width
61
- height = opts[:height] ? opts[:height].to_i : current_height
62
- gravity = opts[:gravity] || 'c'
63
-
64
- if width != current_width || height != current_height
65
- scale = [width.to_f / current_width, height.to_f / current_height].max
66
- temp_object = TempObject.new(resize(temp_object, "#{(scale * current_width).ceil}x#{(scale * current_height).ceil}"))
57
+ if !opts[:width] && !opts[:height]
58
+ return temp_object
59
+ elsif !opts[:width] || !opts[:height]
60
+ attrs = identify(temp_object)
61
+ opts[:width] ||= attrs[:width]
62
+ opts[:height] ||= attrs[:height]
67
63
  end
68
64
 
69
- crop(temp_object, :width => width, :height => height, :gravity => gravity)
65
+ opts[:gravity] ||= 'c'
66
+
67
+ opts[:resize] = "#{opts[:width]}x#{opts[:height]}^^"
68
+ crop(temp_object, opts)
70
69
  end
71
70
 
72
71
  def rotate(temp_object, amount, opts={})
@@ -17,6 +17,10 @@ module Dragonfly
17
17
  jd[name].build!(self, *args)
18
18
  end
19
19
  end
20
+
21
+ def definition_names
22
+ job_definitions.keys
23
+ end
20
24
 
21
25
  private
22
26
 
@@ -19,8 +19,8 @@ begin
19
19
  require 'rack/cache'
20
20
  Rails.application.middleware.insert_before 'Dragonfly::Middleware', 'Rack::Cache', {
21
21
  :verbose => true,
22
- :metastore => URI.encode("file:#{Rails.root}/tmp/dragonfly/cache/meta"), # URI encoded because Windows
23
- :entitystore => URI.encode("file:#{Rails.root}/tmp/dragonfly/cache/body") # has problems with spaces
22
+ :metastore => URI.encode("file:#{Rails.root}/tmp/dragonfly/cache/meta"), # URI encoded in case of spaces
23
+ :entitystore => URI.encode("file:#{Rails.root}/tmp/dragonfly/cache/body")
24
24
  }
25
25
  rescue LoadError => e
26
26
  app.log.warn("Warning: couldn't find rack-cache for caching dragonfly content")
@@ -24,7 +24,7 @@ module Dragonfly
24
24
  job.apply
25
25
  [200, success_headers, job.result]
26
26
  end
27
- rescue DataStorage::DataNotFound => e
27
+ rescue DataStorage::DataNotFound, DataStorage::BadUID => e
28
28
  app.log.warn(e.message)
29
29
  [404, {"Content-Type" => 'text/plain'}, ['Not found']]
30
30
  end
@@ -1,9 +1,14 @@
1
1
  module Dragonfly
2
2
  class Server
3
3
 
4
+ # Exceptions
5
+ class JobNotAllowed < RuntimeError; end
6
+
4
7
  include Loggable
5
8
  include Configurable
6
9
 
10
+ configurable_attr :allow_fetch_file, false
11
+ configurable_attr :allow_fetch_url, false
7
12
  configurable_attr :dragonfly_url, '/dragonfly'
8
13
  configurable_attr :protect_from_dos_attacks, false
9
14
  configurable_attr :url_format, '/:job/:basename.:format'
@@ -28,6 +33,7 @@ module Dragonfly
28
33
  dragonfly_response
29
34
  elsif (params = url_mapper.params_for(env["PATH_INFO"], env["QUERY_STRING"])) && params['job']
30
35
  job = Job.deserialize(params['job'], app)
36
+ validate_job!(job)
31
37
  job.validate_sha!(params['sha']) if protect_from_dos_attacks
32
38
  response = Response.new(job, env)
33
39
  catch(:halt) do
@@ -39,13 +45,16 @@ module Dragonfly
39
45
  else
40
46
  [404, {'Content-Type' => 'text/plain', 'X-Cascade' => 'pass'}, ['Not found']]
41
47
  end
42
- rescue Serializer::BadString, Job::InvalidArray => e
43
- log.warn(e.message)
44
- [404, {'Content-Type' => 'text/plain'}, ['Not found']]
45
48
  rescue Job::NoSHAGiven => e
46
49
  [400, {"Content-Type" => 'text/plain'}, ["You need to give a SHA parameter"]]
47
50
  rescue Job::IncorrectSHA => e
48
51
  [400, {"Content-Type" => 'text/plain'}, ["The SHA parameter you gave (#{e}) is incorrect"]]
52
+ rescue JobNotAllowed => e
53
+ log.warn(e.message)
54
+ [403, {"Content-Type" => 'text/plain'}, ["Forbidden"]]
55
+ rescue Serializer::BadString, Job::InvalidArray => e
56
+ log.warn(e.message)
57
+ [404, {'Content-Type' => 'text/plain'}, ['Not found']]
49
58
  end
50
59
 
51
60
  def url_for(job, opts={})
@@ -99,5 +108,12 @@ module Dragonfly
99
108
  ]
100
109
  end
101
110
 
111
+ def validate_job!(job)
112
+ if job.fetch_file_step && !allow_fetch_file ||
113
+ job.fetch_url_step && !allow_fetch_url
114
+ raise JobNotAllowed, "Dragonfly Server doesn't allow requesting job with steps #{job.steps.inspect}"
115
+ end
116
+ end
117
+
102
118
  end
103
119
  end
@@ -132,4 +132,24 @@ describe Dragonfly::App do
132
132
  end
133
133
  end
134
134
 
135
+ describe "reflection methods" do
136
+ before(:each) do
137
+ @app = test_app.configure do |c|
138
+ c.processor.add(:milk){}
139
+ c.generator.add(:butter){}
140
+ c.job(:bacon){}
141
+ end
142
+
143
+ end
144
+ it "should return processor methods" do
145
+ @app.processor_methods.should == [:milk]
146
+ end
147
+ it "should return generator methods" do
148
+ @app.generator_methods.should == [:butter]
149
+ end
150
+ it "should return job methods" do
151
+ @app.job_methods.should == [:bacon]
152
+ end
153
+ end
154
+
135
155
  end
@@ -168,10 +168,16 @@ describe Dragonfly::DataStorage::FileDataStore do
168
168
  meta.should == {:dog => 'food'}
169
169
  end
170
170
 
171
- it "should raise an error if the file path has .. in it" do
171
+ it "should raise a BadUID error if the file path has ../ in it" do
172
172
  expect{
173
173
  @data_store.retrieve('jelly_beans/../are/good')
174
- }.to raise_error(Dragonfly::DataStorage::FileDataStore::BadUID)
174
+ }.to raise_error(Dragonfly::DataStorage::BadUID)
175
+ end
176
+
177
+ it "should not raise a BadUID error if the file path has .. but not ../ in it" do
178
+ expect{
179
+ @data_store.retrieve('jelly_beans..good')
180
+ }.to raise_error(Dragonfly::DataStorage::DataNotFound)
175
181
  end
176
182
  end
177
183
 
@@ -199,10 +205,10 @@ describe Dragonfly::DataStorage::FileDataStore do
199
205
  File.exist?("#{@data_store.root_path}/#{uid}.extra").should be_false
200
206
  end
201
207
 
202
- it "should raise an error if the file path has .. in it" do
208
+ it "should raise an error if the file path has ../ in it" do
203
209
  expect{
204
210
  @data_store.destroy('jelly_beans/../are/good')
205
- }.to raise_error(Dragonfly::DataStorage::FileDataStore::BadUID)
211
+ }.to raise_error(Dragonfly::DataStorage::BadUID)
206
212
  end
207
213
  end
208
214
 
@@ -89,25 +89,40 @@ describe Dragonfly::DataStorage::S3DataStore do
89
89
  uid = @data_store.store(temp_object)
90
90
  @data_store.retrieve(uid).first.should == "gollum"
91
91
  end
92
- end
93
-
94
- if enabled # Fog.mock! doesn't work consistently with real one here
95
92
 
96
- describe "destroy" do
97
- before(:each) do
98
- @temp_object = Dragonfly::TempObject.new('gollum')
93
+ if enabled # Fog.mock! doesn't act consistently here
94
+ it "should reset the connection and try again if Fog throws a socket EOFError" do
95
+ temp_object = Dragonfly::TempObject.new('gollum')
96
+ @data_store.storage.should_receive(:put_object).exactly(:once).and_raise(Excon::Errors::SocketError.new(EOFError.new))
97
+ @data_store.storage.should_receive(:put_object).with(BUCKET_NAME, anything, anything, hash_including)
98
+ @data_store.store(temp_object)
99
99
  end
100
- it "should raise an error if the data doesn't exist on destroy" do
101
- uid = @data_store.store(@temp_object)
102
- @data_store.destroy(uid)
103
- lambda{
104
- @data_store.destroy(uid)
105
- }.should raise_error(Dragonfly::DataStorage::DataNotFound)
100
+
101
+ it "should just let it raise if Fog throws a socket EOFError again" do
102
+ temp_object = Dragonfly::TempObject.new('gollum')
103
+ @data_store.storage.should_receive(:put_object).and_raise(Excon::Errors::SocketError.new(EOFError.new))
104
+ @data_store.storage.should_receive(:put_object).and_raise(Excon::Errors::SocketError.new(EOFError.new))
105
+ expect{
106
+ @data_store.store(temp_object)
107
+ }.to raise_error(Excon::Errors::SocketError)
106
108
  end
107
109
  end
108
-
109
110
  end
110
111
 
112
+ # Doesn't appear to raise anything right now
113
+ # describe "destroy" do
114
+ # before(:each) do
115
+ # @temp_object = Dragonfly::TempObject.new('gollum')
116
+ # end
117
+ # it "should raise an error if the data doesn't exist on destroy" do
118
+ # uid = @data_store.store(@temp_object)
119
+ # @data_store.destroy(uid)
120
+ # lambda{
121
+ # @data_store.destroy(uid)
122
+ # }.should raise_error(Dragonfly::DataStorage::DataNotFound)
123
+ # end
124
+ # end
125
+
111
126
  describe "domain" do
112
127
  it "should default to the US" do
113
128
  @data_store.region = nil
@@ -183,21 +198,21 @@ describe Dragonfly::DataStorage::S3DataStore do
183
198
  end
184
199
 
185
200
  it "should allow configuring globally" do
186
- @data_store.storage.should_receive(:put_object).with('test-bucket', anything, anything,
201
+ @data_store.storage.should_receive(:put_object).with(BUCKET_NAME, anything, anything,
187
202
  hash_including('x-amz-foo' => 'biscuithead')
188
203
  )
189
204
  @data_store.store(@temp_object)
190
205
  end
191
206
 
192
207
  it "should allow adding per-store" do
193
- @data_store.storage.should_receive(:put_object).with('test-bucket', anything, anything,
208
+ @data_store.storage.should_receive(:put_object).with(BUCKET_NAME, anything, anything,
194
209
  hash_including('x-amz-foo' => 'biscuithead', 'hello' => 'there')
195
210
  )
196
211
  @data_store.store(@temp_object, :headers => {'hello' => 'there'})
197
212
  end
198
213
 
199
214
  it "should let the per-store one take precedence" do
200
- @data_store.storage.should_receive(:put_object).with('test-bucket', anything, anything,
215
+ @data_store.storage.should_receive(:put_object).with(BUCKET_NAME, anything, anything,
201
216
  hash_including('x-amz-foo' => 'override!')
202
217
  )
203
218
  @data_store.store(@temp_object, :headers => {'x-amz-foo' => 'override!'})
@@ -103,7 +103,7 @@ describe Dragonfly::ImageMagick::Processor do
103
103
  it "should take into account the gravity given" do
104
104
  image1 = @processor.crop(@image, :width => '10', :height => '10', :gravity => 'nw')
105
105
  image2 = @processor.crop(@image, :width => '10', :height => '10', :gravity => 'se')
106
- image1.should_not == image2
106
+ image1.should_not equal_image(image2)
107
107
  end
108
108
 
109
109
  it "should clip bits of the image outside of the requested crop area when not nw gravity" do
@@ -147,12 +147,25 @@ describe Dragonfly::ImageMagick::Processor do
147
147
  image.should have_height(355)
148
148
  end
149
149
 
150
+ it "should do nothing if called without width and height" do
151
+ image = @processor.resize_and_crop(@image)
152
+ image.should have_width(280)
153
+ image.should have_height(355)
154
+ image.should eq @image
155
+ end
156
+
150
157
  it "should crop to the correct dimensions" do
151
158
  image = @processor.resize_and_crop(@image, :width => '100', :height => '100')
152
159
  image.should have_width(100)
153
160
  image.should have_height(100)
154
161
  end
155
162
 
163
+ it "should actually resize before cropping" do
164
+ image1 = @processor.resize_and_crop(@image, :width => '100', :height => '100')
165
+ image2 = @processor.crop(@image, :width => '100', :height => '100', :gravity => 'c')
166
+ image1.should_not equal_image(image2)
167
+ end
168
+
156
169
  it "should allow cropping in one dimension" do
157
170
  image = @processor.resize_and_crop(@image, :width => '100')
158
171
  image.should have_width(100)
@@ -162,7 +175,7 @@ describe Dragonfly::ImageMagick::Processor do
162
175
  it "should take into account the gravity given" do
163
176
  image1 = @processor.resize_and_crop(@image, :width => '10', :height => '10', :gravity => 'nw')
164
177
  image2 = @processor.resize_and_crop(@image, :width => '10', :height => '10', :gravity => 'se')
165
- image1.should_not == image2
178
+ image1.should_not equal_image(image2)
166
179
  end
167
180
 
168
181
  end
@@ -32,4 +32,26 @@ describe Dragonfly::JobDefinitions do
32
32
 
33
33
  end
34
34
 
35
+
36
+ describe "#definition_names" do
37
+
38
+ before(:each) do
39
+ @job_definitions = Dragonfly::JobDefinitions.new
40
+ @object = Object.new
41
+ @object.extend @job_definitions
42
+ end
43
+
44
+ it "should provide an empty list when no jobs have been defined" do
45
+ @job_definitions.definition_names.should == []
46
+ end
47
+
48
+ it "should contain the job name when one is defined" do
49
+ @job_definitions.add :foo do |size|
50
+ process :thumb, size
51
+ end
52
+ @job_definitions.definition_names.should eq [:foo]
53
+ end
54
+
55
+ end
56
+
35
57
  end
@@ -86,6 +86,12 @@ describe Dragonfly::JobEndpoint do
86
86
  response.status.should == 404
87
87
  end
88
88
 
89
+ it "should return a 404 if the datastore raises bad uid" do
90
+ @job.should_receive(:apply).and_raise(Dragonfly::DataStorage::BadUID)
91
+ response = make_request(@job)
92
+ response.status.should == 404
93
+ end
94
+
89
95
  describe "ETag" do
90
96
  it "should return an ETag" do
91
97
  response = make_request(@job)
@@ -22,7 +22,7 @@ describe Dragonfly::Server do
22
22
  @app.destroy(@uid)
23
23
  end
24
24
 
25
- describe "successful urls" do
25
+ describe "successful requests" do
26
26
  before(:each) do
27
27
  @server.url_format = '/media/:job/:name.:format'
28
28
  end
@@ -59,74 +59,90 @@ describe Dragonfly::Server do
59
59
  response.status.should == 200
60
60
  response.body.should == 'eggs'
61
61
  end
62
- end
62
+
63
+ it "should return a cacheable response" do
64
+ url = "/media/#{@job.serialize}"
65
+ cache = Rack::Cache.new(@server, :entitystore => 'heap:/')
66
+ response = request(cache, url)
67
+ response.status.should == 200
68
+ response.headers['X-Rack-Cache'].should == "miss, store"
69
+ response = request(cache, url)
70
+ response.status.should == 200
71
+ response.headers['X-Rack-Cache'].should == "fresh"
72
+ end
63
73
 
64
- it "should return successfully even if the job is in the query string" do
65
- @server.url_format = '/'
66
- url = "/?job=#{@job.serialize}"
67
- response = request(@server, url)
68
- response.status.should == 200
69
- response.body.should == 'HELLO THERE'
74
+ it "should return successfully even if the job is in the query string" do
75
+ @server.url_format = '/'
76
+ url = "/?job=#{@job.serialize}"
77
+ response = request(@server, url)
78
+ response.status.should == 200
79
+ response.body.should == 'HELLO THERE'
80
+ end
70
81
  end
71
82
 
72
- it "should return a 400 if no sha given but protection on" do
73
- @server.protect_from_dos_attacks = true
74
- url = "/media/#{@job.serialize}"
75
- response = request(@server, url)
76
- response.status.should == 400
77
- end
83
+ describe "unsuccessful requests" do
84
+ it "should return a 400 if no sha given but protection on" do
85
+ @server.protect_from_dos_attacks = true
86
+ url = "/media/#{@job.serialize}"
87
+ response = request(@server, url)
88
+ response.status.should == 400
89
+ end
78
90
 
79
- it "should return a 400 if wrong sha given and protection on" do
80
- @server.protect_from_dos_attacks = true
81
- url = "/media/#{@job.serialize}?sha=asdfs"
82
- response = request(@server, url)
83
- response.status.should == 400
84
- end
85
-
86
- ['/media', '/media/'].each do |url|
87
- it "should return a 404 when no job given, e.g. #{url.inspect}" do
91
+ it "should return a 400 if wrong sha given and protection on" do
92
+ @server.protect_from_dos_attacks = true
93
+ url = "/media/#{@job.serialize}?sha=asdfs"
88
94
  response = request(@server, url)
95
+ response.status.should == 400
96
+ end
97
+
98
+ ['/media', '/media/'].each do |url|
99
+ it "should return a 404 when no job given, e.g. #{url.inspect}" do
100
+ response = request(@server, url)
101
+ response.status.should == 404
102
+ response.body.should == 'Not found'
103
+ response.content_type.should == 'text/plain'
104
+ response.headers['X-Cascade'].should == 'pass'
105
+ end
106
+ end
107
+
108
+ it "should return a 404 when the url matches but doesn't correspond to a job" do
109
+ response = request(@server, '/media/sadhfasdfdsfsdf')
89
110
  response.status.should == 404
90
111
  response.body.should == 'Not found'
91
112
  response.content_type.should == 'text/plain'
92
- response.headers['X-Cascade'].should == 'pass'
113
+ response.headers['X-Cascade'].should be_nil
93
114
  end
94
- end
95
-
96
- it "should return a 404 when the url matches but doesn't correspond to a job" do
97
- response = request(@server, '/media/sadhfasdfdsfsdf')
98
- response.status.should == 404
99
- response.body.should == 'Not found'
100
- response.content_type.should == 'text/plain'
101
- response.headers['X-Cascade'].should be_nil
102
- end
103
115
 
104
- it "should return a 404 when the url isn't known at all" do
105
- response = request(@server, '/jfasd/dsfa')
106
- response.status.should == 404
107
- response.body.should == 'Not found'
108
- response.content_type.should == 'text/plain'
109
- response.headers['X-Cascade'].should == 'pass'
110
- end
116
+ it "should return a 404 when the url isn't known at all" do
117
+ response = request(@server, '/jfasd/dsfa')
118
+ response.status.should == 404
119
+ response.body.should == 'Not found'
120
+ response.content_type.should == 'text/plain'
121
+ response.headers['X-Cascade'].should == 'pass'
122
+ end
111
123
 
112
- it "should return a 404 when the url is a well-encoded but bad array" do
113
- url = "/media/#{Dragonfly::Serializer.marshal_encode([[:egg, {:some => 'args'}]])}"
114
- response = request(@server, url)
115
- response.status.should == 404
116
- response.body.should == 'Not found'
117
- response.content_type.should == 'text/plain'
118
- response.headers['X-Cascade'].should be_nil
119
- end
120
-
121
- it "should return a cacheable response" do
122
- url = "/media/#{@job.serialize}"
123
- cache = Rack::Cache.new(@server, :entitystore => 'heap:/')
124
- response = request(cache, url)
125
- response.status.should == 200
126
- response.headers['X-Rack-Cache'].should == "miss, store"
127
- response = request(cache, url)
128
- response.status.should == 200
129
- response.headers['X-Rack-Cache'].should == "fresh"
124
+ it "should return a 404 when the url is a well-encoded but bad array" do
125
+ url = "/media/#{Dragonfly::Serializer.marshal_encode([[:egg, {:some => 'args'}]])}"
126
+ response = request(@server, url)
127
+ response.status.should == 404
128
+ response.body.should == 'Not found'
129
+ response.content_type.should == 'text/plain'
130
+ response.headers['X-Cascade'].should be_nil
131
+ end
132
+
133
+ it "should return a 403 Forbidden when someone uses fetch_file" do
134
+ response = request(@server, "/media/#{@app.fetch_file('/some/file.txt').serialize}")
135
+ response.status.should == 403
136
+ response.body.should == 'Forbidden'
137
+ response.content_type.should == 'text/plain'
138
+ end
139
+
140
+ it "should return a 403 Forbidden when someone uses fetch_url" do
141
+ response = request(@server, "/media/#{@app.fetch_url('some.url').serialize}")
142
+ response.status.should == 403
143
+ response.body.should == 'Forbidden'
144
+ response.content_type.should == 'text/plain'
145
+ end
130
146
  end
131
147
 
132
148
  end
@@ -45,3 +45,13 @@ RSpec::Matchers.define :have_size do |size|
45
45
  image_properties(given)[:size].should == size
46
46
  end
47
47
  end
48
+
49
+ RSpec::Matchers.define :equal_image do |other|
50
+ match do |given|
51
+ image_data = given.open.read
52
+ other_image_data = other.open.read
53
+ given.close
54
+ other.close
55
+ image_data == other_image_data
56
+ end
57
+ end
@@ -0,0 +1,49 @@
1
+ require "rubygems"
2
+ require "bundler/setup"
3
+ $:.unshift(File.expand_path('../../lib', __FILE__))
4
+ require 'dragonfly'
5
+
6
+ ROOT = (File.expand_path('../..', __FILE__))
7
+ APP = Dragonfly[:images].configure_with(:imagemagick).configure do |c|
8
+ c.url_format = '/images/:job'
9
+ c.allow_fetch_file = true
10
+ end
11
+
12
+ def row(geometry)
13
+ image = APP.fetch_file(ROOT + '/samples/beach.png').thumb('100x100#')
14
+ %(<tr>
15
+ <th>#{geometry}</th>
16
+ <th><img src="#{image.url}" /></th>
17
+ <th><img src="#{image.thumb(geometry).url}" /></th>
18
+ </tr>)
19
+ end
20
+
21
+ use Dragonfly::Middleware, :images
22
+ run proc{[
23
+ 200,
24
+ {'Content-Type' => 'text/html'},
25
+ [%(
26
+ <table>
27
+ <tr>
28
+ <th>Geometry</th>
29
+ <th>Original(100x100)</th>
30
+ <th>Thumb</th>
31
+ </tr>
32
+ #{[
33
+ row('80x60'),
34
+ row('80x60!'),
35
+ row('80x'),
36
+ row('x60'),
37
+ row('80x60>'),
38
+ row('80x60<'),
39
+ row('50x50%'),
40
+ row('80x60^'),
41
+ row('2000@'),
42
+ row('80x60#'),
43
+ row('80x60#ne'),
44
+ row('80x60se'),
45
+ row('80x60+5+35')
46
+ ].join}
47
+ </table>
48
+ )]
49
+ ]}
@@ -71,7 +71,7 @@ ul.main_files li {
71
71
 
72
72
  ul.main_files li a {
73
73
  margin: 0;
74
- padding:4px 12px;
74
+ padding:3px 12px;
75
75
  display:block;
76
76
  cursor:pointer;
77
77
  }
@@ -61,7 +61,7 @@
61
61
  ['GeneralUsage', 'General usage'],
62
62
  ['Rails2', 'Rails 2.3'],
63
63
  ['Rails3', 'Rails 3'],
64
- ['Models', 'Models (ActiveRecord, ActiveModel, etc.)'],
64
+ ['Models', 'Models'],
65
65
  ['ImageMagick', 'ImageMagick'],
66
66
  ['Rack', 'Rack'],
67
67
  ['Sinatra', 'Sinatra'],
metadata CHANGED
@@ -2,7 +2,7 @@
2
2
  name: dragonfly
3
3
  version: !ruby/object:Gem::Version
4
4
  prerelease:
5
- version: 0.9.0
5
+ version: 0.9.1
6
6
  platform: ruby
7
7
  authors:
8
8
  - Mark Evans
@@ -10,7 +10,7 @@ autorequire:
10
10
  bindir: bin
11
11
  cert_chain: []
12
12
 
13
- date: 2011-04-27 00:00:00 +01:00
13
+ date: 2011-05-11 00:00:00 +01:00
14
14
  default_executable:
15
15
  dependencies:
16
16
  - !ruby/object:Gem::Dependency
@@ -388,6 +388,7 @@ files:
388
388
  - spec/support/argument_matchers.rb
389
389
  - spec/support/image_matchers.rb
390
390
  - spec/support/simple_matchers.rb
391
+ - spec/test_imagemagick.ru
391
392
  - yard/handlers/configurable_attr_handler.rb
392
393
  - yard/setup.rb
393
394
  - yard/templates/default/fulldoc/html/css/common.css
@@ -408,7 +409,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
408
409
  requirements:
409
410
  - - ">="
410
411
  - !ruby/object:Gem::Version
411
- hash: 3169869758436759526
412
+ hash: 3227660044973148793
412
413
  segments:
413
414
  - 0
414
415
  version: "0"