ungulate 0.1.4 → 0.2.0

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.
Files changed (42) hide show
  1. data/VERSION +1 -1
  2. data/bin/ungulate_server.rb +22 -7
  3. data/features/image_resize.feature +2 -12
  4. data/features/notification_url.feature +14 -0
  5. data/features/resize_then_composite.feature +2 -1
  6. data/features/step_definitions/bucket_steps.rb +4 -0
  7. data/features/step_definitions/command_steps.rb +10 -1
  8. data/features/step_definitions/notification_steps.rb +23 -0
  9. data/features/step_definitions/queue_steps.rb +7 -21
  10. data/features/step_definitions/version_steps.rb +2 -2
  11. data/features/{support.rb → support/config.rb} +4 -0
  12. data/features/support/helpers.rb +21 -0
  13. data/features/support/put_server.rb +22 -0
  14. data/lib/ungulate.rb +64 -0
  15. data/lib/ungulate/blob_processor.rb +34 -0
  16. data/lib/ungulate/curl_http.rb +25 -0
  17. data/lib/ungulate/file_upload.rb +22 -16
  18. data/lib/ungulate/job.rb +23 -143
  19. data/lib/ungulate/rmagick_version_creator.rb +77 -0
  20. data/lib/ungulate/s3_storage.rb +41 -0
  21. data/lib/ungulate/server.rb +27 -7
  22. data/lib/ungulate/sqs_message_queue.rb +38 -0
  23. data/spec/fixtures/chuckle.jpg +0 -0
  24. data/spec/fixtures/chuckle.png +0 -0
  25. data/spec/fixtures/chuckle_converted.png +0 -0
  26. data/spec/fixtures/chuckle_converted_badly.png +0 -0
  27. data/spec/fixtures/chuckle_thumbnail.png +0 -0
  28. data/spec/fixtures/watermark.png +0 -0
  29. data/spec/lib/ungulate/blob_processor_spec.rb +68 -0
  30. data/spec/lib/ungulate/curl_http_spec.rb +20 -0
  31. data/spec/{ungulate → lib/ungulate}/file_upload_spec.rb +10 -27
  32. data/spec/lib/ungulate/job_spec.rb +120 -0
  33. data/spec/lib/ungulate/rmagick_version_creator_spec.rb +97 -0
  34. data/spec/lib/ungulate/s3_storage_spec.rb +74 -0
  35. data/spec/lib/ungulate/server_spec.rb +44 -0
  36. data/spec/lib/ungulate/sqs_message_queue_spec.rb +28 -0
  37. data/spec/{ungulate → lib/ungulate}/view_helpers_spec.rb +1 -1
  38. data/spec/lib/ungulate_spec.rb +24 -0
  39. data/spec/spec_helper.rb +73 -0
  40. metadata +67 -25
  41. data/spec/ungulate/job_spec.rb +0 -386
  42. data/spec/ungulate/server_spec.rb +0 -42
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.1.4
1
+ 0.2.0
@@ -1,18 +1,33 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require 'ungulate/server'
3
+ require File.expand_path('../lib/ungulate', File.dirname(__FILE__))
4
+ require 'optparse'
4
5
 
5
- if ARGV[0].nil?
6
- $stderr.puts "Must provide a queue name after calling the server"
7
- exit 1
6
+ logger = Logger.new $stderr
7
+
8
+ options = {
9
+ :config => File.expand_path('../config/ungulate', File.dirname(__FILE__))
10
+ }
11
+
12
+ cmdline_options = OptionParser.new do |opts|
13
+ opts.banner = "Usage: ungulate_server.rb [options]"
14
+
15
+ opts.on '-c', '--config CONFIG' do |config|
16
+ options[:config] = config
17
+ end
8
18
  end
9
19
 
10
- logger = Logger.new STDERR
20
+ cmdline_options.parse!
21
+
22
+ require options[:config]
11
23
 
12
24
  loop do
13
25
  begin
14
- processed_something = Ungulate::Server.run(ARGV[0])
15
- sleep(ENV['SLEEP'] || 2) unless processed_something
26
+ processed_something = Ungulate::Server.run
27
+ sleep(Ungulate.configuration.server_sleep || 2) unless processed_something
28
+ rescue Ungulate::MissingConfiguration => e
29
+ logger.error e.message
30
+ exit 1
16
31
  rescue StandardError => e
17
32
  logger.error e.message
18
33
  end
@@ -5,19 +5,9 @@ Feature: Image resize
5
5
 
6
6
  Background:
7
7
  Given an empty queue
8
+ And an empty bucket
8
9
 
