ungulate 0.1.2 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/VERSION +1 -1
- data/features/resize_then_composite.feature +11 -0
- data/features/step_definitions/queue_steps.rb +21 -0
- data/features/step_definitions/version_steps.rb +7 -0
- data/features/superhugwatermark.png +0 -0
- data/lib/ungulate/file_upload.rb +19 -12
- data/lib/ungulate/job.rb +49 -12
- data/spec/ungulate/file_upload_spec.rb +114 -86
- data/spec/ungulate/job_spec.rb +129 -59
- metadata +44 -13
- data/spec/spec.opts +0 -3
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.1.
|
1
|
+
0.1.3
|
@@ -0,0 +1,11 @@
|
|
1
|
+
Feature: Image resize
|
2
|
+
As a site owner
|
3
|
+
I want Ungulate to resize then composite images
|
4
|
+
So that I don't have to watermark images by hand
|
5
|
+
|
6
|
+
Scenario: Run queue on image key with no path separator
|
7
|
+
Given an empty queue
|
8
|
+
And a request to resize "image.jpg" and then composite with "https://dmxno528jhfy0.cloudfront.net/superhug-watermark.png"
|
9
|
+
When I run Ungulate
|
10
|
+
Then there should be a public watermarked version
|
11
|
+
|
@@ -30,3 +30,24 @@ Given /^a request to resize "([^\"]*)" to sizes:$/ do |key, table|
|
|
30
30
|
@q.send_message(message)
|
31
31
|
end
|
32
32
|
|
33
|
+
Given /^a request to resize "([^"]*)" and then composite with "([^"]*)"$/ do |key, composite_url|
|
34
|
+
@bucket_name = "ungulate-test"
|
35
|
+
|
36
|
+
@s3 = RightAws::S3.new(ENV['AMAZON_ACCESS_KEY_ID'],
|
37
|
+
ENV['AMAZON_SECRET_ACCESS_KEY'])
|
38
|
+
@bucket = @s3.bucket @bucket_name
|
39
|
+
@bucket.put key, File.open('features/camels.jpg').read
|
40
|
+
|
41
|
+
message = {
|
42
|
+
:bucket => @bucket_name,
|
43
|
+
:key => key,
|
44
|
+
:versions => {
|
45
|
+
:watermarked => [
|
46
|
+
[:resize_to_fill, 100, 100],
|
47
|
+
[:composite, composite_url, :center_gravity, :soft_light_composite_op]
|
48
|
+
]
|
49
|
+
}
|
50
|
+
}.to_yaml
|
51
|
+
|
52
|
+
@q.send_message(message)
|
53
|
+
end
|
@@ -7,3 +7,10 @@ Then /^there should be the following public versions:$/ do |table|
|
|
7
7
|
end
|
8
8
|
end
|
9
9
|
|
10
|
+
Then /^there should be a public watermarked version$/ do
|
11
|
+
Net::HTTP.start("#{@bucket_name}.s3.amazonaws.com", 80) do |http|
|
12
|
+
response = http.get("/image_watermarked.jpg")
|
13
|
+
response.should be_a(Net::HTTPSuccess)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
Binary file
|
data/lib/ungulate/file_upload.rb
CHANGED
@@ -2,8 +2,7 @@ require 'active_support/core_ext/class/attribute_accessors'
|
|
2
2
|
class Ungulate::FileUpload
|
3
3
|
attr_accessor(
|
4
4
|
:bucket_url,
|
5
|
-
:key
|
6
|
-
:policy
|
5
|
+
:key
|
7
6
|
)
|
8
7
|
|
9
8
|
cattr_accessor(
|
@@ -21,10 +20,13 @@ class Ungulate::FileUpload
|
|
21
20
|
sqs.queue(queue_name)
|
22
21
|
end
|
23
22
|
|
24
|
-
def initialize(
|
25
|
-
self.bucket_url =
|
26
|
-
self.key =
|
27
|
-
|
23
|
+
def initialize(options = {})
|
24
|
+
self.bucket_url = options[:bucket_url]
|
25
|
+
self.key = options[:key]
|
26
|
+
|
27
|
+
if options[:policy]
|
28
|
+
self.policy = options[:policy]
|
29
|
+
end
|
28
30
|
end
|
29
31
|
|
30
32
|
def acl
|
@@ -41,11 +43,16 @@ class Ungulate::FileUpload
|
|
41
43
|
@policy_ruby['conditions'].map {|condition| condition.to_a.flatten}
|
42
44
|
end
|
43
45
|
|
44
|
-
def policy
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
46
|
+
def policy
|
47
|
+
Base64.encode64(
|
48
|
+
ActiveSupport::JSON.encode(@policy_ruby)
|
49
|
+
).gsub("\n", '')
|
50
|
+
end
|
51
|
+
|
52
|
+
def policy=(new_policy)
|
53
|
+
new_policy['expiration'] = new_policy['expiration'].utc
|
54
|
+
@policy_ruby = new_policy
|
55
|
+
policy
|
49
56
|
end
|
50
57
|
|
51
58
|
def success_action_redirect
|
@@ -55,7 +62,7 @@ class Ungulate::FileUpload
|
|
55
62
|
def signature
|
56
63
|
Base64.encode64(
|
57
64
|
OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('sha1'),
|
58
|
-
secret_access_key,
|
65
|
+
secret_access_key,
|
59
66
|
policy)
|
60
67
|
).gsub("\n", '')
|
61
68
|
end
|
data/lib/ungulate/job.rb
CHANGED
@@ -4,6 +4,7 @@ require 'RMagick'
|
|
4
4
|
require 'mime/types'
|
5
5
|
require 'yaml'
|
6
6
|
require 'active_support/core_ext'
|
7
|
+
require 'curb'
|
7
8
|
|
8
9
|
module Ungulate
|
9
10
|
class Job
|
@@ -45,15 +46,56 @@ module Ungulate
|
|
45
46
|
def processed_versions
|
46
47
|
@processed_versions ||=
|
47
48
|
versions.map do |name, instruction|
|
48
|
-
|
49
|
-
image = Magick::Image.from_blob(source).first
|
50
|
-
@logger.info "Performing #{method} with #{x}, #{y}"
|
51
|
-
processed_image = image.send(method, x, y)
|
52
|
-
image.destroy!
|
53
|
-
[name, processed_image]
|
49
|
+
[name, processed_image(source_image, instruction)]
|
54
50
|
end
|
55
51
|
end
|
56
52
|
|
53
|
+
def instruction_args(args)
|
54
|
+
args.map do |arg|
|
55
|
+
if arg.is_a?(Symbol)
|
56
|
+
"Magick::#{arg.to_s.classify}".constantize
|
57
|
+
elsif arg.respond_to?(:match) && arg.match(/^http/)
|
58
|
+
Magick::Image.from_blob(Curl::Easy.http_get(arg).body_str).first
|
59
|
+
else
|
60
|
+
arg
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def image_from_instruction(original, instruction)
|
66
|
+
method, *args = instruction
|
67
|
+
send_args = instruction_args(args)
|
68
|
+
|
69
|
+
@logger.info "Performing #{method} with #{args.join(', ')}"
|
70
|
+
original.send(method, *send_args).tap do |new_image|
|
71
|
+
original.destroy!
|
72
|
+
send_args.select {|arg| arg.is_a?(Magick::Image)}.each(&:destroy!)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def image_from_instruction_chain(original, chain)
|
77
|
+
if chain.one?
|
78
|
+
image_from_instruction(original, chain.first)
|
79
|
+
else
|
80
|
+
image_from_instruction_chain(
|
81
|
+
image_from_instruction(original, chain.shift),
|
82
|
+
chain
|
83
|
+
)
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def processed_image(original, instruction)
|
88
|
+
if instruction.first.respond_to?(:entries)
|
89
|
+
image_from_instruction_chain(original, instruction)
|
90
|
+
else
|
91
|
+
image_from_instruction(original, instruction)
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def source_image
|
96
|
+
Magick::Image.from_blob(source).first
|
97
|
+
end
|
98
|
+
|
57
99
|
def source
|
58
100
|
if @source
|
59
101
|
@source
|
@@ -92,12 +134,7 @@ module Ungulate
|
|
92
134
|
return false if notification_url.blank?
|
93
135
|
|
94
136
|
@logger.info "Sending notification to #{notification_url}"
|
95
|
-
|
96
|
-
url = URI.parse(notification_url)
|
97
|
-
|
98
|
-
http = Net::HTTP.new(url.host, url.port)
|
99
|
-
http.use_ssl = true if url.scheme == 'https'
|
100
|
-
http.start {|http| http.put(url.path, nil) }
|
137
|
+
Curl::Easy.http_put(notification_url, '')
|
101
138
|
end
|
102
139
|
|
103
140
|
def version_key(version)
|
@@ -3,54 +3,61 @@ require 'ungulate/file_upload'
|
|
3
3
|
|
4
4
|
module Ungulate
|
5
5
|
describe FileUpload do
|
6
|
+
let(:expiration) { 10.hours.from_now }
|
7
|
+
let(:bucket_url) { "http://images.bob.com/" }
|
8
|
+
let(:key) { "new-file" }
|
9
|
+
let(:access_key_id) { "asdf" }
|
10
|
+
let(:secret_access_key) { 'uV3F3YluFJax1cknvbcGwgjvx4QpvB+leU8dUj2o' }
|
11
|
+
let(:queue_name) { 'some-queue-name' }
|
12
|
+
|
6
13
|
before do
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
@policy = {
|
12
|
-
"expiration" => @expiration,
|
13
|
-
"conditions" => [
|
14
|
-
{"bucket" => "johnsmith" },
|
15
|
-
["starts-with", "$key", "user/eric/"],
|
16
|
-
{"acl" => "public-read" },
|
17
|
-
{"success_action_redirect" => "http://johnsmith.s3.amazonaws.com/successful_upload.html" },
|
18
|
-
["starts-with", "$Content-Type", "image/"],
|
19
|
-
{"x-amz-meta-uuid" => "14365123651274"},
|
20
|
-
["starts-with", "$x-amz-meta-tag", ""]
|
21
|
-
]
|
22
|
-
}
|
23
|
-
|
24
|
-
@access_key_id = "asdf"
|
25
|
-
@secret_access_key = 'uV3F3YluFJax1cknvbcGwgjvx4QpvB+leU8dUj2o'
|
26
|
-
FileUpload.access_key_id = @access_key_id
|
27
|
-
FileUpload.secret_access_key = @secret_access_key
|
28
|
-
FileUpload.queue_name = 'some-queue-name'
|
14
|
+
FileUpload.access_key_id = access_key_id
|
15
|
+
FileUpload.secret_access_key = secret_access_key
|
16
|
+
FileUpload.queue_name = queue_name
|
29
17
|
end
|
30
18
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
)
|
37
|
-
|
19
|
+
its(:access_key_id) { should == access_key_id }
|
20
|
+
its(:queue_name) { should == queue_name }
|
21
|
+
its(:secret_access_key) { should == secret_access_key }
|
22
|
+
|
23
|
+
context "policy set directly" do
|
24
|
+
let(:policy) do
|
25
|
+
{
|
26
|
+
"expiration" => expiration,
|
27
|
+
"conditions" => [
|
28
|
+
{"bucket" => "johnsmith" },
|
29
|
+
["starts-with", "$key", "user/eric/"],
|
30
|
+
{"acl" => "public-read" },
|
31
|
+
{"success_action_redirect" => "http://johnsmith.s3.amazonaws.com/successful_upload.html" },
|
32
|
+
["starts-with", "$Content-Type", "image/"],
|
33
|
+
{"x-amz-meta-uuid" => "14365123651274"},
|
34
|
+
["starts-with", "$x-amz-meta-tag", ""]
|
35
|
+
]
|
36
|
+
}
|
37
|
+
end
|
38
38
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
39
|
+
subject do
|
40
|
+
FileUpload.new(
|
41
|
+
:bucket_url => bucket_url,
|
42
|
+
:policy => policy,
|
43
|
+
:key => key
|
44
|
+
)
|
45
|
+
end
|
46
|
+
|
47
|
+
its(:acl) { should == 'public-read' }
|
48
|
+
its(:bucket_url) { should == bucket_url }
|
49
|
+
its(:conditions) { should == [
|
50
|
+
['bucket', 'johnsmith'],
|
51
|
+
['starts-with', '$key', 'user/eric/'],
|
52
|
+
['acl', 'public-read'],
|
53
|
+
['success_action_redirect', 'http://johnsmith.s3.amazonaws.com/successful_upload.html'],
|
54
|
+
['starts-with', '$Content-Type', 'image/'],
|
55
|
+
["x-amz-meta-uuid", "14365123651274"],
|
56
|
+
["starts-with", "$x-amz-meta-tag", ""]
|
57
|
+
] }
|
58
|
+
its(:key) { should == key }
|
59
|
+
its(:success_action_redirect) { should == 'http://johnsmith.s3.amazonaws.com/successful_upload.html' }
|
60
|
+
end
|
54
61
|
|
55
62
|
describe "condition" do
|
56
63
|
before do
|
@@ -58,28 +65,28 @@ module Ungulate
|
|
58
65
|
and_return([ ['colour', 'blue'], ['predicate', 'subject', 'object'] ])
|
59
66
|
end
|
60
67
|
|
61
|
-
it "
|
68
|
+
it "returns value of index 1 in a two-item array" do
|
62
69
|
subject.condition('colour').should == 'blue'
|
63
70
|
end
|
64
71
|
|
65
|
-
it "
|
72
|
+
it "copes with missing attribute" do
|
66
73
|
subject.condition('bob').should be_nil
|
67
74
|
end
|
68
75
|
end
|
69
76
|
|
70
77
|
describe "conditions" do
|
71
|
-
it "
|
78
|
+
it "memoizes" do
|
72
79
|
subject.instance_variable_set('@conditions', :cache)
|
73
80
|
subject.conditions.should == :cache
|
74
81
|
end
|
75
82
|
|
76
|
-
it "
|
83
|
+
it "converts mixed hash and array policy to nested arrays" do
|
77
84
|
subject.
|
78
|
-
instance_variable_set('@policy_ruby',
|
79
|
-
{
|
80
|
-
'conditions' => [
|
81
|
-
{'colour' => 'blue'},
|
82
|
-
['predicate', 'subject', 'object']
|
85
|
+
instance_variable_set('@policy_ruby',
|
86
|
+
{
|
87
|
+
'conditions' => [
|
88
|
+
{'colour' => 'blue'},
|
89
|
+
['predicate', 'subject', 'object']
|
83
90
|
]
|
84
91
|
})
|
85
92
|
subject.conditions.should == [ ['colour', 'blue'], ['predicate', 'subject', 'object'] ]
|
@@ -87,73 +94,94 @@ module Ungulate
|
|
87
94
|
end
|
88
95
|
|
89
96
|
describe "enqueue" do
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
97
|
+
let(:q) { stub 'queue' }
|
98
|
+
let(:job_hash) { stub('Hash', :to_yaml => :some_yaml) }
|
99
|
+
before { Ungulate::FileUpload.stub(:queue).and_return(q) }
|
100
|
+
|
101
|
+
it "queues the yamlised version of the passed job hash" do
|
102
|
+
q.should_receive(:send_message).with(:some_yaml)
|
103
|
+
Ungulate::FileUpload.enqueue(job_hash)
|
94
104
|
end
|
105
|
+
end
|
95
106
|
|
96
|
-
|
97
|
-
|
98
|
-
|
107
|
+
describe "policy" do
|
108
|
+
it "returns base64 encoded JSON version of stored policy" do
|
109
|
+
subject.instance_variable_set('@policy_ruby', :some_policy)
|
110
|
+
ActiveSupport::JSON.stub(:encode).with(:some_policy).and_return(:json)
|
111
|
+
Base64.stub(:encode64).with(:json).and_return("ENCODED\nLINE\nLINE")
|
112
|
+
subject.policy.should == "ENCODEDLINELINE"
|
99
113
|
end
|
100
114
|
end
|
101
115
|
|
102
116
|
describe "policy=" do
|
103
|
-
|
104
|
-
|
105
|
-
|
117
|
+
let(:policy) do
|
118
|
+
{
|
119
|
+
"expiration" => expiration,
|
120
|
+
"conditions" => [
|
121
|
+
{"bucket" => "johnsmith" },
|
122
|
+
["starts-with", "$key", "user/eric/"],
|
123
|
+
{"acl" => "public-read" },
|
124
|
+
{"success_action_redirect" => "http://johnsmith.s3.amazonaws.com/successful_upload.html" },
|
125
|
+
["starts-with", "$Content-Type", "image/"],
|
126
|
+
{"x-amz-meta-uuid" => "14365123651274"},
|
127
|
+
["starts-with", "$x-amz-meta-tag", ""]
|
128
|
+
]
|
129
|
+
}
|
106
130
|
end
|
107
131
|
|
108
|
-
it "
|
109
|
-
subject
|
110
|
-
|
111
|
-
ActiveSupport::JSON.stub(:encode).with(@policy).and_return(:some_json)
|
112
|
-
Base64.stub(:encode64).with(:some_json).and_return("ENCODED\nLINE\nLINE")
|
113
|
-
subject.policy = @policy
|
114
|
-
subject.policy.should == "ENCODEDLINELINE"
|
132
|
+
it "stores the ruby version for later use" do
|
133
|
+
subject.policy = policy
|
134
|
+
subject.instance_variable_get('@policy_ruby').should_not be_blank
|
115
135
|
end
|
116
136
|
|
117
|
-
it "
|
137
|
+
it "ensures the expiration is utc" do
|
118
138
|
utc_time = Time.now.utc
|
119
139
|
|
120
|
-
|
140
|
+
expiration.stub(:utc).and_return(utc_time)
|
121
141
|
Base64.stub(:encode64).and_return('')
|
122
142
|
|
123
143
|
ActiveSupport::JSON.should_receive(:encode).
|
124
144
|
with(hash_including('expiration' => utc_time)).
|
125
145
|
any_number_of_times
|
126
146
|
|
127
|
-
subject.policy =
|
147
|
+
subject.policy = policy
|
148
|
+
end
|
149
|
+
|
150
|
+
it "returns the encoded policy" do
|
151
|
+
subject.stub(:policy).and_return(:encoded_policy)
|
152
|
+
subject.send(:policy=, policy).should == :encoded_policy
|
128
153
|
end
|
129
154
|
end
|
130
155
|
|
131
156
|
describe "queue" do
|
157
|
+
let(:sqs) do
|
158
|
+
sqs = stub 'SQS'
|
159
|
+
sqs.stub(:queue).with(queue_name).and_return(:queue_instance)
|
160
|
+
sqs
|
161
|
+
end
|
162
|
+
|
163
|
+
subject { Ungulate::FileUpload.queue }
|
164
|
+
|
132
165
|
before do
|
133
|
-
|
134
|
-
|
135
|
-
FileUpload.access_key_id = 'someaccesskey'
|
136
|
-
FileUpload.secret_access_key = 'somesecret'
|
137
|
-
RightAws::SqsGen2.stub(:new).with('someaccesskey', 'somesecret').
|
166
|
+
RightAws::SqsGen2.stub(:new).
|
167
|
+
with(access_key_id, secret_access_key).
|
138
168
|
and_return(sqs)
|
139
|
-
sqs.stub(:queue).with('somequeuename').and_return(:queue_instance)
|
140
169
|
end
|
141
170
|
|
142
|
-
it
|
143
|
-
Ungulate::FileUpload.queue.should == :queue_instance
|
144
|
-
end
|
171
|
+
it { should == :queue_instance }
|
145
172
|
end
|
146
173
|
|
147
174
|
describe "signature" do
|
175
|
+
let(:sha1) { stub 'SHA1' }
|
176
|
+
|
148
177
|
before do
|
149
178
|
subject.stub(:policy).and_return(:policy)
|
150
|
-
|
151
|
-
OpenSSL::
|
152
|
-
OpenSSL::HMAC.stub(:digest).with(@sha1, @secret_access_key, :policy).and_return(:digest)
|
179
|
+
OpenSSL::Digest::Digest.stub(:new).with('sha1').and_return(sha1)
|
180
|
+
OpenSSL::HMAC.stub(:digest).with(sha1, secret_access_key, :policy).and_return(:digest)
|
153
181
|
Base64.stub(:encode64).with(:digest).and_return("str\nipme\n")
|
154
182
|
end
|
155
183
|
|
156
|
-
it "
|
184
|
+
it "returns the stripped base64 encoded digest" do
|
157
185
|
subject.signature.should == "stripme"
|
158
186
|
end
|
159
187
|
end
|
data/spec/ungulate/job_spec.rb
CHANGED
@@ -3,6 +3,11 @@ require 'ungulate/job'
|
|
3
3
|
|
4
4
|
module Ungulate
|
5
5
|
describe Job do
|
6
|
+
let(:source_image) { stub('Source Image', :destroy! => nil) }
|
7
|
+
let(:processed_image_1) { stub('Image 1', :destroy! => nil) }
|
8
|
+
let(:processed_image_2) { stub('Image 2', :destroy! => nil) }
|
9
|
+
let(:curl_easy) { stub('Curl::Easy', :body_str => body_str) }
|
10
|
+
|
6
11
|
before do
|
7
12
|
ENV['AMAZON_ACCESS_KEY_ID'] = 'test-key-id'
|
8
13
|
ENV['AMAZON_SECRET_ACCESS_KEY'] = 'test-secret'
|
@@ -14,6 +19,8 @@ module Ungulate
|
|
14
19
|
:thumb => [ :resize_to_fit, 100, 200 ],
|
15
20
|
:large => [ :resize_to_fit, 200, 300 ],
|
16
21
|
}
|
22
|
+
|
23
|
+
Curl::Easy.stub(:http_get)
|
17
24
|
end
|
18
25
|
|
19
26
|
its(:versions) { should == [] }
|
@@ -128,38 +135,136 @@ module Ungulate
|
|
128
135
|
end
|
129
136
|
end
|
130
137
|
|
131
|
-
describe :
|
138
|
+
describe :image do
|
139
|
+
let(:blob) { 'asdf' }
|
140
|
+
|
141
|
+
it "returns a Magick::Image from the source" do
|
142
|
+
subject.stub(:source).and_return(blob)
|
143
|
+
Magick::Image.should_receive(:from_blob).with(blob).and_return([source_image])
|
144
|
+
subject.source_image.should == source_image
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
describe :image_from_instruction do
|
149
|
+
let(:instruction) { [ :composite, url, arg1, arg2 ] }
|
150
|
+
let(:overlay) { stub('Overlay', :destroy! => nil) }
|
151
|
+
let(:arg1) { 1 }
|
152
|
+
let(:arg2) { 2 }
|
153
|
+
let(:url) { 'https://www.some.url/' }
|
154
|
+
let(:body_str) { 'blob' }
|
155
|
+
|
132
156
|
before do
|
133
|
-
|
134
|
-
|
157
|
+
Curl::Easy.stub(:http_get).and_return(curl_easy)
|
158
|
+
Magick::Image.stub(:from_blob).and_return([overlay])
|
159
|
+
end
|
160
|
+
|
161
|
+
context "when an argument is a URL" do
|
162
|
+
it "converts the URL to an Image" do
|
163
|
+
Curl::Easy.should_receive(:http_get).with(url).and_return(curl_easy)
|
164
|
+
Magick::Image.should_receive(:from_blob).with('blob').and_return([overlay])
|
165
|
+
source_image.should_receive(:composite).
|
166
|
+
with(overlay, arg1, arg2).
|
167
|
+
and_return(processed_image_1)
|
168
|
+
|
169
|
+
subject.image_from_instruction(source_image, instruction).
|
170
|
+
should == processed_image_1
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
context "when argument isn't a valid http URL" do
|
175
|
+
let(:url) { 'somethingwithhttpinit' }
|
176
|
+
|
177
|
+
it "passes the argument through to the method" do
|
178
|
+
source_image.should_receive(:composite).
|
179
|
+
with(url, arg1, arg2).
|
180
|
+
and_return(processed_image_1)
|
181
|
+
|
182
|
+
subject.image_from_instruction(source_image, instruction)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
context "when arguments are symbols" do
|
187
|
+
let(:arg1) { :center_gravity }
|
188
|
+
let(:arg2) { :soft_light_composite_op }
|
189
|
+
|
190
|
+
it "converts the symbols to Magick::XxXx constants" do
|
191
|
+
source_image.should_receive(:composite).
|
192
|
+
with(anything, Magick::CenterGravity, Magick::SoftLightCompositeOp).
|
193
|
+
and_return(processed_image_1)
|
194
|
+
|
195
|
+
subject.image_from_instruction(source_image, instruction)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
describe :processed_versions do
|
201
|
+
let(:versions) do
|
202
|
+
{
|
135
203
|
:large => [ :resize_to_fit, 100, 200 ],
|
136
204
|
:small => [ :resize_to_fill, 64, 64 ],
|
137
205
|
}
|
138
|
-
|
139
|
-
@job.stub(:key).and_return('someimage.jpg')
|
206
|
+
end
|
140
207
|
|
141
|
-
|
142
|
-
|
143
|
-
|
208
|
+
before do
|
209
|
+
subject.stub(:versions).and_return(versions)
|
210
|
+
subject.stub(:key).and_return('someimage.jpg')
|
211
|
+
subject.stub(:source_image).and_return(source_image)
|
144
212
|
|
145
|
-
|
146
|
-
|
213
|
+
source_image.stub(:resize_to_fit).with(100, 200).and_return(processed_image_1)
|
214
|
+
source_image.stub(:resize_to_fill).with(64, 64).and_return(processed_image_2)
|
147
215
|
end
|
148
216
|
|
149
|
-
|
150
|
-
subject
|
151
|
-
|
152
|
-
it { should include([:small, :small_image]) }
|
217
|
+
it "processes multiple versions" do
|
218
|
+
subject.processed_versions.should include([:large, processed_image_1])
|
219
|
+
subject.processed_versions.should include([:small, processed_image_2])
|
153
220
|
end
|
154
221
|
|
155
|
-
it "
|
156
|
-
|
157
|
-
|
222
|
+
it "destroys the image object" do
|
223
|
+
source_image.should_receive(:destroy!)
|
224
|
+
subject.processed_versions
|
158
225
|
end
|
159
226
|
|
160
|
-
it "
|
161
|
-
|
162
|
-
|
227
|
+
it "memoizes" do
|
228
|
+
subject.instance_variable_set('@processed_versions', :cache)
|
229
|
+
subject.processed_versions.should == :cache
|
230
|
+
end
|
231
|
+
|
232
|
+
context "with three 'method' arguments" do
|
233
|
+
let(:versions) do
|
234
|
+
{ :large => [ :some_method, 'some-value', 1, 2 ] }
|
235
|
+
end
|
236
|
+
|
237
|
+
it "passes each value" do
|
238
|
+
source_image.should_receive(:some_method).
|
239
|
+
with('some-value', 1, 2).
|
240
|
+
and_return(processed_image_1)
|
241
|
+
subject.processed_versions.should == [[:large, processed_image_1]]
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
context "with multiple instructions in a version" do
|
246
|
+
let(:versions) do
|
247
|
+
{
|
248
|
+
:large => [
|
249
|
+
[ :method_1, 'value-1' ],
|
250
|
+
[ :method_2, 'value-2' ]
|
251
|
+
]
|
252
|
+
}
|
253
|
+
end
|
254
|
+
|
255
|
+
before do
|
256
|
+
source_image.stub(:method_1).and_return(processed_image_1)
|
257
|
+
processed_image_1.stub(:method_2).and_return(processed_image_2)
|
258
|
+
end
|
259
|
+
|
260
|
+
it "chains the processing" do
|
261
|
+
subject.processed_versions.should == [[:large, processed_image_2]]
|
262
|
+
end
|
263
|
+
|
264
|
+
it "destroys intermediate images" do
|
265
|
+
processed_image_1.should_receive(:destroy!)
|
266
|
+
subject.processed_versions
|
267
|
+
end
|
163
268
|
end
|
164
269
|
end
|
165
270
|
|
@@ -233,48 +338,13 @@ module Ungulate
|
|
233
338
|
end
|
234
339
|
|
235
340
|
describe :send_notification do
|
236
|
-
|
237
|
-
|
238
|
-
let(:http_instance) { mock('Net::HTTP', :start => nil) }
|
239
|
-
let(:http_block_instance) { mock('Net::HTTP', :put => nil) }
|
341
|
+
let(:url) { 'https://some-url' }
|
240
342
|
|
241
343
|
context "notification URL provided" do
|
242
|
-
before do
|
243
|
-
subject.stub(:notification_url).and_return('http://some.host/processing_images/some/path')
|
244
|
-
end
|
245
|
-
|
246
344
|
it "should PUT to the URL" do
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
end
|
251
|
-
end
|
252
|
-
|
253
|
-
context "https notification URL provided" do
|
254
|
-
before do
|
255
|
-
subject.stub(:notification_url).and_return('https://some.host/processing_images/some/path')
|
256
|
-
http_instance.stub(:use_ssl=)
|
257
|
-
end
|
258
|
-
|
259
|
-
it "should PUT to the URL" do
|
260
|
-
Net::HTTP.should_receive(:new).with('some.host', 443).and_return(http_instance)
|
261
|
-
http_instance.should_receive(:start).and_yield(http_block_instance)
|
262
|
-
http_block_instance.should_receive(:put).with('/processing_images/some/path', nil)
|
263
|
-
end
|
264
|
-
|
265
|
-
it "should use SSL" do
|
266
|
-
Net::HTTP.should_receive(:new).with('some.host', 443).and_return(http_instance)
|
267
|
-
http_instance.should_receive(:use_ssl=).with(true)
|
268
|
-
end
|
269
|
-
end
|
270
|
-
|
271
|
-
context "notification URL not provided" do
|
272
|
-
before do
|
273
|
-
subject.stub(:notification_url).and_return(nil)
|
274
|
-
end
|
275
|
-
|
276
|
-
it "should not PUT" do
|
277
|
-
Net::HTTP.should_not_receive(:put)
|
345
|
+
subject.stub(:notification_url).and_return(url)
|
346
|
+
Curl::Easy.should_receive(:http_put).with(url, '')
|
347
|
+
subject.send_notification
|
278
348
|
end
|
279
349
|
end
|
280
350
|
end
|
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: ungulate
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 29
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 1
|
9
|
-
-
|
10
|
-
version: 0.1.
|
9
|
+
- 3
|
10
|
+
version: 0.1.3
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Andrew Bruce
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-
|
18
|
+
date: 2011-09-17 00:00:00 +01:00
|
19
19
|
default_executable: ungulate_server.rb
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -82,9 +82,25 @@ dependencies:
|
|
82
82
|
type: :runtime
|
83
83
|
version_requirements: *id004
|
84
84
|
- !ruby/object:Gem::Dependency
|
85
|
-
name:
|
85
|
+
name: curb
|
86
86
|
prerelease: false
|
87
87
|
requirement: &id005 !ruby/object:Gem::Requirement
|
88
|
+
none: false
|
89
|
+
requirements:
|
90
|
+
- - ">="
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
hash: 29
|
93
|
+
segments:
|
94
|
+
- 0
|
95
|
+
- 7
|
96
|
+
- 15
|
97
|
+
version: 0.7.15
|
98
|
+
type: :runtime
|
99
|
+
version_requirements: *id005
|
100
|
+
- !ruby/object:Gem::Dependency
|
101
|
+
name: rake
|
102
|
+
prerelease: false
|
103
|
+
requirement: &id006 !ruby/object:Gem::Requirement
|
88
104
|
none: false
|
89
105
|
requirements:
|
90
106
|
- - ">="
|
@@ -94,11 +110,11 @@ dependencies:
|
|
94
110
|
- 0
|
95
111
|
version: "0"
|
96
112
|
type: :development
|
97
|
-
version_requirements: *
|
113
|
+
version_requirements: *id006
|
98
114
|
- !ruby/object:Gem::Dependency
|
99
115
|
name: rspec
|
100
116
|
prerelease: false
|
101
|
-
requirement: &
|
117
|
+
requirement: &id007 !ruby/object:Gem::Requirement
|
102
118
|
none: false
|
103
119
|
requirements:
|
104
120
|
- - ">="
|
@@ -110,11 +126,11 @@ dependencies:
|
|
110
126
|
- 0
|
111
127
|
version: 2.4.0
|
112
128
|
type: :development
|
113
|
-
version_requirements: *
|
129
|
+
version_requirements: *id007
|
114
130
|
- !ruby/object:Gem::Dependency
|
115
131
|
name: cucumber
|
116
132
|
prerelease: false
|
117
|
-
requirement: &
|
133
|
+
requirement: &id008 !ruby/object:Gem::Requirement
|
118
134
|
none: false
|
119
135
|
requirements:
|
120
136
|
- - ">="
|
@@ -126,11 +142,11 @@ dependencies:
|
|
126
142
|
- 0
|
127
143
|
version: 0.10.0
|
128
144
|
type: :development
|
129
|
-
version_requirements: *
|
145
|
+
version_requirements: *id008
|
130
146
|
- !ruby/object:Gem::Dependency
|
131
147
|
name: i18n
|
132
148
|
prerelease: false
|
133
|
-
requirement: &
|
149
|
+
requirement: &id009 !ruby/object:Gem::Requirement
|
134
150
|
none: false
|
135
151
|
requirements:
|
136
152
|
- - ">="
|
@@ -142,7 +158,21 @@ dependencies:
|
|
142
158
|
- 0
|
143
159
|
version: 0.5.0
|
144
160
|
type: :development
|
145
|
-
version_requirements: *
|
161
|
+
version_requirements: *id009
|
162
|
+
- !ruby/object:Gem::Dependency
|
163
|
+
name: ruby-debug
|
164
|
+
prerelease: false
|
165
|
+
requirement: &id010 !ruby/object:Gem::Requirement
|
166
|
+
none: false
|
167
|
+
requirements:
|
168
|
+
- - ">="
|
169
|
+
- !ruby/object:Gem::Version
|
170
|
+
hash: 3
|
171
|
+
segments:
|
172
|
+
- 0
|
173
|
+
version: "0"
|
174
|
+
type: :development
|
175
|
+
version_requirements: *id010
|
146
176
|
description: |
|
147
177
|
= Ungulate
|
148
178
|
|
@@ -195,16 +225,17 @@ files:
|
|
195
225
|
- features/camels.jpg
|
196
226
|
- features/cope_with_empty_queue.feature
|
197
227
|
- features/image_resize.feature
|
228
|
+
- features/resize_then_composite.feature
|
198
229
|
- features/step_definitions/command_steps.rb
|
199
230
|
- features/step_definitions/queue_steps.rb
|
200
231
|
- features/step_definitions/version_steps.rb
|
232
|
+
- features/superhugwatermark.png
|
201
233
|
- features/support.rb
|
202
234
|
- lib/ungulate/file_upload.rb
|
203
235
|
- lib/ungulate/job.rb
|
204
236
|
- lib/ungulate/server.rb
|
205
237
|
- lib/ungulate/view_helpers.rb
|
206
238
|
- lib/ungulate.rb
|
207
|
-
- spec/spec.opts
|
208
239
|
- spec/spec_helper.rb
|
209
240
|
- spec/ungulate/file_upload_spec.rb
|
210
241
|
- spec/ungulate/job_spec.rb
|
data/spec/spec.opts
DELETED