dragonfly 1.0 → 1.0.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.

Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +1 -0
  3. data/.travis.yml +1 -4
  4. data/History.md +17 -0
  5. data/README.md +6 -2
  6. data/Rakefile +1 -0
  7. data/dragonfly.gemspec +17 -1
  8. data/lib/dragonfly/app.rb +10 -1
  9. data/lib/dragonfly/file_data_store.rb +1 -1
  10. data/lib/dragonfly/image_magick/plugin.rb +3 -2
  11. data/lib/dragonfly/job.rb +5 -178
  12. data/lib/dragonfly/job/fetch.rb +19 -0
  13. data/lib/dragonfly/job/fetch_file.rb +27 -0
  14. data/lib/dragonfly/job/fetch_url.rb +75 -0
  15. data/lib/dragonfly/job/generate.rb +27 -0
  16. data/lib/dragonfly/job/process.rb +27 -0
  17. data/lib/dragonfly/job/step.rb +44 -0
  18. data/lib/dragonfly/model/attachment.rb +0 -2
  19. data/lib/dragonfly/model/class_methods.rb +5 -0
  20. data/lib/dragonfly/rails/images.rb +1 -0
  21. data/lib/dragonfly/response.rb +11 -2
  22. data/lib/dragonfly/routed_endpoint.rb +15 -5
  23. data/lib/dragonfly/serializer.rb +2 -2
  24. data/lib/dragonfly/shell.rb +28 -5
  25. data/lib/dragonfly/version.rb +1 -1
  26. data/spec/dragonfly/file_data_store_spec.rb +29 -32
  27. data/spec/dragonfly/job/fetch_file_spec.rb +26 -0
  28. data/spec/dragonfly/job/fetch_spec.rb +26 -0
  29. data/spec/dragonfly/job/fetch_url_spec.rb +123 -0
  30. data/spec/dragonfly/job/generate_spec.rb +28 -0
  31. data/spec/dragonfly/job/process_spec.rb +28 -0
  32. data/spec/dragonfly/job_endpoint_spec.rb +17 -0
  33. data/spec/dragonfly/job_spec.rb +0 -187
  34. data/spec/dragonfly/model/model_spec.rb +27 -12
  35. data/spec/dragonfly/routed_endpoint_spec.rb +69 -28
  36. data/spec/dragonfly/shell_spec.rb +2 -2
  37. data/spec/support/simple_matchers.rb +1 -1
  38. metadata +43 -16
@@ -0,0 +1,27 @@
1
+ require 'dragonfly/job/step'
2
+
3
+ module Dragonfly
4
+ class Job
5
+ class Generate < Step
6
+ def init
7
+ generator.update_url(job.url_attributes, *arguments) if generator.respond_to?(:update_url)
8
+ end
9
+
10
+ def name
11
+ args.first
12
+ end
13
+
14
+ def generator
15
+ @generator ||= app.get_generator(name)
16
+ end
17
+
18
+ def arguments
19
+ args[1..-1]
20
+ end
21
+
22
+ def apply
23
+ generator.call(job.content, *arguments)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ require 'dragonfly/job/step'
2
+
3
+ module Dragonfly
4
+ class Job
5
+ class Process < Step
6
+ def init
7
+ processor.update_url(job.url_attributes, *arguments) if processor.respond_to?(:update_url)
8
+ end
9
+
10
+ def name
11
+ args.first.to_sym
12
+ end
13
+
14
+ def arguments
15
+ args[1..-1]
16
+ end
17
+
18
+ def processor
19
+ @processor ||= app.get_processor(name)
20
+ end
21
+
22
+ def apply
23
+ processor.call(job.content, *arguments)
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,44 @@
1
+ module Dragonfly
2
+ class Job
3
+ class Step
4
+
5
+ class << self
6
+ # Dragonfly::Job::Fetch -> 'Fetch'
7
+ def basename
8
+ @basename ||= name.split('::').last
9
+ end
10
+ # Dragonfly::Job::Fetch -> :fetch
11
+ def step_name
12
+ @step_name ||= basename.gsub(/[A-Z]/){ "_#{$&.downcase}" }.sub('_','').to_sym
13
+ end
14
+ # Dragonfly::Job::Fetch -> 'f'
15
+ def abbreviation
16
+ @abbreviation ||= basename.scan(/[A-Z]/).join.downcase
17
+ end
18
+ end
19
+
20
+ def initialize(job, *args)
21
+ @job, @args = job, args
22
+ init
23
+ end
24
+
25
+ def init # To be overridden
26
+ end
27
+
28
+ attr_reader :job, :args
29
+
30
+ def app
31
+ job.app
32
+ end
33
+
34
+ def to_a
35
+ [self.class.abbreviation, *args]
36
+ end
37
+
38
+ def inspect
39
+ "#{self.class.step_name}(#{args.map{|a| a.inspect }.join(', ')})"
40
+ end
41
+
42
+ end
43
+ end
44
+ end
@@ -164,8 +164,6 @@ module Dragonfly
164
164
  "<Dragonfly Attachment uid=#{uid.inspect}, app=#{app.name.inspect}>"