9
- Scenario: Run queue on image key with no path separator
10
- Given a request to resize "image.jpg" to sizes:
11
- | label | width | height |
12
- | large | 200 | 100 |
13
- | small | 100 | 50 |
14
- When I run Ungulate
15
- Then there should be the following public versions:
16
- | key |
17
- | image_large.jpg |
18
- | image_small.jpg |
19
-
20
- Scenario: Run queue on image key with path separator
10
+ Scenario: Run queue that has one image job
21
11
  Given a request to resize "some/path/to/image.jpg" to sizes:
22
12
  | label | width | height |
23
13
  | large | 200 | 100 |
@@ -0,0 +1,14 @@
1
+ Feature: Notification URL
2
+ As a developer
3
+ I want Ungulate to PUT to a URL when it's finished a job
4
+ So that I don't have to poll for the completed resource
5
+
6
+ Background:
7
+ Given an empty queue
8
+ And an empty bucket
9
+
10
+ Scenario: Run queue that has one image job
11
+ Given a request that has a notification URL
12
+ When I run Ungulate
13
+ Then the notification URL should receive a PUT
14
+
@@ -1,10 +1,11 @@
1
- Feature: Image resize
1
+ Feature: Image resize then composite
2
2
  As a site owner
3
3
  I want Ungulate to resize then composite images
4
4
  So that I don't have to watermark images by hand
5
5
 
6
6
  Scenario: Run queue on image key with no path separator
7
7
  Given an empty queue
8
+ And an empty bucket
8
9
  And a request to resize "image.jpg" and then composite with "https://dmxno528jhfy0.cloudfront.net/superhug-watermark.png"
9
10
  When I run Ungulate
10
11
  Then there should be a public watermarked version
@@ -0,0 +1,4 @@
1
+ Given /^an empty bucket$/ do
2
+ bucket.clear
3
+ end
4
+
@@ -1,9 +1,18 @@
1
1
  require 'ostruct'
2
+ require 'ungulate'
2
3
 
3
4
  When /^I run Ungulate$/ do
4
5
  @errors = OpenStruct.new :write => ''
5
6
  $stderr = @errors
6
- Ungulate::Server.run @queue_name
7
+
8
+ Ungulate.configure do |config|
9
+ config.queue_name = QUEUE_NAME
10
+ config.queue_server = sqs_server
11
+ end
12
+
13
+ 10.times do
14
+ break if Ungulate::Server.run
15
+ end
7
16
  end
8
17
 
9
18
  Then /^there should be no errors$/ do
@@ -0,0 +1,23 @@
1
+ Given /^a request that has a notification URL$/ do
2
+ key = 'bobbyjpeg'
3
+
4
+ bucket.put key, File.open('features/camels.jpg').read
5
+
6
+ message = {
7
+ :bucket => BUCKET_NAME,
8
+ :key => key,
9
+ :notification_url => 'http://localhost:9999/bob',
10
+ :versions => {
11
+ :medium => [
12
+ [:resize_to_fill, 100, 100],
13
+ ]
14
+ }
15
+ }.to_yaml
16
+
17
+ send_message(message)
18
+ end
19
+
20
+ Then /^the notification URL should receive a PUT$/ do
21
+ File.read(TEST_FILE.path).should == 'http://localhost:9999/bob'
22
+ end
23
+
@@ -1,19 +1,10 @@
1
1
  Given /^an empty queue$/ do
2
- sqs = RightAws::SqsGen2.new(ENV['AMAZON_ACCESS_KEY_ID'],
3
- ENV['AMAZON_SECRET_ACCESS_KEY'])
4
- @queue_name = 'ungulate-test-queue'
5
- @q = sqs.queue @queue_name
2
+ @q = sqs.queue QUEUE_NAME
6
3
  @q.clear
7
4
  end
8
5
 
9
6
  Given /^a request to resize "([^\"]*)" to sizes:$/ do |key, table|
10
- @bucket_name = "ungulate-test"
11
-
12
- @s3 = RightAws::S3.new(ENV['AMAZON_ACCESS_KEY_ID'],
13
- ENV['AMAZON_SECRET_ACCESS_KEY'])
14
- @bucket = @s3.bucket @bucket_name
15
- @bucket.put key, File.open('features/camels.jpg').read
16
-
7
+ bucket.put key, File.open('features/camels.jpg').read
17
8
 
18
9
  versions = table.rows.inject({}) do |hash, row|
19
10
  label, width, height = row
@@ -22,24 +13,19 @@ Given /^a request to resize "([^\"]*)" to sizes:$/ do |key, table|
22
13
  end
23
14
 
24
15
  message = {
25
- :bucket => @bucket_name,
16
+ :bucket => BUCKET_NAME,
26
17
  :key => key,
27
18
  :versions => versions
28
19
  }.to_yaml
29
20
 
