dragonfly 0.7.5 → 0.7.6
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 +3 -0
- data/History.md +23 -0
- data/README.md +1 -1
- data/VERSION +1 -1
- data/dragonfly.gemspec +3 -3
- data/extra_docs/Configuration.md +6 -0
- data/extra_docs/Models.md +15 -0
- data/extra_docs/Rails2.md +1 -1
- data/extra_docs/Rails3.md +1 -1
- data/extra_docs/URLs.md +55 -2
- data/lib/dragonfly/active_model_extensions/attachment.rb +22 -9
- data/lib/dragonfly/analysis/r_magick_analyser.rb +0 -1
- data/lib/dragonfly/app.rb +12 -9
- data/lib/dragonfly/configurable.rb +4 -2
- data/lib/dragonfly/data_storage/file_data_store.rb +29 -20
- data/lib/dragonfly/data_storage/mongo_data_store.rb +7 -3
- data/lib/dragonfly/data_storage/s3data_store.rb +1 -1
- data/lib/dragonfly/job.rb +94 -17
- data/lib/dragonfly/job_endpoint.rb +1 -3
- data/lib/dragonfly/r_magick_utils.rb +2 -0
- data/lib/dragonfly/response.rb +82 -0
- data/lib/dragonfly/routed_endpoint.rb +1 -3
- data/lib/dragonfly/simple_endpoint.rb +2 -3
- data/lib/dragonfly/temp_object.rb +7 -6
- data/spec/dragonfly/active_model_extensions/model_spec.rb +52 -0
- data/spec/dragonfly/app_spec.rb +41 -8
- data/spec/dragonfly/configurable_spec.rb +2 -4
- data/spec/dragonfly/data_storage/file_data_store_spec.rb +20 -7
- data/spec/dragonfly/data_storage/s3_data_store_spec.rb +25 -9
- data/spec/dragonfly/job_endpoint_spec.rb +95 -41
- data/spec/dragonfly/job_spec.rb +168 -0
- data/spec/dragonfly/temp_object_spec.rb +39 -25
- data/spec/simple_matchers.rb +2 -2
- metadata +4 -4
- data/lib/dragonfly/endpoint.rb +0 -43
@@ -11,6 +11,10 @@ module Dragonfly
|
|
11
11
|
configurable_attr :port
|
12
12
|
configurable_attr :database, 'dragonfly'
|
13
13
|
|
14
|
+
# Mongo gem deprecated ObjectID in favour of ObjectId
|
15
|
+
OBJECT_ID = defined?(BSON::ObjectId) ? BSON::ObjectId : BSON::ObjectID
|
16
|
+
INVALID_OBJECT_ID = defined?(BSON::InvalidObjectId) ? BSON::InvalidObjectId : BSON::InvalidObjectID
|
17
|
+
|
14
18
|
def initialize(opts={})
|
15
19
|
self.host = opts[:host]
|
16
20
|
self.port = opts[:port]
|
@@ -32,13 +36,13 @@ module Dragonfly
|
|
32
36
|
grid_io.read,
|
33
37
|
extra
|
34
38
|
]
|
35
|
-
rescue Mongo::GridFileNotFound,
|
39
|
+
rescue Mongo::GridFileNotFound, INVALID_OBJECT_ID => e
|
36
40
|
raise DataNotFound, "#{e} - #{uid}"
|
37
41
|
end
|
38
42
|
|
39
43
|
def destroy(uid)
|
40
44
|
grid.delete(bson_id(uid))
|
41
|
-
rescue Mongo::GridFileNotFound,
|
45
|
+
rescue Mongo::GridFileNotFound, INVALID_OBJECT_ID => e
|
42
46
|
raise DataNotFound, "#{e} - #{uid}"
|
43
47
|
end
|
44
48
|
|
@@ -57,7 +61,7 @@ module Dragonfly
|
|
57
61
|
end
|
58
62
|
|
59
63
|
def bson_id(uid)
|
60
|
-
|
64
|
+
OBJECT_ID.from_string(uid)
|
61
65
|
end
|
62
66
|
|
63
67
|
end
|
@@ -32,7 +32,7 @@ module Dragonfly
|
|
32
32
|
end
|
33
33
|
|
34
34
|
def store(temp_object, opts={})
|
35
|
-
uid = generate_uid(temp_object.name || 'file')
|
35
|
+
uid = opts[:path] || generate_uid(temp_object.name || 'file')
|
36
36
|
ensure_initialized
|
37
37
|
object = use_filesystem ? temp_object.file : temp_object.data
|
38
38
|
extra_data = temp_object.attributes
|
data/lib/dragonfly/job.rb
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
require 'forwardable'
|
2
2
|
require 'digest/sha1'
|
3
|
+
require 'base64'
|
3
4
|
|
4
5
|
module Dragonfly
|
5
6
|
class Job
|
@@ -15,7 +16,7 @@ module Dragonfly
|
|
15
16
|
class IncorrectSHA < StandardError; end
|
16
17
|
|
17
18
|
extend Forwardable
|
18
|
-
def_delegators :result, :data, :file, :tempfile, :path, :to_file, :size, :ext, :name, :meta, :format, :_format
|
19
|
+
def_delegators :result, :data, :file, :tempfile, :path, :to_file, :size, :ext, :name, :name=, :basename, :meta, :meta=, :format, :_format
|
19
20
|
|
20
21
|
class Step
|
21
22
|
|
@@ -37,7 +38,15 @@ module Dragonfly
|
|
37
38
|
def initialize(*args)
|
38
39
|
@args = args
|
39
40
|
end
|
41
|
+
|
40
42
|
attr_reader :args
|
43
|
+
|
44
|
+
def update_temp_object(job, content, extra)
|
45
|
+
temp_object = TempObject.new(content, job.temp_object.attributes)
|
46
|
+
temp_object.extract_attributes_from(extra) if extra
|
47
|
+
job.temp_object = temp_object
|
48
|
+
end
|
49
|
+
|
41
50
|
def inspect
|
42
51
|
"#{self.class.step_name}(#{args.map{|a| a.inspect }.join(', ')})"
|
43
52
|
end
|
@@ -49,7 +58,7 @@ module Dragonfly
|
|
49
58
|
end
|
50
59
|
def apply(job)
|
51
60
|
content, extra = job.app.datastore.retrieve(uid)
|
52
|
-
job.temp_object = TempObject.new(content,
|
61
|
+
job.temp_object = TempObject.new(content, extra)
|
53
62
|
end
|
54
63
|
end
|
55
64
|
|
@@ -62,11 +71,8 @@ module Dragonfly
|
|
62
71
|
end
|
63
72
|
def apply(job)
|
64
73
|
raise NothingToProcess, "Can't process because temp object has not been initialized. Need to fetch first?" unless job.temp_object
|
65
|
-
|
66
|
-
job
|
67
|
-
job.app.processor.process(old, name, *arguments),
|
68
|
-
old.attributes
|
69
|
-
)
|
74
|
+
content, extra = job.app.processor.process(job.temp_object, name, *arguments)
|
75
|
+
update_temp_object(job, content, extra)
|
70
76
|
end
|
71
77
|
end
|
72
78
|
|
@@ -79,18 +85,16 @@ module Dragonfly
|
|
79
85
|
end
|
80
86
|
def apply(job)
|
81
87
|
raise NothingToEncode, "Can't encode because temp object has not been initialized. Need to fetch first?" unless job.temp_object
|
82
|
-
|
83
|
-
job
|
84
|
-
|
85
|
-
old.attributes.merge(:format => format)
|
86
|
-
)
|
88
|
+
content, extra = job.app.encoder.encode(job.temp_object, format, *arguments)
|
89
|
+
update_temp_object(job, content, extra)
|
90
|
+
job.temp_object.format = format
|
87
91
|
end
|
88
92
|
end
|
89
93
|
|
90
94
|
class Generate < Step
|
91
95
|
def apply(job)
|
92
96
|
content, extra = job.app.generator.generate(*args)
|
93
|
-
job.temp_object = TempObject.new(content,
|
97
|
+
job.temp_object = TempObject.new(content, extra)
|
94
98
|
end
|
95
99
|
end
|
96
100
|
|
@@ -210,6 +214,8 @@ module Dragonfly
|
|
210
214
|
new_job
|
211
215
|
end
|
212
216
|
|
217
|
+
# Applying, etc.
|
218
|
+
|
213
219
|
def apply
|
214
220
|
pending_steps.each{|step| step.apply(self) }
|
215
221
|
self.next_step_index = steps.length
|
@@ -235,6 +241,8 @@ module Dragonfly
|
|
235
241
|
}
|
236
242
|
end
|
237
243
|
|
244
|
+
# Serializing, etc.
|
245
|
+
|
238
246
|
def serialize
|
239
247
|
Serializer.marshal_encode(to_a)
|
240
248
|
end
|
@@ -258,6 +266,18 @@ module Dragonfly
|
|
258
266
|
end
|
259
267
|
end
|
260
268
|
|
269
|
+
# URLs, etc.
|
270
|
+
|
271
|
+
def url(*args)
|
272
|
+
app.url_for(self, *args) unless steps.empty?
|
273
|
+
end
|
274
|
+
|
275
|
+
def b64_data
|
276
|
+
"data:#{resolve_mime_type};base64,#{Base64.encode64(data)}"
|
277
|
+
end
|
278
|
+
|
279
|
+
# to_stuff...
|
280
|
+
|
261
281
|
def to_app
|
262
282
|
JobEndpoint.new(self)
|
263
283
|
end
|
@@ -266,8 +286,8 @@ module Dragonfly
|
|
266
286
|
to_app.call(env)
|
267
287
|
end
|
268
288
|
|
269
|
-
def
|
270
|
-
|
289
|
+
def to_path
|
290
|
+
"/#{serialize}"
|
271
291
|
end
|
272
292
|
|
273
293
|
def to_fetched_job(uid)
|
@@ -277,8 +297,59 @@ module Dragonfly
|
|
277
297
|
new_job
|
278
298
|
end
|
279
299
|
|
280
|
-
|
281
|
-
|
300
|
+
# Step inspection
|
301
|
+
|
302
|
+
def fetch_step
|
303
|
+
last_step_of_type(Fetch)
|
304
|
+
end
|
305
|
+
|
306
|
+
def uid
|
307
|
+
step = fetch_step
|
308
|
+
step.uid if step
|
309
|
+
end
|
310
|
+
|
311
|
+
def uid_basename
|
312
|
+
File.basename(uid, '.*') if uid
|
313
|
+
end
|
314
|
+
|
315
|
+
def uid_extname
|
316
|
+
File.extname(uid) if uid
|
317
|
+
end
|
318
|
+
|
319
|
+
def generate_step
|
320
|
+
last_step_of_type(Generate)
|
321
|
+
end
|
322
|
+
|
323
|
+
def fetch_file_step
|
324
|
+
last_step_of_type(FetchFile)
|
325
|
+
end
|
326
|
+
|
327
|
+
def process_steps
|
328
|
+
steps.select{|s| s.is_a?(Process) }
|
329
|
+
end
|
330
|
+
|
331
|
+
def encode_step
|
332
|
+
last_step_of_type(Encode)
|
333
|
+
end
|
334
|
+
|
335
|
+
def encoded_format
|
336
|
+
step = encode_step
|
337
|
+
step.format if step
|
338
|
+
end
|
339
|
+
|
340
|
+
def encoded_extname
|
341
|
+
format = encoded_format
|
342
|
+
".#{format}" if format
|
343
|
+
end
|
344
|
+
|
345
|
+
# Misc
|
346
|
+
|
347
|
+
def store(opts={})
|
348
|
+
app.store(result, opts)
|
349
|
+
end
|
350
|
+
|
351
|
+
def resolve_mime_type
|
352
|
+
app.resolve_mime_type(result)
|
282
353
|
end
|
283
354
|
|
284
355
|
def inspect
|
@@ -290,5 +361,11 @@ module Dragonfly
|
|
290
361
|
attr_writer :steps
|
291
362
|
attr_accessor :next_step_index
|
292
363
|
|
364
|
+
private
|
365
|
+
|
366
|
+
def last_step_of_type(type)
|
367
|
+
steps.select{|s| s.is_a?(type) }.last
|
368
|
+
end
|
369
|
+
|
293
370
|
end
|
294
371
|
end
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require 'uri'
|
2
|
+
|
3
|
+
module Dragonfly
|
4
|
+
class Response
|
5
|
+
|
6
|
+
DEFAULT_FILENAME = proc{|job, request|
|
7
|
+
if job.basename
|
8
|
+
extname = job.encoded_extname || (".#{job.ext}" if job.ext)
|
9
|
+
"#{job.basename}#{extname}"
|
10
|
+
end
|
11
|
+
}
|
12
|
+
|
13
|
+
def initialize(job, env)
|
14
|
+
@job, @env = job, env
|
15
|
+
@app = @job.app
|
16
|
+
end
|
17
|
+
|
18
|
+
def to_response
|
19
|
+
if etag_matches?
|
20
|
+
# Not Modified
|
21
|
+
[304, cache_headers, []]
|
22
|
+
else
|
23
|
+
# Success
|
24
|
+
[200, success_headers.merge(cache_headers), job.result]
|
25
|
+
end
|
26
|
+
rescue DataStorage::DataNotFound => e
|
27
|
+
[404, {"Content-Type" => 'text/plain'}, [e.message]]
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
attr_reader :job, :env, :app
|
33
|
+
|
34
|
+
def request
|
35
|
+
@request ||= Rack::Request.new(env)
|
36
|
+
end
|
37
|
+
|
38
|
+
def cache_headers
|
39
|
+
{
|
40
|
+
"Cache-Control" => "public, max-age=#{app.cache_duration}",
|
41
|
+
"ETag" => %("#{job.unique_signature}")
|
42
|
+
}
|
43
|
+
end
|
44
|
+
|
45
|
+
def etag_matches?
|
46
|
+
if_none_match = env['HTTP_IF_NONE_MATCH']
|
47
|
+
if if_none_match
|
48
|
+
if_none_match.tr!('"','')
|
49
|
+
if_none_match.split(',').include?(job.unique_signature) || if_none_match == '*'
|
50
|
+
else
|
51
|
+
false
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def success_headers
|
56
|
+
{
|
57
|
+
"Content-Type" => job.resolve_mime_type,
|
58
|
+
"Content-Length" => job.size.to_s
|
59
|
+
}.merge(content_disposition_header)
|
60
|
+
end
|
61
|
+
|
62
|
+
def content_disposition_header
|
63
|
+
parts = []
|
64
|
+
parts << content_disposition if content_disposition
|
65
|
+
parts << %(filename="#{URI.encode(filename)}") if filename
|
66
|
+
parts.any? ? {"Content-Disposition" => parts.join('; ')} : {}
|
67
|
+
end
|
68
|
+
|
69
|
+
def content_disposition
|
70
|
+
@content_disposition ||= evaluate(app.content_disposition)
|
71
|
+
end
|
72
|
+
|
73
|
+
def filename
|
74
|
+
@filename ||= evaluate(app.content_filename)
|
75
|
+
end
|
76
|
+
|
77
|
+
def evaluate(attribute)
|
78
|
+
attribute.respond_to?(:call) ? attribute.call(job, request) : attribute
|
79
|
+
end
|
80
|
+
|
81
|
+
end
|
82
|
+
end
|
@@ -1,8 +1,6 @@
|
|
1
1
|
module Dragonfly
|
2
2
|
class RoutedEndpoint
|
3
3
|
|
4
|
-
include Endpoint
|
5
|
-
|
6
4
|
class NoRoutingParams < RuntimeError; end
|
7
5
|
|
8
6
|
def initialize(app, &block)
|
@@ -13,7 +11,7 @@ module Dragonfly
|
|
13
11
|
def call(env)
|
14
12
|
params = symbolize_keys_of Rack::Request.new(env).params
|
15
13
|
job = @block.call(params.merge(routing_params(env)), @app)
|
16
|
-
|
14
|
+
Response.new(job, env).to_response
|
17
15
|
rescue Job::NoSHAGiven => e
|
18
16
|
[400, {"Content-Type" => 'text/plain'}, ["You need to give a SHA parameter"]]
|
19
17
|
rescue Job::IncorrectSHA => e
|
@@ -1,7 +1,6 @@
|
|
1
1
|
module Dragonfly
|
2
2
|
class SimpleEndpoint
|
3
3
|
|
4
|
-
include Endpoint
|
5
4
|
include Loggable
|
6
5
|
|
7
6
|
# Instance methods
|
@@ -20,7 +19,7 @@ module Dragonfly
|
|
20
19
|
else
|
21
20
|
job = Job.from_path(request.path_info, app)
|
22
21
|
job.validate_sha!(request['s']) if app.protect_from_dos_attacks
|
23
|
-
|
22
|
+
Response.new(job, env).to_response
|
24
23
|
end
|
25
24
|
rescue Serializer::BadString, Job::InvalidArray => e
|
26
25
|
log.warn(e.message)
|
@@ -47,7 +46,7 @@ module Dragonfly
|
|
47
46
|
`-.~--' |=| '--~.-'
|
48
47
|
_~-.~'" /|=|\\ "'~.-~_
|
49
48
|
( ./ |=| \\. )
|
50
|
-
`~~`"` |=| `"'
|
49
|
+
`~~`"` |=| `"'ME"
|
51
50
|
|-|
|
52
51
|
<->
|
53
52
|
V
|
@@ -41,6 +41,7 @@ module Dragonfly
|
|
41
41
|
# Instance Methods
|
42
42
|
|
43
43
|
def initialize(obj, opts={})
|
44
|
+
opts ||= {} # in case it's nil
|
44
45
|
initialize_from_object!(obj)
|
45
46
|
validate_options!(opts)
|
46
47
|
extract_attributes_from(opts)
|
@@ -57,6 +58,7 @@ module Dragonfly
|
|
57
58
|
@tempfile = initialized_tempfile
|
58
59
|
when :data
|
59
60
|
@tempfile = Tempfile.new('dragonfly')
|
61
|
+
@tempfile.binmode
|
60
62
|
@tempfile.write(initialized_data)
|
61
63
|
when :file
|
62
64
|
@tempfile = copy_to_tempfile(initialized_file.path)
|
@@ -89,8 +91,9 @@ module Dragonfly
|
|
89
91
|
end
|
90
92
|
end
|
91
93
|
|
92
|
-
|
94
|
+
attr_accessor :name, :format
|
93
95
|
alias _format format
|
96
|
+
attr_writer :meta
|
94
97
|
|
95
98
|
def meta
|
96
99
|
@meta ||= {}
|
@@ -138,9 +141,9 @@ module Dragonfly
|
|
138
141
|
end
|
139
142
|
|
140
143
|
def extract_attributes_from(hash)
|
141
|
-
self.name = hash.delete(:name)
|
142
|
-
self.
|
143
|
-
self.
|
144
|
+
self.name = hash.delete(:name) unless hash[:name].blank?
|
145
|
+
self.format = hash.delete(:format) unless hash[:format].blank?
|
146
|
+
self.meta.merge!(hash.delete(:meta)) unless hash[:meta].blank?
|
144
147
|
end
|
145
148
|
|
146
149
|
def inspect
|
@@ -160,8 +163,6 @@ module Dragonfly
|
|
160
163
|
|
161
164
|
private
|
162
165
|
|
163
|
-
attr_writer :name, :meta, :format
|
164
|
-
|
165
166
|
def initialize_from_object!(obj)
|
166
167
|
case obj
|
167
168
|
when TempObject
|
@@ -615,6 +615,58 @@ describe Item do
|
|
615
615
|
}.should raise_error(NoMethodError)
|
616
616
|
end
|
617
617
|
end
|
618
|
+
|
619
|
+
describe "setting things on the attachment" do
|
620
|
+
|
621
|
+
before(:each) do
|
622
|
+
@item = Item.new
|
623
|
+
end
|
624
|
+
|
625
|
+
describe "name" do
|
626
|
+
before(:each) do
|
627
|
+
@item.preview_image = "Hello"
|
628
|
+
@item.preview_image.name = 'hello.there'
|
629
|
+
end
|
630
|
+
it "should allow for setting the name" do
|
631
|
+
@item.preview_image.name.should == 'hello.there'
|
632
|
+
end
|
633
|
+
it "should update the magic attribute" do
|
634
|
+
@item.preview_image_name.should == 'hello.there'
|
635
|
+
end
|
636
|
+
it "should return the name" do
|
637
|
+
(@item.preview_image.name = 'no.silly').should == 'no.silly'
|
638
|
+
end
|
639
|
+
end
|
640
|
+
|
641
|
+
describe "meta" do
|
642
|
+
before(:each) do
|
643
|
+
@item.preview_image = "Hello all"
|
644
|
+
@item.preview_image.meta = {:slime => 'balls'}
|
645
|
+
end
|
646
|
+
it "should allow for setting the meta" do
|
647
|
+
@item.preview_image.meta.should == {:slime => 'balls'}
|
648
|
+
end
|
649
|
+
it "should allow for updating the meta" do
|
650
|
+
@item.preview_image.meta[:numb] = 'nuts'
|
651
|
+
@item.preview_image.meta.should == {:slime => 'balls', :numb => 'nuts'}
|
652
|
+
end
|
653
|
+
it "should return the meta" do
|
654
|
+
(@item.preview_image.meta = {:doogs => 'boogs'}).should == {:doogs => 'boogs'}
|
655
|
+
end
|
656
|
+
it "should save it correctly" do
|
657
|
+
@item.save!
|
658
|
+
item = Item.find(@item.id)
|
659
|
+
item.preview_image.meta.should include_hash(:slime => 'balls')
|
660
|
+
end
|
661
|
+
it "should store meta info about the model" do
|
662
|
+
@item.save!
|
663
|
+
meta = {:model_class => 'Item', :model_attachment => :preview_image, :slime => 'balls'}
|
664
|
+
@app.fetch(@item.preview_image_uid).meta.should == meta
|
665
|
+
end
|
666
|
+
end
|
667
|
+
|
668
|
+
end
|
669
|
+
|
618
670
|
end
|
619
671
|
|
620
672
|
describe "inheritance" do
|