165
165
  end
166
166
 
167
- protected
168
-
169
167
  attr_reader :job
170
168
 
171
169
  private
@@ -53,6 +53,11 @@ module Dragonfly
53
53
  dragonfly_attachments[attribute].stored?
54
54
  end
55
55
 
56
+ # Define the xxx_changed? method
57
+ define_method "#{attribute}_changed?" do
58
+ dragonfly_attachments[attribute].changed?
59
+ end
60
+
56
61
  # Define the URL setter
57
62
  define_method "#{attribute}_url=" do |url|
58
63
  unless Utils.blank?(url)
@@ -0,0 +1 @@
1
+ raise LoadError, "\n*****\ndragonfly/rails/images no longer exists! Please use the rails generator instead\n\n\trails generate dragonfly\n\nsee the documentation at http://markevans.github.io/dragonfly/rails for more details\n*****\n"
@@ -80,7 +80,7 @@ module Dragonfly
80
80
  {
81
81
  "Content-Type" => job.mime_type,
82
82
  "Content-Length" => job.size.to_s,
83
- "Content-Disposition" => (%(filename="#{URI.encode(job.name)}") if job.name)
83
+ "Content-Disposition" => filename_string
84
84
  }
85
85
  end
86
86
 
@@ -97,6 +97,15 @@ module Dragonfly
97
97
  end
98
98
  end
99
99
 
100
+ def filename_string
101
+ return unless job.name
102
+ filename = request_from_msie? ? URI.encode(job.name) : job.name
103
+ %(filename="#{filename}")
104
+ end
105
+
106
+ def request_from_msie?
107
+ env["HTTP_USER_AGENT"] =~ /MSIE/
108
+ end
109
+
100
110
  end
101
111
  end
102
-
@@ -14,12 +14,20 @@ module Dragonfly
14
14
 
15
15
  def call(env)
16
16
  params = Utils.symbolize_keys Rack::Request.new(env).params
17
- job = @block.call(params.merge(routing_params(env)), @app)
18
- Response.new(job, env).to_response
17
+ value = @block.call(params.merge(routing_params(env)), @app)
18
+ case value
19
+ when nil then plain_response(404, "Not Found")
20
+ when Job, Model::Attachment
21
+ job = value.is_a?(Model::Attachment) ? value.job : value
22
+ Response.new(job, env).to_response
23
+ else
24
+ Dragonfly.warn("can't handle return value from routed endpoint: #{value.inspect}")
25
+ plain_response(500, "Server Error")
26
+ end
19
27
  rescue Job::NoSHAGiven => e
20
- [400, {"Content-Type" => 'text/plain'}, ["You need to give a SHA parameter"]]
28
+ plain_response(400, "You need to give a SHA parameter")
21
29
  rescue Job::IncorrectSHA => e
22
- [400, {"Content-Type" => 'text/plain'}, ["The SHA parameter you gave (#{e}) is incorrect"]]
30
+ plain_response(400, "The SHA parameter you gave is incorrect")
23
31
  end
24
32
 
25
33
  def inspect
@@ -32,10 +40,12 @@ module Dragonfly
32
40
  env['rack.routing_args'] ||
