ungulate 0.1.4 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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