30
- @q.send_message(message)
21
+ send_message(message)
31
22
  end
32
23
 
33
24
  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
25
+ bucket.put key, File.open('features/camels.jpg').read
40
26
 
41
27
  message = {
42
- :bucket => @bucket_name,
28
+ :bucket => BUCKET_NAME,
43
29
  :key => key,
44
30
  :versions => {
45
31
  :watermarked => [
@@ -49,5 +35,5 @@ Given /^a request to resize "([^"]*)" and then composite with "([^"]*)"$/ do |ke
49
35
  }
50
36
  }.to_yaml
51
37
 
52
- @q.send_message(message)
38
+ send_message(message)
53
39
  end
@@ -1,5 +1,5 @@
1
1
  Then /^there should be the following public versions:$/ do |table|
2
- Net::HTTP.start("#{@bucket_name}.s3.amazonaws.com", 80) do |http|
2
+ Net::HTTP.start("#{BUCKET_NAME}.s3.amazonaws.com", 80) do |http|
3
3
  table.rows.flatten.each do |key|
4
4
  response = http.get("/#{key}")
5
5
  response.should be_a(Net::HTTPSuccess)
@@ -8,7 +8,7 @@ Then /^there should be the following public versions:$/ do |table|
8
8
  end
9
9
 
10
10
  Then /^there should be a public watermarked version$/ do
11
- Net::HTTP.start("#{@bucket_name}.s3.amazonaws.com", 80) do |http|
11
+ Net::HTTP.start("#{BUCKET_NAME}.s3.amazonaws.com", 80) do |http|
12
12
  response = http.get("/image_watermarked.jpg")
13
13
  response.should be_a(Net::HTTPSuccess)
14
14
  end
@@ -1,2 +1,6 @@
1
1
  $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib'))
2
2
  require 'lib/ungulate'
3
+ require 'ruby-debug'
4
+
5
+ QUEUE_NAME = 'ungulate-test-queue'
6
+ BUCKET_NAME = 'ungulate-test'
@@ -0,0 +1,21 @@
1
+ require 'right_aws'
2
+
3
+ def bucket
4
+ s3 = RightAws::S3.new(ENV['AMAZON_ACCESS_KEY_ID'],
5
+ ENV['AMAZON_SECRET_ACCESS_KEY'])
6
+ s3.bucket BUCKET_NAME
7
+ end
8
+
9
+ def sqs_server
10
+ 'sqs.eu-west-1.amazonaws.com'
11
+ end
12
+
13
+ def sqs
14
+ sqs = RightAws::SqsGen2.new(ENV['AMAZON_ACCESS_KEY_ID'],
15
+ ENV['AMAZON_SECRET_ACCESS_KEY'],
16
+ :server => sqs_server)
17
+ end
18
+
19
+ def send_message(message)
20
+ @q.send_message message
21
+ end
@@ -0,0 +1,22 @@
1
+ require 'tempfile'
2
+
3
+ TEST_FILE = Tempfile.new('put request')
4
+
5
+ pid = fork do
6
+ require 'webrick'
7
+
8
+ class PutServlet < WEBrick::HTTPServlet::AbstractServlet
9
+ def do_PUT(req, resp)
10
+ TEST_FILE << req.request_uri
11
+ TEST_FILE.close
12
+ raise WEBrick::HTTPStatus::OK
13
+ end
14
+ end
15
+
16
+ server = WEBrick::HTTPServer.new(:Port => 9999)
17
+ server.mount('/bob', PutServlet)
18
+ server.start
19
+ end
20
+
21
+ at_exit { Process.kill('KILL', pid) }
22
+
data/lib/ungulate.rb CHANGED
@@ -1,7 +1,71 @@
1
1
  require 'rubygems'
2
+ require 'bundler/setup'
3
+
4
+ require 'hashie'
2
5
  require 'ungulate/server'
6
+ require 'ungulate/job'
7
+ require 'ungulate/sqs_message_queue'
8
+ require 'ungulate/blob_processor'
9
+ require 'ungulate/rmagick_version_creator'
10
+ require 'ungulate/s3_storage'
11
+ require 'ungulate/curl_http'
3
12
 
4
13
  module Ungulate
14
+ class Configuration < Hashie::Mash; end
15
+ class MissingConfiguration < StandardError; end
16
+
17
+ def self.configuration
18
+ @config ||=
19
+ begin
20
+ config = Configuration.new
21
+
22
+ amazon_credentials = lambda {
23
+ {
24
+ :access_key_id => config.access_key_id || ENV['AMAZON_ACCESS_KEY_ID'],
25
+ :secret_access_key => config.secret_access_key || ENV['AMAZON_SECRET_ACCESS_KEY']
26
+ }
27
+ }
28
+
29
+ config.queue = lambda {
30
+ SqsMessageQueue.new(
31
+ config.queue_name,
32
+ amazon_credentials.call.merge(:server => config.queue_server)
33
+ )
34
+ }
35
+
36
+ config.http = lambda {
37
+ CurlHttp.new
38
+ }
39
+
40
+ config.version_creator = lambda {
41
+ RmagickVersionCreator.new(:http => config.http.call)
42
+ }
43
+
44
+ config.storage = lambda {
45
+ S3Storage.new(amazon_credentials.call)
46
+ }
47
+
48
+ config.blob_processor = lambda {
49
+ BlobProcessor.new(
50
+ :version_creator => config.version_creator.call
51
+ )
52
+ }
53
+
54
+ config.job_processor = lambda {
55
+ Job.new(
56
+ :blob_processor => config.blob_processor.call,
57
+ :storage => config.storage.call,
58
+ :http => config.http.call
59
+ )
60
+ }
61
+
62
+ config
63
+ end
64
+ end
65
+
66
+ def self.configure(&block)
67
+ yield configuration
68
+ end
5
69
  end
6
70
 
7
71
  if defined? ActionView::Base
@@ -0,0 +1,34 @@
1
+ module Ungulate
2
+ class BlobProcessor
3
+ def initialize(options)
4
+ @creator = options[:version_creator]
5
+ end
6
+
7
+ def process(options)
8
+ versions = options.delete(:versions)
9
+ blob = options.delete(:blob)
10
+ original_key = options.delete(:original_key)
11
+ bucket = options.delete(:bucket)
12
+
13
+ versions.each_pair do |name, instructions|
14
+ stored_data = @creator.create(blob, instructions)
15
+
16
+ bucket.store(
17
+ new_key(original_key, name),
18
+ stored_data[:blob],
19
+ :version => name,
20
+ :content_type => stored_data[:content_type]
21
+ )
22
+ end
23
+ end
24
+
25
+ protected
26
+
27
+ def new_key(original, version)
28
+ dirname = File.dirname(original)
29
+ extname = File.extname(original)
30
+ basename = File.basename(original, extname)
31
+ "#{dirname}/#{basename}_#{version}#{extname}".sub(/^\.\//, '')
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,25 @@
1
+ require 'curb'
2
+
3
+ module Ungulate
4
+ class CurlHttp
5
+ def initialize(options = {})
6
+ @easy = Curl::Easy
7
+ @logger = options[:logger] || ::Logger.new($stdout)
8
+ end
9
+
10
+ def get_body(url)
11
+ @logger.info "GET via HTTP: #{url}"
12
+ @easy.http_get(url).body_str
13
+ end
14
+
15
+ def put(url)
16
+ @logger.info "PUT #{url}"
17
+ @response = @easy.http_put(url, '')
18
+ self
19
+ end
20
+
21
+ def code
22
+ @response.response_code
23
+ end
24
+ end
25
+ end
@@ -1,23 +1,21 @@
1
1
  require 'active_support/core_ext/class/attribute_accessors'
2
+ require 'active_support/json'
3
+
2
4
  class Ungulate::FileUpload
3
- attr_accessor(
4
- :bucket_url,
5
- :key
6
- )
5
+ attr_accessor :bucket_url, :key
7
6
 
8
- cattr_accessor(
9
- :access_key_id,
10
- :queue_name,
11
- :secret_access_key
12
- )
7
+ class << self
8
+ def config
9
+ Ungulate.configuration
10
+ end
13
11
 
14
- def self.enqueue(job_description)
15
- queue.send_message(job_description.to_yaml)
16
- end
12
+ def queue
13
+ @queue ||= config.queue.call
14
+ end
17
15
 
18
- def self.queue
19
- sqs = RightAws::SqsGen2.new(access_key_id, secret_access_key)
20
- sqs.queue(queue_name)
16
+ def enqueue(job_description)
17
+ queue.push(job_description.to_yaml)
18
+ end
21
19
  end
22
20
 
23
21
  def initialize(options = {})
@@ -29,6 +27,14 @@ class Ungulate::FileUpload
29
27
  end
30
28
  end
31
29
 
30
+ def config
31
+ self.class.config
32
+ end
33
+
34
+ def access_key_id
35
+ config.access_key_id
36
+ end
37
+
32
38
  def acl
33
39
  condition 'acl'
34
40
  end
@@ -62,7 +68,7 @@ class Ungulate::FileUpload
62
68
  def signature
63
69
  Base64.encode64(
64
70
  OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('sha1'),
65
- secret_access_key,
71
+ config.secret_access_key,
66
72
  policy)
67
73
  ).gsub("\n", '')
68
74
  end