33
41
  env['action_dispatch.request.path_parameters'] ||
34
42
  env['router.params'] ||
35
- env['usher.params'] ||
36
43
  env['dragonfly.params'] ||
37
44
  raise(NoRoutingParams, "couldn't find any routing parameters in env #{env.inspect}")
38
45
  end
39
46
 
47
+ def plain_response(status, message)
48
+ [status, {"Content-Type" => "text/plain"}, [message]]
49
+ end
40
50
  end
41
51
  end
@@ -31,7 +31,7 @@ module Dragonfly
31
31
  raise MaliciousString, "potentially malicious marshal string #{marshal_string.inspect}" if opts[:check_malicious] && marshal_string[/@[a-z_]/i]
32
32
  Marshal.load(marshal_string)
33
33
  rescue TypeError, ArgumentError => e
34
- raise BadString, "couldn't decode #{string} - got #{e}"
34
+ raise BadString, "couldn't marshal decode string - got #{e}"
35
35
  end
36
36
 
37
37
  def json_encode(object)
@@ -42,7 +42,7 @@ module Dragonfly
42
42
  raise BadString, "can't decode blank string" if Utils.blank?(string)
43
43
  MultiJson.decode(string)
44
44
  rescue MultiJson::DecodeError => e
45
- raise BadString, "couldn't decode #{string} - got #{e}"
45
+ raise BadString, "couldn't json decode string - got #{e}"
46
46
  end
47
47
 
48
48
  def json_b64_encode(object)
@@ -11,11 +11,7 @@ module Dragonfly
11
11
  def run(command, opts={})
12
12
  command = escape_args(command) unless opts[:escape] == false
13
13
  Dragonfly.debug("shell command: #{command}")
14
- Open3.popen3 command do |stdin, stdout, stderr, wait_thread|
15
- status = wait_thread.value
16
- raise CommandFailed, "Command failed (#{command}) with exit status #{status.exitstatus} and stderr #{stderr.read}" unless status.success?
17
- stdout.read
18
- end
14
+ run_command(command)
19
15
  end
20
16
 
21
17
  def escape_args(args)
@@ -29,5 +25,32 @@ module Dragonfly
29
25
  q + string + q
30
26
  end
31
27
 
28
+ private
29
+
30
+ # Annoyingly, Open3 seems buggy on jruby/1.8.7:
31
+ # Some versions don't yield a wait_thread in the block and
32
+ # you can't run sub-shells (if explicitly turning shell-escaping off)
33
+ if RUBY_PLATFORM == 'java' || RUBY_VERSION < '1.9'
34
+
35
+ # Unfortunately we have no control over stderr this way
36
+ def run_command(command)
37
+ result = `#{command}`
38
+ status = $?
39
+ raise CommandFailed, "Command failed (#{command}) with exit status #{status.exitstatus}" unless status.success?
40
+ result
41
+ end
42
+
43
+ else
44
+
45
+ def run_command(command)
46
+ Open3.popen3 command do |stdin, stdout, stderr, wait_thread|
47
+ status = wait_thread.value
48
+ raise CommandFailed, "Command failed (#{command}) with exit status #{status.exitstatus} and stderr #{stderr.read}" unless status.success?
49
+ stdout.read
50
+ end
51
+ end
52
+
53
+ end
54
+
32
55
  end
33
56
  end
@@ -1,3 +1,3 @@
1
1
  module Dragonfly
2
- VERSION = '1.0'
2
+ VERSION = '1.0.1'
3
3
  end
@@ -16,6 +16,13 @@ describe Dragonfly::FileDataStore do
16
16
  File.exists?(path).should be_false
17
17
  end
18
18
 
19
+ def assert_contains(dir, filepattern)
20
+ entries = Dir["#{dir}/*"]
21
+ unless entries.any?{|f| filepattern === f}
22
+ raise "expected #{entries} to contain #{filepattern}"
23
+ end
24
+ end
25
+
19
26
  let (:app) { test_app }
20
27
  let (:content) { Dragonfly::Content.new(app, 'goobydoo') }
21
28
  let (:new_content) { Dragonfly::Content.new(app) }
@@ -24,10 +31,10 @@ describe Dragonfly::FileDataStore do
24
31
 
