happening 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE.txt ADDED
@@ -0,0 +1,15 @@
1
+ /*
2
+ * Copyright (c) 2010 Peritor GmbH <info@peritor.com>
3
+ *
4
+ * Permission to use, copy, modify, and distribute this software for any
5
+ * purpose with or without fee is hereby granted, provided that the above
6
+ * copyright notice and this permission notice appear in all copies.
7
+ *
8
+ * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
9
+ * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
10
+ * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
11
+ * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
12
+ * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
13
+ * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
14
+ * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
15
+ */
data/README.md ADDED
@@ -0,0 +1,116 @@
1
+ Amazon S3 Ruby library that leverages [EventMachine](http://rubyeventmachine.com/) and [em-http-request](http://github.com/igrigorik/em-http-request).
2
+
3
+ By using EventMachine Happening does not block on S3 downloads/uploads thus allowing for a higher concurrency.
4
+
5
+ Happening was developed by [Peritor](http://www.peritor.com) for usage inside Nanite/EventMachine.
6
+ Alternatives like RightAws block during the HTTP calls thus blocking the Nanite-Agent.
7
+
8
+ For now it only supports GET, PUT and DELETE operations on S3 items. The PUT operations support S3 ACLs/permissions.
9
+ Happening will handle redirects and retries on errors by default.
10
+
11
+ Installation
12
+ ============
13
+
14
+ gem install happening
15
+
16
+ Usage
17
+ =============
18
+
19
+ require 'happening'
20
+
21
+ EM.run do
22
+ item = Happening::S3::Item.new('bucket', 'item_id')
23
+ item.get # non-authenticated download, works only for public-read content
24
+
25
+ item = Happening::S3::Item.new('bucket', 'item_id', :aws_access_key_id => 'Your-ID', :aws_secret_access_key => 'secret')
26
+ item.get # authenticated download
27
+
28
+ item.put("The new content")
29
+
30
+ item.delete
31
+ end
32
+
33
+ The above examples are a bit useless, as you never get any content back.
34
+ You need to specify a callback that interacts with the http response:
35
+
36
+ EM.run do
37
+ on_success = Proc.new {|http| puts "the response is: #{http.response}"; EM.stop }
38
+ item = Happening::S3::Item.new('bucket', 'item_id', :aws_access_key_id => 'Your-ID', :aws_secret_access_key => 'secret')
39
+ item.get # authenticated download
40
+ end
41
+
42
+ This will enqueue your download and run it in the EventMachine event loop.
43
+
44
+ You can also react to errors:
45
+
46
+ EM.run do
47
+ on_error = Proc.new {|http| puts "An error occured: #{http.response_header.status}"; EM.stop }
48
+ on_success = Proc.new {|http| puts "the response is: #{http.response}"; EM.stop }
49
+ item = Happening::S3::Item.new('bucket', 'item_id', :aws_access_key_id => 'Your-ID', :aws_secret_access_key => 'secret')
50
+ item.get
51
+ end
52
+
53
+ Downloading many files would look like this:
54
+
55
+ EM.run do
56
+ count = 100
57
+ on_error = Proc.new {|http| puts "An error occured: #{http.response_header.status}"; EM.stop if count <= 0}
58
+ on_success = Proc.new {|http| puts "the response is: #{http.response}"; EM.stop if count <= 0}
59
+
60
+ count.times do |i|
61
+ item = Happening::S3::Item.new('bucket', "item_#{i}", :aws_access_key_id => 'Your-ID', :aws_secret_access_key => 'secret')
62
+ item.get
63
+ end
64
+ end
65
+
66
+ Upload
67
+ =============
68
+
69
+ Happening supports the simple S3 PUT upload:
70
+
71
+ EM.run do
72
+ on_error = Proc.new {|http| puts "An error occured: #{http.response_header.status}"; EM.stop }
73
+ on_success = Proc.new {|http| puts "Upload finished!"; EM.stop }
74
+ item = Happening::S3::Item.new('bucket', 'item_id', :aws_access_key_id => 'Your-ID', :aws_secret_access_key => 'secret')
75
+ item.put( File.read('/etc/passwd') )
76
+ end
77
+
78
+ Setting permissions looks like this:
79
+
80
+ EM.run do
81
+ on_error = Proc.new {|http| puts "An error occured: #{http.response_header.status}"; EM.stop }
82
+ on_success = Proc.new {|http| puts "the response is: #{http.response}"; EM.stop }
83
+ item = Happening::S3::Item.new('bucket', 'item_id', :aws_access_key_id => 'Your-ID', :aws_secret_access_key => 'secret', :permissions => 'public-write')
84
+ item.get
85
+ end
86
+
87
+ Deleting
88
+ =============
89
+
90
+ Happening support the simple S3 PUT upload:
91
+
92
+ EM.run do
93
+ on_error = Proc.new {|http| puts "An error occured: #{http.response_header.status}"; EM.stop }
94
+ on_success = Proc.new {|http| puts "Deleted!"; EM.stop }
95
+ item = Happening::S3::Item.new('bucket', 'item_id', :aws_access_key_id => 'Your-ID', :aws_secret_access_key => 'secret')
96
+ item.delete
97
+ end
98
+
99
+ Amazon returns no content on delete, so having a success handler is usually not needed for delete operations.
100
+
101
+ Credits
102
+ =============
103
+
104
+ The AWS signing and canonical request description is based on [RightAws](http://github.com/rightscale/right_aws).
105
+
106
+
107
+ License
108
+ =============
109
+
110
+ Happening is licensed under the OpenBSD / two-clause BSD license, modeled after the ISC license. See LICENSE.txt
111
+
112
+
113
+ About
114
+ =============
115
+
116
+ Happening was written by [Jonathan Weiss](http://twitter.com/jweiss) for [Peritor](http://www.peritor.com).
data/lib/aws.rb ADDED
@@ -0,0 +1,59 @@
1
+ module Happening
2
+ class AWS
3
+
4
+ AMAZON_HEADER_PREFIX = 'x-amz-'
5
+ AMAZON_METADATA_PREFIX = 'x-amz-meta-'
6
+ DIGEST = OpenSSL::Digest.new('sha1')
7
+
8
+ attr_accessor :aws_access_key_id, :aws_secret_access_key
9
+
10
+ def initialize(aws_access_key_id, aws_secret_access_key)
11
+ @aws_access_key_id = aws_access_key_id
12
+ @aws_secret_access_key = aws_secret_access_key
13
+ raise ArgumentError, "need AWS Access Key Id and AWS Secret Key" unless aws_access_key_id.present? && aws_secret_access_key.present?
14
+ end
15
+
16
+ def sign(method, path, headers={})
17
+ headers = {
18
+ 'date' => Time.now.httpdate
19
+ }.update(headers)
20
+
21
+ request_description = canonical_request_description(method, path, headers)
22
+ headers.update("Authorization" => "AWS #{aws_access_key_id}:#{generate_signature(request_description)}")
23
+ end
24
+
25
+ protected
26
+
27
+ def generate_signature(request_description)
28
+ Base64.encode64(OpenSSL::HMAC.digest(DIGEST, aws_secret_access_key, request_description)).strip
29
+ end
30
+
31
+ def canonical_request_description(method, path, headers = {}, expires = nil)
32
+ s3_attributes = {}
33
+ headers.each do |key, value|
34
+ key = key.downcase
35
+ s3_attributes[key] = value.to_s.strip if key.match(/^#{AMAZON_HEADER_PREFIX}|^content-md5$|^content-type$|^date$/o)
36
+ end
37
+ s3_attributes['content-type'] ||= ''
38
+ s3_attributes['content-md5'] ||= ''
39
+ s3_attributes['date'] = '' if s3_attributes.has_key?('x-amz-date')
40
+ s3_attributes['date'] = expires if expires
41
+
42
+ description = "#{method}\n"
43
+ s3_attributes.sort { |a, b| a[0] <=> b[0] }.each do |key, value|
44
+ description << (key.match(/^#{AMAZON_HEADER_PREFIX}/o) ? "#{key}:#{value}\n" : "#{value}\n")
45
+ end
46
+
47
+ # ignore all parameters by default
48
+ description << path.gsub(/\?.*$/, '')
49
+
50
+ # handle amazon parameters
51
+ description << '?acl' if path[/[&?]acl($|&|=)/]
52
+ description << '?torrent' if path[/[&?]torrent($|&|=)/]
53
+ description << '?location' if path[/[&?]location($|&|=)/]
54
+ description << '?logging' if path[/[&?]logging($|&|=)/]
55
+ description
56
+ end
57
+
58
+ end
59
+ end
data/lib/log.rb ADDED
@@ -0,0 +1,40 @@
1
+ module Happening
2
+ class Log
3
+
4
+ @@logger = Logger.new(STDOUT)
5
+ @@logger.level = Logger::ERROR
6
+
7
+ def self.logger=(log)
8
+ @@logger = log
9
+ end
10
+
11
+ def self.logger
12
+ @@logger
13
+ end
14
+
15
+ def self.level=(lev)
16
+ logger.level = lev
17
+ end
18
+
19
+ def self.level
20
+ logger.level
21
+ end
22
+
23
+ def self.debug(msg)
24
+ logger.debug(msg)
25
+ end
26
+
27
+ def self.info(msg)
28
+ logger.info(msg)
29
+ end
30
+
31
+ def self.warn(msg)
32
+ logger.warn(msg)
33
+ end
34
+
35
+ def self.error(msg)
36
+ logger.error(msg)
37
+ end
38
+
39
+ end
40
+ end
data/lib/s3/item.rb ADDED
@@ -0,0 +1,196 @@
1
+ require 'uri'
2
+ require 'cgi'
3
+
4
+ module Happening
5
+ module S3
6
+ class Item
7
+
8
+ REQUIRED_FIELDS = [:server]
9
+
10
+ attr_accessor :bucket, :aws_id, :options
11
+
12
+ def initialize(bucket, aws_id, options = {})
13
+ @options = {
14
+ :timeout => 10,
15
+ :on_error => nil,
16
+ :on_success => nil,
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
+ }.update(options.symbolize_keys)
24
+ options.assert_valid_keys(:timeout, :on_success, :on_error, :server, :protocol, :aws_access_key_id, :aws_secret_access_key, :retry_count, :permissions)
25
+ @aws_id = aws_id.to_s
26
+ @bucket = bucket.to_s
27
+
28
+ validate
29
+ end
30
+
31
+ def get
32
+ headers = needs_to_sign? ? aws.sign("GET", path) : {}
33
+ Happening::Log.debug "GET #{url}"
34
+ http = http_class.new(url).get(:timeout => options[:timeout], :head => headers)
35
+
36
+ http.errback { error_callback(:get, http) }
37
+ http.callback { success_callback(:get, http) }
38
+ nil
39
+ end
40
+
41
+ def put(data)
42
+ permissions = options[:permissions] != 'private' ? {'x-amz-acl' => options[:permissions] } : {}
43
+ headers = needs_to_sign? ? aws.sign("PUT", path, permissions.update({'url' => path})) : {}
44
+ Happening::Log.debug "PUT #{url}"
45
+ http = http_class.new(url).put(:timeout => options[:timeout], :head => headers, :body => data)
46
+
47
+ http.errback { error_callback(:put, http) }
48
+ http.callback { success_callback(:put, http, data) }
49
+ nil
50
+ end
51
+
52
+ def delete
53
+ headers = needs_to_sign? ? aws.sign("DELETE", path, {'url' => path}) : {}
54
+ Happening::Log.debug "DELETE #{url}"
55
+ http = http_class.new(url).delete(:timeout => options[:timeout], :head => headers)
56
+
57
+ http.errback { error_callback(:delete, http) }
58
+ http.callback { success_callback(:delete, http) }
59
+ nil
60
+ end
61
+
62
+ def url
63
+ URI::Generic.new(options[:protocol], nil, server, port, nil, path(!dns_bucket?), nil, nil, nil).to_s
64
+ end
65
+
66
+ def http_class
67
+ EventMachine::HttpRequest
68
+ end
69
+
70
+ def server
71
+ dns_bucket? ? "#{bucket}.#{options[:server]}" : options[:server]
72
+ end
73
+
74
+ def path(with_bucket=true)
75
+ with_bucket ? "/#{bucket}/#{CGI::escape(aws_id)}" : "/#{CGI::escape(aws_id)}"
76
+ end
77
+
78
+ protected
79
+
80
+ def error_callback(http_method, http)
81
+ call_user_error_handler(http)
82
+ end
83
+
84
+ def success_callback(http_method, http, data=nil)
85
+ Happening::Log.debug "Response #{http.response_header.status}"
86
+ case http.response_header.status
87
+ when 0, 400, 401, 404, 403, 409, 411, 412, 416, 500, 503
88
+ if should_retry?
89
+ Happening::Log.debug "retrying after: status #{http.response_header.status rescue ''}"
90
+ handle_retry(http_method, data)
91
+ else
92
+ call_user_error_handler(http)
93
+ end
94
+ when 300, 301, 303, 304, 307
95
+ Happening::Log.info "being redirected_to: #{http.response_header['LOCATION'] rescue ''}"
96
+ handle_redirect(http_method, http.response_header['LOCATION'], data)
97
+ else
98
+ call_user_success_handler(http)
99
+ end
100
+ end
101
+
102
+ def call_user_error_handler(http)
103
+ options[:on_error].call(http) if options[:on_error].respond_to?(:call)
104
+ end
105
+
106
+ def call_user_success_handler(http)
107
+ options[:on_success].call(http) if options[:on_success].respond_to?(:call)
108
+ end
109
+
110
+ def should_retry?
111
+ options[:retry_count] > 0
112
+ end
113
+
114
+ def handle_retry(http_method, data)
115
+ if should_retry?
116
+ new_request = self.class.new(bucket, aws_id, options.update(:retry_count => options[:retry_count] - 1 ))
117
+ case http_method
118
+ when :get
119
+ new_request.get
120
+ when :put
121
+ new_request.put(data)
122
+ when :delete
123
+ new_request.delete
124
+ else
125
+ raise "unknown http method #{http_method}"
126
+ end
127
+ else
128
+ Happening::Log.info "Re-tried too often - giving up" if Happening.debug?
129
+ end
130
+ end
131
+
132
+ def handle_redirect(http_method, location, data)
133
+ new_server, new_path = extract_location(location)
134
+
135
+ new_request = self.class.new(bucket, aws_id, options.update(:server => new_server))
136
+ case http_method
137
+ when :get
138
+ new_request.get
139
+ when :put
140
+ new_request.put(data)
141
+ when :delete
142
+ new_request.delete
143
+ else
144
+ raise "unknown http method #{http_method}"
145
+ end
146
+ end
147
+
148
+ def extract_location(location)
149
+ uri = URI.parse(location)
150
+ if match = uri.host.match(/\A#{bucket}\.(.*)/)
151
+ server = match[1]
152
+ path = uri.path
153
+ elsif match = uri.path.match(/\A\/#{bucket}\/(.*)/)
154
+ server = uri.host
155
+ path = match[1]
156
+ else
157
+ raise "being redirected to an not understood place: #{location}"
158
+ end
159
+ return server, path.sub(/^\//, '')
160
+ end
161
+
162
+ def needs_to_sign?
163
+ options[:aws_access_key_id].present?
164
+ end
165
+
166
+ def dns_bucket?
167
+ # http://docs.amazonwebservices.com/AmazonS3/2006-03-01/index.html?BucketRestrictions.html
168
+ return false unless (3..63) === bucket.size
169
+ bucket.split('.').each do |component|
170
+ return false unless component[/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/]
171
+ end
172
+ true
173
+ end
174
+
175
+ def port
176
+ (options[:protocol].to_s == 'https') ? 443 : 80
177
+ end
178
+
179
+ def validate
180
+ raise ArgumentError, "need a bucket name" unless bucket.present?
181
+ raise ArgumentError, "need a AWS Key" unless aws_id.present?
182
+
183
+ REQUIRED_FIELDS.each do |field|
184
+ raise ArgumentError, "need field #{field}" unless options[field].present?
185
+ end
186
+
187
+ raise ArgumentError, "unknown protocoll #{options[:protocol]}" unless ['http', 'https'].include?(options[:protocol])
188
+ end
189
+
190
+ def aws
191
+ @aws ||= Happening::AWS.new(options[:aws_access_key_id], options[:aws_secret_access_key])
192
+ end
193
+
194
+ end
195
+ end
196
+ end
data/test/aws_test.rb ADDED
@@ -0,0 +1,41 @@
1
+ require File.dirname(__FILE__) + "/test_helper"
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
@@ -0,0 +1,333 @@
1
+ require File.dirname(__FILE__) + "/../test_helper"
2
+
3
+ class ItemTest < Test::Unit::TestCase
4
+ context "An Happening::S3::Item instance" do
5
+
6
+ setup do
7
+ Happening::Log.level = Logger::ERROR
8
+ @item = Happening::S3::Item.new('the-bucket', 'the-key', :aws_access_key_id => '123', :aws_secret_access_key => 'secret', :server => '127.0.0.1')
9
+
10
+ @time = "Thu, 25 Feb 2010 10:00:00 GMT"
11
+ Time.stubs(:now).returns(stub(:httpdate => @time, :to_i => 99, :usec => 88))
12
+ end
13
+
14
+ context "validation" do
15
+ should "require a bucket and a key" do
16
+ assert_raise(ArgumentError) do
17
+ item = Happening::S3::Item.new()
18
+ end
19
+
20
+ assert_raise(ArgumentError) do
21
+ item = Happening::S3::Item.new('the-key')
22
+ end
23
+
24
+ assert_nothing_raised(ArgumentError) do
25
+ item = Happening::S3::Item.new('the-bucket', 'the-key')
26
+ end
27
+
28
+ end
29
+
30
+ should "not allow unknown options" do
31
+ assert_raise(ArgumentError) do
32
+ item = Happening::S3::Item.new('the-bucket', 'the-key', :aws_access_key_id => '123', :aws_secret_access_key => 'secret', :lala => 'lulul')
33
+ end
34
+ end
35
+
36
+ should "check valid protocol" do
37
+ assert_raise(ArgumentError) do
38
+ item = Happening::S3::Item.new('the-bucket', 'the-key', :aws_access_key_id => '123', :aws_secret_access_key => 'secret', :protocol => 'lulul')
39
+ end
40
+
41
+ assert_nothing_raised do
42
+ item = Happening::S3::Item.new('the-bucket', 'the-key', :aws_access_key_id => '123', :aws_secret_access_key => 'secret', :protocol => 'http')
43
+ end
44
+
45
+ assert_nothing_raised do
46
+ item = Happening::S3::Item.new('the-bucket', 'the-key', :aws_access_key_id => '123', :aws_secret_access_key => 'secret', :protocol => 'https')
47
+ end
48
+ end
49
+ end
50
+
51
+ context "when building the item url" do
52
+ should "build the full path out of the server, bucket, and key" do
53
+ @item = Happening::S3::Item.new('the-bucketissoooooooooooooooooooooooooooooooooooooolonggggggggggggggggggggggggggggggggggg', 'the-key', :aws_access_key_id => '123', :aws_secret_access_key => 'secret', :server => '127.0.0.1')
54
+ assert_equal "https://127.0.0.1:443/the-bucketissoooooooooooooooooooooooooooooooooooooolonggggggggggggggggggggggggggggggggggg/the-key", @item.url
55
+ end
56
+
57
+ should "use the DNS bucket name where possible" do
58
+ @item = Happening::S3::Item.new('bucket', 'the-key', :aws_access_key_id => '123', :aws_secret_access_key => 'secret')
59
+ assert_equal "https://bucket.s3.amazonaws.com:443/the-key", @item.url
60
+ end
61
+ end
62
+
63
+ context "when getting an item" do
64
+
65
+ should "call the on success callback" do
66
+ EventMachine::MockHttpRequest.register('https://bucket.s3.amazonaws.com:443/the-key', :get, {}, fake_response("data-here"))
67
+
68
+ called = false
69
+ data = nil
70
+ on_success = Proc.new {|http| called = true, data = http.response}
71
+ @item = Happening::S3::Item.new('bucket', 'the-key', :on_success => on_success)
72
+ run_in_em_loop do
73
+ @item.get
74
+
75
+ EM.add_timer(1) {
76
+ assert called
77
+ assert_equal 1, EventMachine::MockHttpRequest.count('https://bucket.s3.amazonaws.com:443/the-key', :get, {})
78
+ assert_equal "data-here\n", data
79
+ EM.stop_event_loop
80
+ }
81
+
82
+ end
83
+ end
84
+
85
+ should "sign requests if AWS credentials are passend" do
86
+ time = "Thu, 25 Feb 2010 12:06:33 GMT"
87
+ Time.stubs(:now).returns(mock(:httpdate => time))
88
+ EventMachine::MockHttpRequest.register('https://bucket.s3.amazonaws.com:443/the-key', :get, {"Authorization"=>"AWS abc:3OEcVbE//maUUmqh3A5ETEcr9TE=", 'date' => time}, fake_response("data-here"))
89
+
90
+ @item = Happening::S3::Item.new('bucket', 'the-key', :aws_access_key_id => 'abc', :aws_secret_access_key => '123')
91
+ run_in_em_loop do
92
+ @item.get
93
+
94
+ EM.add_timer(1) {
95
+ EM.stop_event_loop
96
+ assert_equal 1, EventMachine::MockHttpRequest.count('https://bucket.s3.amazonaws.com:443/the-key', :get, {"Authorization"=>"AWS abc:3OEcVbE//maUUmqh3A5ETEcr9TE=", 'date' => time})
97
+ }
98
+
99
+ end
100
+ end
101
+
102
+ should "retry on error" do
103
+ EventMachine::MockHttpRequest.register('https://bucket.s3.amazonaws.com:443/the-key', :get, {}, error_response(400))
104
+
105
+ @item = Happening::S3::Item.new('bucket', 'the-key')
106
+ run_in_em_loop do
107
+ @item.get
108
+
109
+ EM.add_timer(1) {
110
+ EM.stop_event_loop
111
+ assert_equal 5, EventMachine::MockHttpRequest.count('https://bucket.s3.amazonaws.com:443/the-key', :get, {})
112
+ }
113
+
114
+ end
115
+ end
116
+
117
+ should "handle re-direct" do
118
+ EventMachine::MockHttpRequest.register('https://bucket.s3.amazonaws.com:443/the-key', :get, {}, redirect_response('https://bucket.s3-external-3.amazonaws.com/the-key'))
119
+ EventMachine::MockHttpRequest.register('https://bucket.s3-external-3.amazonaws.com:443/the-key', :get, {}, fake_response('hy there'))
120
+
121
+ @item = Happening::S3::Item.new('bucket', 'the-key')
122
+ run_in_em_loop do
123
+ @item.get
124
+
125
+ EM.add_timer(1) {
126
+ EM.stop_event_loop
127
+ assert_equal 1, EventMachine::MockHttpRequest.count('https://bucket.s3.amazonaws.com:443/the-key', :get, {})
128
+ assert_equal 1, EventMachine::MockHttpRequest.count('https://bucket.s3-external-3.amazonaws.com:443/the-key', :get, {})
129
+ }
130
+
131
+ end
132
+ end
133
+ end
134
+
135
+ context "when deleting an item" do
136
+ should "send a DELETE to the items location" do
137
+ EventMachine::MockHttpRequest.register('https://bucket.s3.amazonaws.com:443/the-key', :delete, {
138
+ "Authorization"=>"AWS abc:nvkrlq4wor1qbFXZh6rHnAbiRjk=",
139
+ 'date' => @time,
140
+ 'url' => "/bucket/the-key"}, fake_response("data-here"))
141
+
142
+ @item = Happening::S3::Item.new('bucket', 'the-key', :aws_access_key_id => 'abc', :aws_secret_access_key => '123')
143
+ run_in_em_loop do
144
+ @item.delete
145
+
146
+ EM.add_timer(1) {
147
+ EM.stop_event_loop
148
+ assert_equal 1, EventMachine::MockHttpRequest.count('https://bucket.s3.amazonaws.com:443/the-key', :delete, {
149
+ "Authorization"=>"AWS abc:nvkrlq4wor1qbFXZh6rHnAbiRjk=",
150
+ 'date' => @time,
151
+ 'url' => "/bucket/the-key"})
152
+ }
153
+
154
+ end
155
+ end
156
+
157
+ should "handle re-direct" do
158
+ EventMachine::MockHttpRequest.register('https://bucket.s3.amazonaws.com:443/the-key', :delete, {
159
+ "Authorization"=>"AWS abc:nvkrlq4wor1qbFXZh6rHnAbiRjk=",
160
+ 'date' => @time,
161
+ 'url' => "/bucket/the-key"}, redirect_response('https://bucket.s3-external-3.amazonaws.com/the-key'))
162
+ EventMachine::MockHttpRequest.register('https://bucket.s3-external-3.amazonaws.com:443/the-key', :delete, {
163
+ "Authorization"=>"AWS abc:nvkrlq4wor1qbFXZh6rHnAbiRjk=",
164
+ 'date' => @time,
165
+ 'url' => "/bucket/the-key"}, fake_response("success!"))
166
+
167
+ @item = Happening::S3::Item.new('bucket', 'the-key', :aws_access_key_id => 'abc', :aws_secret_access_key => '123')
168
+ run_in_em_loop do
169
+ @item.delete
170
+
171
+ EM.add_timer(1) {
172
+ EM.stop_event_loop
173
+ assert_equal 1, EventMachine::MockHttpRequest.count('https://bucket.s3.amazonaws.com:443/the-key', :delete, {
174
+ "Authorization"=>"AWS abc:nvkrlq4wor1qbFXZh6rHnAbiRjk=",
175
+ 'date' => @time,
176
+ 'url' => "/bucket/the-key"})
177
+ assert_equal 1, EventMachine::MockHttpRequest.count('https://bucket.s3-external-3.amazonaws.com:443/the-key', :delete, {
178
+ "Authorization"=>"AWS abc:nvkrlq4wor1qbFXZh6rHnAbiRjk=",
179
+ 'date' => @time,
180
+ 'url' => "/bucket/the-key"})
181
+ }
182
+
183
+ end
184
+ end
185
+
186
+ should "handle retry" do
187
+ EventMachine::MockHttpRequest.register('https://bucket.s3.amazonaws.com:443/the-key', :delete, {
188
+ "Authorization"=>"AWS abc:nvkrlq4wor1qbFXZh6rHnAbiRjk=",
189
+ 'date' => @time,
190
+ 'url' => "/bucket/the-key"}, error_response(400))
191
+
192
+ @item = Happening::S3::Item.new('bucket', 'the-key', :aws_access_key_id => 'abc', :aws_secret_access_key => '123')
193
+ run_in_em_loop do
194
+ @item.delete
195
+
196
+ EM.add_timer(1) {
197
+ EM.stop_event_loop
198
+ assert_equal 5, EventMachine::MockHttpRequest.count('https://bucket.s3.amazonaws.com:443/the-key', :delete, {
199
+ "Authorization"=>"AWS abc:nvkrlq4wor1qbFXZh6rHnAbiRjk=",
200
+ 'date' => @time,
201
+ 'url' => "/bucket/the-key"})
202
+ }
203
+
204
+ end
205
+ end
206
+ end
207
+
208
+ context "when saving an item" do
209
+
210
+ should "post to the desired location" do
211
+ EventMachine::MockHttpRequest.register('https://bucket.s3.amazonaws.com:443/the-key', :put, {
212
+ "Authorization"=>"AWS abc:lZMKxGDKcQ1PH8yjbpyN7o2sPWg=",
213
+ 'date' => @time,
214
+ 'url' => "/bucket/the-key"}, fake_response("data-here"))
215
+
216
+ @item = Happening::S3::Item.new('bucket', 'the-key', :aws_access_key_id => 'abc', :aws_secret_access_key => '123')
217
+ run_in_em_loop do
218
+ @item.put('content')
219
+
220
+ EM.add_timer(1) {
221
+ EM.stop_event_loop
222
+ assert_equal 1, EventMachine::MockHttpRequest.count('https://bucket.s3.amazonaws.com:443/the-key', :put, {
223
+ "Authorization"=>"AWS abc:lZMKxGDKcQ1PH8yjbpyN7o2sPWg=",
224
+ 'date' => @time,
225
+ 'url' => "/bucket/the-key"})
226
+ }
227
+
228
+ end
229
+ end
230
+
231
+ should "set the desired permissions" do
232
+ EventMachine::MockHttpRequest.register('https://bucket.s3.amazonaws.com:443/the-key', :put, {
233
+ "Authorization"=>"AWS abc:cqkfX+nC7WIkYD+yWaUFuoRuePA=",
234
+ 'date' => @time,
235
+ 'url' => "/bucket/the-key",
236
+ "x-amz-acl" => 'public-read'}, fake_response("data-here"))
237
+
238
+ @item = Happening::S3::Item.new('bucket', 'the-key', :aws_access_key_id => 'abc', :aws_secret_access_key => '123' , :permissions => 'public-read')
239
+ run_in_em_loop do
240
+ @item.put('content')
241
+
242
+ EM.add_timer(1) {
243
+ EM.stop_event_loop
244
+ assert_equal 1, EventMachine::MockHttpRequest.count('https://bucket.s3.amazonaws.com:443/the-key', :put, {
245
+ "Authorization"=>"AWS abc:cqkfX+nC7WIkYD+yWaUFuoRuePA=",
246
+ 'date' => @time,
247
+ 'url' => "/bucket/the-key",
248
+ 'x-amz-acl' => 'public-read'})
249
+ }
250
+
251
+ end
252
+ end
253
+
254
+ should "re-post to a new location" do
255
+ EventMachine::MockHttpRequest.register('https://bucket.s3.amazonaws.com:443/the-key', :put, {
256
+ "Authorization"=>"AWS abc:lZMKxGDKcQ1PH8yjbpyN7o2sPWg=",
257
+ 'date' => @time,
258
+ 'url' => "/bucket/the-key"}, redirect_response('https://bucket.s3-external-3.amazonaws.com/the-key'))
259
+ EventMachine::MockHttpRequest.register('https://bucket.s3-external-3.amazonaws.com:443/the-key', :put, {
260
+ "Authorization"=>"AWS abc:lZMKxGDKcQ1PH8yjbpyN7o2sPWg=",
261
+ 'date' => @time,
262
+ 'url' => "/bucket/the-key"}, fake_response('Thanks!'))
263
+
264
+ @item = Happening::S3::Item.new('bucket', 'the-key', :aws_access_key_id => 'abc', :aws_secret_access_key => '123')
265
+ run_in_em_loop do
266
+ @item.put('content')
267
+
268
+ EM.add_timer(1) {
269
+ EM.stop_event_loop
270
+ assert_equal 1, EventMachine::MockHttpRequest.count('https://bucket.s3.amazonaws.com:443/the-key', :put, {
271
+ "Authorization"=>"AWS abc:lZMKxGDKcQ1PH8yjbpyN7o2sPWg=",
272
+ 'date' => @time,
273
+ 'url' => "/bucket/the-key"})
274
+
275
+ assert_equal 1, EventMachine::MockHttpRequest.count('https://bucket.s3-external-3.amazonaws.com:443/the-key', :put, {
276
+ "Authorization"=>"AWS abc:lZMKxGDKcQ1PH8yjbpyN7o2sPWg=",
277
+ 'date' => @time,
278
+ 'url' => "/bucket/the-key"})
279
+ }
280
+
281
+ end
282
+ end
283
+
284
+ should "retry on error" do
285
+ EventMachine::MockHttpRequest.register('https://bucket.s3.amazonaws.com:443/the-key', :put, {
286
+ "Authorization"=>"AWS abc:lZMKxGDKcQ1PH8yjbpyN7o2sPWg=",
287
+ 'date' => @time,
288
+ 'url' => "/bucket/the-key"}, error_response(400))
289
+
290
+ @item = Happening::S3::Item.new('bucket', 'the-key', :aws_access_key_id => 'abc', :aws_secret_access_key => '123')
291
+ run_in_em_loop do
292
+ @item.put('content')
293
+
294
+ EM.add_timer(1) {
295
+ EM.stop_event_loop
296
+ assert_equal 5, EventMachine::MockHttpRequest.count('https://bucket.s3.amazonaws.com:443/the-key', :put, {
297
+ "Authorization"=>"AWS abc:lZMKxGDKcQ1PH8yjbpyN7o2sPWg=",
298
+ 'date' => @time,
299
+ 'url' => "/bucket/the-key"})
300
+ }
301
+
302
+ end
303
+ end
304
+
305
+ should "call error handler after retry reached" do
306
+ EventMachine::MockHttpRequest.register('https://bucket.s3.amazonaws.com:443/the-key', :put, {
307
+ "Authorization"=>"AWS abc:lZMKxGDKcQ1PH8yjbpyN7o2sPWg=",
308
+ 'date' => @time,
309
+ 'url' => "/bucket/the-key"}, error_response(400))
310
+
311
+ called = false
312
+ on_error = Proc.new {|http| called = true}
313
+
314
+ @item = Happening::S3::Item.new('bucket', 'the-key', :aws_access_key_id => 'abc', :aws_secret_access_key => '123', :retry_count => 1, :on_error => on_error)
315
+ run_in_em_loop do
316
+ @item.put('content')
317
+
318
+ EM.add_timer(1) {
319
+ EM.stop_event_loop
320
+ assert called
321
+ assert_equal 2, EventMachine::MockHttpRequest.count('https://bucket.s3.amazonaws.com:443/the-key', :put, {
322
+ "Authorization"=>"AWS abc:lZMKxGDKcQ1PH8yjbpyN7o2sPWg=",
323
+ 'date' => @time,
324
+ 'url' => "/bucket/the-key"})
325
+ }
326
+
327
+ end
328
+ end
329
+
330
+ end
331
+
332
+ end
333
+ end
@@ -0,0 +1,111 @@
1
+ require 'rubygems'
2
+
3
+ require 'test/unit'
4
+ require 'shoulda'
5
+ require 'mocha'
6
+
7
+ $:.unshift(File.dirname(__FILE__) + "/../")
8
+
9
+ require 'happening'
10
+
11
+ require 'em-http/mock'
12
+
13
+ EventMachine.instance_eval do
14
+ # Switching out EM's defer since it makes tests just a tad more unreliable
15
+ alias :defer_original :defer
16
+ def defer
17
+ yield
18
+ end
19
+ end unless EM.respond_to?(:defer_original)
20
+
21
+ class Test::Unit::TestCase
22
+ def setup
23
+ EventMachine::MockHttpRequest.reset_counts!
24
+ EventMachine::MockHttpRequest.reset_registry!
25
+ end
26
+
27
+ def run_in_em_loop
28
+ EM.run {
29
+ yield
30
+ }
31
+ end
32
+ end
33
+
34
+ module Happening
35
+ module S3
36
+ class Item
37
+ def http_class
38
+ EventMachine::MockHttpRequest
39
+ end
40
+ end
41
+ end
42
+ end
43
+
44
+ def fake_response(data)
45
+ <<-HEREDOC
46
+ HTTP/1.0 200 OK
47
+ Date: Mon, 16 Nov 2009 20:39:15 GMT
48
+ Expires: -1
49
+ Cache-Control: private, max-age=0
50
+ Content-Type: text/html; charset=ISO-8859-1
51
+ Set-Cookie: PREF=ID=9454187d21c4a6a6:TM=1258403955:LM=1258403955:S=2-mf1n5oV5yAeT9-; expires=Wed, 16-Nov-2011 20:39:15 GMT; path=/; domain=.google.ca
52
+ Set-Cookie: NID=28=lvxxVdiBQkCetu_WFaUxLyB7qPlHXS5OdAGYTqge_laVlCKVN8VYYeVBh4bNZiK_Oan2gm8oP9GA-FrZfMPC3ZMHeNq37MG2JH8AIW9LYucU8brOeuggMEbLNNXuiWg4; expires=Tue, 18-May-2010 20:39:15 GMT; path=/; domain=.google.ca; HttpOnly
53
+ Server: gws
54
+ X-XSS-Protection: 0
55
+ X-Cache: MISS from .
56
+ Via: 1.0 .:80 (squid)
57
+ Connection: close
58
+
59
+ #{data}
60
+ HEREDOC
61
+ end
62
+
63
+ # amazon tells us to upload to another location, e.g. happening-benchmark.s3-external-3.amazonaws.com instead of happening-benchmark.s3.amazonaws.com
64
+ def redirect_response(location)
65
+ <<-HEREDOC
66
+ HTTP/1.0 301 Moved Permanently
67
+ Date: Mon, 16 Nov 2009 20:39:15 GMT
68
+ Expires: -1
69
+ Cache-Control: private, max-age=0
70
+ Content-Type: text/html; charset=ISO-8859-1
71
+ Via: 1.0 .:80 (squid)
72
+ Connection: close
73
+ Location: #{location}
74
+
75
+ <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Error><Code>TemporaryRedirect</Code><Message>Please re-send this request to the specified temporary endpoint. Continue to use the original request endpoint for future requests.</Message><RequestId>137D5486D66095AE</RequestId><Bucket>happening-benchmark</Bucket><HostId>Nyk+Zq9GbtxcspdbKDWyGhsZhyUZquZP55tteYef4QVodsn73HUUad0xrIeD09lF</HostId><Endpoint>#{location}</Endpoint></Error>
76
+ HEREDOC
77
+ end
78
+
79
+ def error_response(error_code)
80
+ <<-HEREDOC
81
+ HTTP/1.0 #{error_code} OK
82
+ Date: Mon, 16 Nov 2009 20:39:15 GMT
83
+ Content-Type: text/html; charset=ISO-8859-1
84
+ Connection: close
85
+
86
+ <?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<Error><Code>TemporaryRedirect</Code><Message>Please re-send this request to the specified temporary endpoint. Continue to use the original request endpoint for future requests.</Message><RequestId>137D5486D66095AE</RequestId><Bucket>happening-benchmark</Bucket><HostId>Nyk+Zq9GbtxcspdbKDWyGhsZhyUZquZP55tteYef4QVodsn73HUUad0xrIeD09lF</HostId><Endpoint>https://s3.amazonaws.com</Endpoint></Error>
87
+ HEREDOC
88
+ end
89
+
90
+ module EventMachine
91
+ class MockHttpRequest
92
+ @@pass_through_requests = false
93
+ end
94
+ end
95
+ # def send_request(&blk)
96
+ # # raise @options.inspect
97
+ # query = "#{@uri.scheme}://#{@uri.host}:#{@uri.port}#{encode_query(@uri.path, @options[:query], @uri.query)}"
98
+ # cache_key = query + @options.to_s
99
+ # if s = @@registry[cache_key] and fake = s[@method]
100
+ # @@registry_count[cache_key][@method] += 1
101
+ # client = FakeHttpClient.new(nil)
102
+ # client.setup(fake, @uri)
103
+ # client
104
+ # elsif @@pass_through_requests
105
+ # real_send_request
106
+ # else
107
+ # raise "this request #{query} for method #{@method} isn't registered, and pass_through_requests is current set to false"
108
+ # end
109
+ # end
110
+ # end
111
+ # end
metadata ADDED
@@ -0,0 +1,81 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: happening
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Jonathan Weiss
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-02-26 00:00:00 +01:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: em-http-request
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: activesupport
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ description: An EventMachine based S3 client - using em-http-request
36
+ email: info@peritor.com
37
+ executables: []
38
+
39
+ extensions: []
40
+
41
+ extra_rdoc_files:
42
+ - LICENSE.txt
43
+ - README.md
44
+ files:
45
+ - LICENSE.txt
46
+ - README.md
47
+ - lib/aws.rb
48
+ - lib/log.rb
49
+ - lib/s3/item.rb
50
+ has_rdoc: true
51
+ homepage: http://github.com/peritor/happening
52
+ licenses: []
53
+
54
+ post_install_message:
55
+ rdoc_options:
56
+ - --charset=UTF-8
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: "0"
64
+ version:
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: "0"
70
+ version:
71
+ requirements: []
72
+
73
+ rubyforge_project:
74
+ rubygems_version: 1.3.5
75
+ signing_key:
76
+ specification_version: 3
77
+ summary: An EventMachine based S3 client
78
+ test_files:
79
+ - test/aws_test.rb
80
+ - test/s3/item_test.rb
81
+ - test/test_helper.rb