rcarvalho-happening 0.2.5.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,120 @@
1
+ require 'uri'
2
+ require 'cgi'
3
+
4
+ module Happening
5
+ module S3
6
+ class Item
7
+ include Utils
8
+
9
+ REQUIRED_FIELDS = [:server]
10
+ VALID_HEADERS = ['Cache-Control', 'Content-Disposition', 'Content-Encoding', 'Content-Length', 'Content-MD5', 'Content-Type', 'Expect', 'Expires']
11
+
12
+ attr_accessor :bucket, :aws_id, :options
13
+
14
+ def initialize(bucket, aws_id, options = {})
15
+ @options = {
16
+ :timeout => 10,
17
+ :server => 's3.amazonaws.com',
18
+ :protocol => 'https',
19
+ :aws_access_key_id => nil,
20
+ :aws_secret_access_key => nil,
21
+ :retry_count => 4,
22
+ :permissions => 'private',
23
+ :ssl => Happening::S3.ssl_options
24
+ }.update(symbolize_keys(options))
25
+ assert_valid_keys(options, :timeout, :server, :protocol, :aws_access_key_id, :aws_secret_access_key, :retry_count, :permissions, :ssl)
26
+ @aws_id = aws_id.to_s
27
+ @bucket = bucket.to_s
28
+
29
+ validate
30
+ end
31
+
32
+ def head(request_options = {}, &blk)
33
+ headers = needs_to_sign? ? aws.sign("HEAD", path) : {}
34
+ request_options[:on_success] = blk if blk
35
+ request_options.update(:headers => headers)
36
+ Happening::S3::Request.new(:head, url, {:ssl => options[:ssl]}.update(request_options)).execute
37
+ end
38
+
39
+ def get(request_options = {}, &blk)
40
+ headers = needs_to_sign? ? aws.sign("GET", path) : {}
41
+ request_options[:on_success] = blk if blk
42
+ request_options.update(:headers => headers)
43
+ Happening::S3::Request.new(:get, url, {:ssl => options[:ssl]}.update(request_options)).execute
44
+ end
45
+
46
+ def put(data, request_options = {}, &blk)
47
+ headers = construct_aws_headers('PUT', request_options.delete(:headers) || {})
48
+ request_options[:on_success] = blk if blk
49
+ request_options.update(:headers => headers, :data => data)
50
+ Happening::S3::Request.new(:put, url, {:ssl => options[:ssl]}.update(request_options)).execute
51
+ end
52
+
53
+ def delete(request_options = {}, &blk)
54
+ headers = needs_to_sign? ? aws.sign("DELETE", path, {'url' => path}) : {}
55
+ request_options[:on_success] = blk if blk
56
+ request_options.update(:headers => headers)
57
+ Happening::S3::Request.new(:delete, url, {:ssl => options[:ssl]}.update(request_options)).execute
58
+ end
59
+
60
+ def url
61
+ URI::Generic.new(options[:protocol], nil, server, port, nil, path(!dns_bucket?), nil, nil, nil).to_s
62
+ end
63
+
64
+ def server
65
+ dns_bucket? ? "#{bucket}.#{options[:server]}" : options[:server]
66
+ end
67
+
68
+ def path(with_bucket=true)
69
+ with_bucket ? "/#{bucket}/#{CGI::escape(aws_id)}" : "/#{CGI::escape(aws_id)}"
70
+ end
71
+
72
+ protected
73
+
74
+ def needs_to_sign?
75
+ present?(options[:aws_access_key_id])
76
+ end
77
+
78
+ def dns_bucket?
79
+ # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/index.html?BucketRestrictions.html
80
+ return false unless (3..63) === bucket.size
81
+ bucket.split('.').each do |component|
82
+ return false unless component[/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/]
83
+ end
84
+ true
85
+ end
86
+
87
+ def port
88
+ (options[:protocol].to_s == 'https') ? 443 : 80
89
+ end
90
+
91
+ def validate
92
+ raise ArgumentError, "need a bucket name" unless present?(bucket)
93
+ raise ArgumentError, "need a AWS Key" unless present?(aws_id)
94
+
95
+ REQUIRED_FIELDS.each do |field|
96
+ raise ArgumentError, "need field #{field}" unless present?(options[field])
97
+ end
98
+
99
+ raise ArgumentError, "unknown protocoll #{options[:protocol]}" unless ['http', 'https'].include?(options[:protocol])
100
+ end
101
+
102
+ def aws
103
+ @aws ||= Happening::AWS.new(options[:aws_access_key_id], options[:aws_secret_access_key])
104
+ end
105
+
106
+ def construct_aws_headers(http_method, headers = {})
107
+ unless headers.keys.all?{|header| VALID_HEADERS.include?(header) || header.to_s.match(/\Ax-amz-/) }
108
+ raise ArgumentError, "invalid headers. All headers must either one of #{VALID_HEADERS} or start with 'x-amz-'"
109
+ end
110
+
111
+ permissions = options[:permissions] != 'private' ? {'x-amz-acl' => options[:permissions] } : {}
112
+ headers.update(permissions)
113
+ headers.update({'url' => path})
114
+
115
+ headers = needs_to_sign? ? aws.sign(http_method, path, headers) : headers
116
+ end
117
+
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,111 @@
1
+ module Happening
2
+ module S3
3
+ class Request
4
+ include Utils
5
+
6
+ VALID_HTTP_METHODS = [:head, :get, :put, :delete]
7
+
8
+ attr_accessor :http_method, :url, :options, :response
9
+
10
+ def initialize(http_method, url, options = {})
11
+ @options = {
12
+ :timeout => 10,
13
+ :retry_count => 4,
14
+ :headers => {},
15
+ :on_error => nil,
16
+ :on_success => nil,
17
+ :data => nil,
18
+ :ssl => {
19
+ :cert_chain_file => nil,
20
+ :verify_peer => false
21
+ }
22
+ }.update(options)
23
+ assert_valid_keys(options, :timeout, :on_success, :on_error, :retry_count, :headers, :data, :ssl)
24
+ @http_method = http_method
25
+ @url = url
26
+
27
+ validate
28
+ end
29
+
30
+ def execute
31
+ Happening::Log.debug "Request: #{http_method.to_s.upcase} #{url}"
32
+ @response = http_class.new(url).send(http_method, :timeout => options[:timeout], :head => options[:headers], :body => options[:data], :ssl => options[:ssl])
33
+
34
+ @response.errback { error_callback }
35
+ @response.callback { success_callback }
36
+ @response
37
+ end
38
+
39
+ def http_class
40
+ EventMachine::HttpRequest
41
+ end
42
+
43
+ protected
44
+
45
+ def validate
46
+ raise ArgumentError, "#{http_method} is not a valid HTTP method that #{self.class.name} understands." unless VALID_HTTP_METHODS.include?(http_method)
47
+ end
48
+
49
+ def error_callback
50
+ Happening::Log.error "Response error: #{http_method.to_s.upcase} #{url}: #{response.response_header.status rescue ''}"
51
+ if should_retry?
52
+ Happening::Log.info "#{http_method.to_s.upcase} #{url}: retrying after error: status #{response.response_header.status rescue ''}"
53
+ handle_retry
54
+ elsif options[:on_error].respond_to?(:call)
55
+ call_user_error_handler
56
+ else
57
+ raise Happening::Error.new("#{http_method.to_s.upcase} #{url}: Failed reponse! Status code was #{response.response_header.status rescue ''}")
58
+ end
59
+ end
60
+
61
+ def success_callback
62
+ Happening::Log.debug "Response success: #{http_method.to_s.upcase} #{url}: #{response.response_header.status rescue ''}"
63
+ case response.response_header.status
64
+ when 0, 400, 401, 404, 403, 409, 411, 412, 416, 500, 503
65
+ if should_retry?
66
+ Happening::Log.info "#{http_method.to_s.upcase} #{url}: retrying after: status #{response.response_header.status rescue ''}"
67
+ handle_retry
68
+ else
69
+ Happening::Log.error "#{http_method.to_s.upcase} #{url}: Re-tried too often - giving up"
70
+ error_callback
71
+ end
72
+ when 300, 301, 303, 304, 307
73
+ Happening::Log.info "#{http_method.to_s.upcase} #{url}: being redirected_to: #{response.response_header['LOCATION'] rescue ''}"
74
+ handle_redirect
75
+ else
76
+ call_user_success_handler
77
+ end
78
+ end
79
+
80
+ def call_user_success_handler
81
+ options[:on_success].call(response) if options[:on_success].respond_to?(:call)
82
+ end
83
+
84
+ def call_user_error_handler
85
+ options[:on_error].call(response) if options[:on_error].respond_to?(:call)
86
+ end
87
+
88
+ def should_retry?
89
+ options[:retry_count] > 0
90
+ end
91
+
92
+ def handle_retry
93
+ if should_retry?
94
+ new_request = self.class.new(http_method, url, options.update(:retry_count => options[:retry_count] - 1 ))
95
+ new_request.execute
96
+ else
97
+ Happening::Log.error "#{http_method.to_s.upcase} #{url}: Re-tried too often - giving up"
98
+ end
99
+ end
100
+
101
+ def handle_redirect
102
+ new_location = response.response_header['LOCATION'] rescue ''
103
+ raise "Could not find the location to redirect to, empty location header?" if blank?(new_location)
104
+
105
+ new_request = self.class.new(http_method, new_location, options)
106
+ new_request.execute
107
+ end
108
+
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,16 @@
1
+ module Happening
2
+ module S3
3
+
4
+ def self.ssl_options
5
+ @_ssl_options ||= {
6
+ :cert_chain_file => nil,
7
+ :verify_peer => false
8
+ }
9
+ end
10
+
11
+ def self.ssl_options=(val)
12
+ @_ssl_options = val
13
+ end
14
+
15
+ end
16
+ end
@@ -0,0 +1,26 @@
1
+ module Happening
2
+ module Utils
3
+ protected
4
+
5
+ def symbolize_keys(hash)
6
+ hash.inject({}) do |h, kv|
7
+ h[kv[0].to_sym] = kv[1]
8
+ h
9
+ end
10
+ end
11
+
12
+ def assert_valid_keys(hash, *valid_keys)
13
+ unknown_keys = hash.keys - [valid_keys].flatten
14
+ raise(ArgumentError, "Unknown key(s): #{unknown_keys.join(", ")}") unless unknown_keys.empty?
15
+ end
16
+
17
+ def present?(obj)
18
+ !blank?(obj)
19
+ end
20
+
21
+ def blank?(obj)
22
+ obj.respond_to?(:empty?) ? obj.empty? : !obj
23
+ end
24
+
25
+ end
26
+ end
data/lib/happening.rb ADDED
@@ -0,0 +1,24 @@
1
+ require 'rubygems'
2
+ require 'em-http'
3
+ require 'openssl'
4
+ require 'logger'
5
+
6
+ unless defined?(Happening)
7
+ $:<<(File.expand_path(File.dirname(__FILE__) + "/lib"))
8
+ require File.expand_path(File.dirname(__FILE__) + '/happening/utils')
9
+ require File.expand_path(File.dirname(__FILE__) + '/happening/log')
10
+ require File.expand_path(File.dirname(__FILE__) + '/happening/aws')
11
+ require File.expand_path(File.dirname(__FILE__) + '/happening/s3')
12
+ require File.expand_path(File.dirname(__FILE__) + '/happening/s3/request')
13
+ require File.expand_path(File.dirname(__FILE__) + '/happening/s3/item')
14
+ require File.expand_path(File.dirname(__FILE__) + '/happening/s3/bucket')
15
+
16
+ module Happening
17
+ MAJOR = 0
18
+ MINOR = 2
19
+ PATCH = 5
20
+
21
+ VERSION = [MAJOR, MINOR, PATCH].compact.join('.')
22
+ class Error < RuntimeError; end
23
+ end
24
+ end
data/test/aws_test.rb ADDED
@@ -0,0 +1,41 @@
1
+ require File.expand_path('../test_helper', __FILE__)
2
+
3
+ class ItemTest < Test::Unit::TestCase
4
+ context "An Happening::AWS instance" do
5
+
6
+ setup do
7
+ @aws = Happening::AWS.new('the-aws-access-key', 'the-aws-secret-key')
8
+ end
9
+
10
+ context "when constructing" do
11
+ should "require Access Key and Secret Key" do
12
+ assert_raise(ArgumentError) do
13
+ Happening::AWS.new(nil, nil)
14
+ end
15
+
16
+ assert_raise(ArgumentError) do
17
+ Happening::AWS.new('', '')
18
+ end
19
+
20
+ assert_nothing_raised do
21
+ Happening::AWS.new('abc', 'abc')
22
+ end
23
+ end
24
+ end
25
+
26
+ context "when signing parameters" do
27
+ should "return a header hash" do
28
+ assert_not_nil @aws.sign("GET", '/')['Authorization']
29
+ end
30
+
31
+ should "include the current date" do
32
+ assert_not_nil @aws.sign("GET", '/')['date']
33
+ end
34
+
35
+ should "keep given headers" do
36
+ assert_equal 'bar', @aws.sign("GET", '/', {'foo' => 'bar'})['foo']
37
+ end
38
+ end
39
+
40
+ end
41
+ end