25
32
  before(:each) do
26
33
  @data_store = Dragonfly::FileDataStore.new(:root_path => 'tmp/file_data_store_test')
34
+ FileUtils.rm_rf("#{@data_store.root_path}")
27
35
  end
28
36
 
29
37
  after(:each) do
30
- # Clean up created files
31
38
  FileUtils.rm_rf("#{@data_store.root_path}")
32
39
  end
33
40
 
@@ -38,56 +45,51 @@ describe Dragonfly::FileDataStore do
38
45
  before(:each) do
39
46
  # Set 'now' to a date in the past
40
47
  Time.stub(:now).and_return Time.mktime(1984,"may",4,14,28,1)
41
- @file_pattern_prefix_without_root = '1984/05/04/14_28_01_0_'
42
- @file_pattern_prefix = "#{@data_store.root_path}/#{@file_pattern_prefix_without_root}"
48
+ @dir = "#{@data_store.root_path}/1984/05/04"
43
49
  end
44
50
 
45
51
  it "should store the file in a folder based on date, with default filename" do
46
52
  @data_store.write(content)
47
- assert_exists "#{@file_pattern_prefix}file"
53
+ assert_contains @dir, /file$/
48
54
  end
49
55
 
50
56
  it "should use the content name if it exists" do
51
57
  content.should_receive(:name).at_least(:once).and_return('hello.there')
52
58
  @data_store.write(content)
53
- assert_exists "#{@file_pattern_prefix}hello.there"
59
+ assert_contains @dir, /hello\.there$/
54
60
  end
55
61
 
56
62
  it "should get rid of funny characters in the content name" do
57
63
  content.should_receive(:name).at_least(:once).and_return('A Picture with many spaces in its name (at 20:00 pm).png')
58
64
  @data_store.write(content)
59
- assert_exists "#{@file_pattern_prefix}A_Picture_with_many_spaces_in_its_name_at_20_00_pm_.png"
65
+ assert_contains @dir, /A_Picture_with_many_spaces_in_its_name_at_20_00_pm_\.png$/
60
66
  end
61
67
 
62
68
  it "stores meta as YAML" do
63
69
  content.meta = {'wassup' => 'doc'}
64
- @data_store.write(content)
65
- File.read("#{@file_pattern_prefix}file.meta.yml").should =~ /---\s+wassup: doc/
70
+ uid = @data_store.write(content)
71
+ File.read("#{@data_store.root_path}/#{uid}.meta.yml").should =~ /---\s+wassup: doc/
66
72
  end
67
73
 
68
74
  describe "when the filename already exists" do
69
75
 
70
76
  it "should use a different filename" do
71
- touch_file("#{@file_pattern_prefix}file")
72
- @data_store.should_receive(:disambiguate).with("#{@file_pattern_prefix}file").and_return("#{@file_pattern_prefix}file_2")
73
- @data_store.write(content)
74
- assert_exists "#{@file_pattern_prefix}file_2"
75
- end
76
-
77
- it "should use a different filename taking into account the name and ext" do
78
- content.should_receive(:name).at_least(:once).and_return('hello.png')
79
- touch_file("#{@file_pattern_prefix}hello.png")
80
- @data_store.should_receive(:disambiguate).with("#{@file_pattern_prefix}hello.png").and_return("#{@file_pattern_prefix}blah.png")
81
- @data_store.write(content)
77
+ touch_file("#{@data_store.root_path}/blah")
78
+ @data_store.should_receive(:disambiguate).with(/blah/).and_return("#{@data_store.root_path}/blah2")
79
+ @data_store.write(content, :path => 'blah')
80
+ assert_exists "#{@data_store.root_path}/blah2"
82
81
  end
83
82
 
84
83
  it "should keep trying until it finds a free filename" do
