happening 0.0.1

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/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