85
- touch_file("#{@file_pattern_prefix}file")
86
- touch_file("#{@file_pattern_prefix}file_2")
87
- @data_store.should_receive(:disambiguate).with("#{@file_pattern_prefix}file").and_return("#{@file_pattern_prefix}file_2")
88
- @data_store.should_receive(:disambiguate).with("#{@file_pattern_prefix}file_2").and_return("#{@file_pattern_prefix}file_3")
89
- @data_store.write(content)
90
- assert_exists "#{@file_pattern_prefix}file_3"
84
+ path1 = "#{@data_store.root_path}/blah1"
85
+ path2 = "#{@data_store.root_path}/blah2"
86
+ path3 = "#{@data_store.root_path}/blah3"
87
+ touch_file(path1)
88
+ touch_file(path2)
89
+ @data_store.should_receive(:disambiguate).with(path1).and_return(path2)
90
+ @data_store.should_receive(:disambiguate).with(path2).and_return(path3)
91
+ @data_store.write(content, :path => 'blah1')
92
+ assert_exists path3
91
93
  end
92
94
 
93
95
  describe "specifying the uid" do
@@ -107,13 +109,8 @@ describe Dragonfly::FileDataStore do
107
109
 
108
110
  describe "return value" do
109
111
 
110
- it "should return the filepath without the root of the stored file when a file name is not provided" do
111
- @data_store.write(content).should == "#{@file_pattern_prefix_without_root}file"
112
- end
113
-
114
- it "should return the filepath without the root of the stored file when a file name is provided" do
115
- content.should_receive(:name).at_least(:once).and_return('hello.you.png')
116
- @data_store.write(content).should == "#{@file_pattern_prefix_without_root}hello.you.png"
112
+ it "should return the relative path" do
113
+ @data_store.write(content).should =~ /^1984\/05\/04\/\w+$/
117
114
  end
118
115
 
119
116
  end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dragonfly::Job::FetchFile do
4
+
5
+ let (:app) { test_app }
6
+ let (:job) { Dragonfly::Job.new(app) }
7
+
8
+ before(:each) do
9
+ job.fetch_file!(SAMPLES_DIR.join('egg.png'))
10
+ end
11
+
12
+ it { job.steps.should match_steps([Dragonfly::Job::FetchFile]) }
13
+
14
+ it "should fetch the specified file when applied" do
15
+ job.size.should == 62664
16
+ end
17
+
18
+ it "should set the url_attributes" do
19
+ job.url_attributes.name.should == 'egg.png'
20
+ end
21
+
22
+ it "should set the name" do
23
+ job.name.should == 'egg.png'
24
+ end
25
+
26
+ end
@@ -0,0 +1,26 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dragonfly::Job::Fetch do
4
+
5
+ let (:app) { test_app }
6
+ let (:job) { Dragonfly::Job.new(app) }
7
+
8
+ before(:each) do
9
+ job.fetch!('some_uid')
10
+ end
11
+
12
+ it { job.steps.should match_steps([Dragonfly::Job::Fetch]) }
13
+
14
+ it "should read from the app's datastore when applied" do
15
+ app.datastore.should_receive(:read).with('some_uid').and_return ["", {}]
16
+ job.apply
17
+ end
18
+
19
+ it "raises NotFound if the datastore returns nil" do
20
+ app.datastore.should_receive(:read).and_return(nil)
21
+ expect {
22
+ job.apply
23
+ }.to raise_error(Dragonfly::Job::Fetch::NotFound)
24
+ end
25
+
26
+ end
@@ -0,0 +1,123 @@
1
+ require 'spec_helper'
2
+
3
+ describe Dragonfly::Job::FetchUrl do
4
+
5
+ let (:app) { test_app }
6
+ let (:job) { Dragonfly::Job.new(app) }
7
+
8
+ before(:each) do
9
+ stub_request(:get, %r{http://place\.com/.*}).to_return(:body => 'result!')
10
+ end
11
+
12
+ it "adds a step" do
13
+ job.fetch_url!('some.url')
14
+ job.steps.should match_steps([Dragonfly::Job::FetchUrl])
15
+ end
16
+
17
+ it "should fetch the specified url when applied" do
18
+ job.fetch_url!('http://place.com')
19
+ job.data.should == "result!"
20
+ end
21
+
22
+ it "should default to http" do
23
+ job.fetch_url!('place.com')
24
+ job.data.should == "result!"
25
+ end
26
+
27
+ it "should also work with https" do
28
+ stub_request(:get, /place.com/).to_return(:body => 'secure result!')
29
+ job.fetch_url!('https://place.com')
30
+ job.data.should == "secure result!"
31
+ end
32
+
33
+ [
34
+ "place.com",
35
+ "http://place.com",
36
+ "place.com/",
37
+ "place.com/stuff/",
38
+ "place.com/?things"
39
+ ].each do |url|
40
+ it "doesn't set the name if there isn't one, e.g. for #{url}" do
41
+ job.fetch_url!(url)
42
+ job.name.should be_nil
43
+ end
44
+
45
+ it "doesn't set the name url_attr if there isn't one, e.g. for #{url}" do
46
+ job.fetch_url!(url)
47
+ job.url_attributes.name.should be_nil
48
+ end
49
+ end
50
+
51
+ [
52
+ "place.com/dung.beetle",
53
+ "http://place.com/dung.beetle",
54
+ "place.com/stuff/dung.beetle",
55
+ "place.com/dung.beetle?morethings"
56
+ ].each do |url|
57
+ it "sets the name if there is one, e.g. for #{url}" do
58
+ job.fetch_url!(url)
59
+ job.name.should == 'dung.beetle'
60
+ end
61
+
62
+ it "sets the name url_attr if there is one, e.g. for #{url}" do
63
+ job.fetch_url!(url)
64
+ job.url_attributes.name.should == 'dung.beetle'
65
+ end
66
+ end
67
+
68
+ it "should raise an error if not found" do
69
+ stub_request(:get, "notfound.com").to_return(:status => 404, :body => "BLAH")
70
+ expect{
71
+ job.fetch_url!('notfound.com').apply
72
+ }.to raise_error(Dragonfly::Job::FetchUrl::ErrorResponse){|error|
73
+ error.status.should == 404
74
+ error.body.should == "BLAH"
75
+ }
76
+ end
77
+
78
+ it "should raise an error if server error" do
79
+ stub_request(:get, "error.com").to_return(:status => 500, :body => "BLAH")
80
+ expect{
81
+ job.fetch_url!('error.com').apply
82
+ }.to raise_error(Dragonfly::Job::FetchUrl::ErrorResponse){|error|
83
+ error.status.should == 500
84
+ error.body.should == "BLAH"
85
+ }
86
+ end
87
+
88
+ it "should follow redirects" do
89
+ stub_request(:get, "redirectme.com").to_return(:status => 302, :headers => {'Location' => 'http://ok.com'})
90
+ stub_request(:get, "ok.com").to_return(:body => "OK!")
91
+ job.fetch_url('redirectme.com').data.should == 'OK!'
92
+ end
93
+
94
+ it "follows redirects to https" do
95
+ stub_request(:get, "redirectme.com").to_return(:status => 302, :headers => {'Location' => 'https://ok.com'})
96
+ stub_request(:get, /ok.com/).to_return(:body => "OK!")
97
+ job.fetch_url('redirectme.com').data.should == 'OK!'
98
+ end
99
+
100
+ it "raises if redirecting too many times" do
101
+ stub_request(:get, "redirectme.com").to_return(:status => 302, :headers => {'Location' => 'http://redirectme-back.com'})
102
+ stub_request(:get, "redirectme-back.com").to_return(:status => 302, :headers => {'Location' => 'http://redirectme.com'})
103
+ expect {
104
+ job.fetch_url('redirectme.com').apply
105
+ }.to raise_error(Dragonfly::Job::FetchUrl::TooManyRedirects)
106
+ end
107
+
108
+ describe "data uris" do
109
+ it "accepts standard base64 encoded data uris" do
110
+ job.fetch_url!("data:text/plain;base64,aGVsbG8=\n")
111
+ job.data.should == 'hello'
112
+ job.mime_type.should == 'text/plain'
113
+ job.ext.should == 'txt'
114
+ end
115
+
116
+ it "doesn't accept other data uris" do
117
+ expect {
118
+ job.fetch_url!("data:text/html;charset=utf-8,<stuff />").apply
119
+ }.to raise_error(Dragonfly::Job::FetchUrl::CannotHandle)
120
+ end
121
+ end
122
+
123
+ end