em_s3 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/.gitignore +17 -0
- data/.rvmrc +1 -0
- data/Gemfile +4 -0
- data/LICENSE +22 -0
- data/README +11 -0
- data/README.md +29 -0
- data/Rakefile +2 -0
- data/em_s3.gemspec +19 -0
- data/examples/agent.rb +22 -0
- data/examples/read_write.rb +17 -0
- data/lib/em_s3/version.rb +3 -0
- data/lib/em_s3.rb +4 -0
- data/lib/s3_agent.rb +69 -0
- data/lib/s3_interface.rb +134 -0
- metadata +75 -0
data/.gitignore
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm 'ruby-1.9.3-p125@em_s3'
|
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Paul Victor Raj
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README
ADDED
@@ -0,0 +1,11 @@
|
|
1
|
+
S3Interface :
|
2
|
+
This is a general purpose S3 upload/download library using EM::Deferrables which can retry while accessing from S3. Who doesn't want to retry when they get a 5xx from S3?
|
3
|
+
|
4
|
+
S3Agent :
|
5
|
+
A serialization framework on top of S3Interface which could possibly occur when you are `put`ting objects in S3 in a reactor loop. Crude but works.
|
6
|
+
|
7
|
+
Caveats :
|
8
|
+
* Doesn't (yet) run the event loop. I developed this when I was working on a Thin based app server. Future versions may have support for running an event loop.
|
9
|
+
* Works only for get_object and put_object. More methods coming soon.
|
10
|
+
* Do not define errbacks on instances of S3Interface. It uses errbacks to retry and __always__ succeeds and responds with an error code in case of an error.
|
11
|
+
* Feel free to fork and modify.
|
data/README.md
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# EmS3
|
2
|
+
|
3
|
+
TODO: Write a gem description
|
4
|
+
|
5
|
+
## Installation
|
6
|
+
|
7
|
+
Add this line to your application's Gemfile:
|
8
|
+
|
9
|
+
gem 'em_s3'
|
10
|
+
|
11
|
+
And then execute:
|
12
|
+
|
13
|
+
$ bundle
|
14
|
+
|
15
|
+
Or install it yourself as:
|
16
|
+
|
17
|
+
$ gem install em_s3
|
18
|
+
|
19
|
+
## Usage
|
20
|
+
|
21
|
+
TODO: Write usage instructions here
|
22
|
+
|
23
|
+
## Contributing
|
24
|
+
|
25
|
+
1. Fork it
|
26
|
+
2. Create your feature branch (`git checkout -b my-new-feature`)
|
27
|
+
3. Commit your changes (`git commit -am 'Added some feature'`)
|
28
|
+
4. Push to the branch (`git push origin my-new-feature`)
|
29
|
+
5. Create new Pull Request
|
data/Rakefile
ADDED
data/em_s3.gemspec
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require File.expand_path('../lib/em_s3/version', __FILE__)
|
3
|
+
|
4
|
+
Gem::Specification.new do |gem|
|
5
|
+
gem.authors = ["Paul Victor Raj"]
|
6
|
+
gem.email = ["paulvictor@gmail.com"]
|
7
|
+
gem.description = %q{Paul Victor Raj}
|
8
|
+
gem.summary = %q{Enables evented access to S3 get and put interface}
|
9
|
+
gem.homepage = ""
|
10
|
+
|
11
|
+
gem.add_dependency('em-http-request', '>=1.0.2')
|
12
|
+
|
13
|
+
gem.files = `git ls-files`.split($\)
|
14
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
15
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
16
|
+
gem.name = "em_s3"
|
17
|
+
gem.require_paths = ["lib"]
|
18
|
+
gem.version = EmS3::VERSION
|
19
|
+
end
|
data/examples/agent.rb
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
EM.run{
|
2
|
+
s3a = S3Agent.new
|
3
|
+
s3a.set_keys('public_key', 'private_key')
|
4
|
+
bucket = "bucket"
|
5
|
+
object = "obj"
|
6
|
+
value = "value"
|
7
|
+
prefix = "prefix"
|
8
|
+
s3a.request_service(:get, bucket, object){|resp, code|
|
9
|
+
if code == 200
|
10
|
+
new_value = prefix + resp
|
11
|
+
elsif code == 404
|
12
|
+
new_value = prefix
|
13
|
+
else
|
14
|
+
s3a.revoke_service(bucket, object)
|
15
|
+
EM.stop
|
16
|
+
end
|
17
|
+
s3a.request_service(:put, bucket, object, new_value){|resp, code|
|
18
|
+
puts "AWS gave a #{code}"
|
19
|
+
EM.stop
|
20
|
+
}
|
21
|
+
}
|
22
|
+
}
|
@@ -0,0 +1,17 @@
|
|
1
|
+
EM.run{
|
2
|
+
s3i = S3Interface.new('your_aws_key', 'your_aws_secret')
|
3
|
+
s3i.callback{|resp, code|
|
4
|
+
puts "AWS replied with #{resp} and #{code}"
|
5
|
+
EM.stop
|
6
|
+
}
|
7
|
+
s3i.put_object('my_bucket', 'foo', 'bar')
|
8
|
+
}
|
9
|
+
|
10
|
+
EM.run{
|
11
|
+
s3i = S3Interface.new('your_aws_key', 'your_aws_secret')
|
12
|
+
s3i.callback{|resp, code|
|
13
|
+
puts "AWS replied with #{resp} and #{code}"
|
14
|
+
EM.stop
|
15
|
+
}
|
16
|
+
s3i.get_object('my_bucket', 'foo')
|
17
|
+
}
|
data/lib/em_s3.rb
ADDED
data/lib/s3_agent.rb
ADDED
@@ -0,0 +1,69 @@
|
|
1
|
+
class S3Agent < EM::Queue
|
2
|
+
include Singleton
|
3
|
+
# Refer to http://eventmachine.rubyforge.org/EventMachine/Queue.html for interface details
|
4
|
+
# This agent class solves the following problem
|
5
|
+
# RMW in S3.
|
6
|
+
# It has a pool of (s3) bucket, object names which are right now being processed.
|
7
|
+
# When a new request comes, we check if its right now sent out to S3.
|
8
|
+
# If yes, add it to the queue and add a pop request which will do the same thing again.
|
9
|
+
# If no, process it
|
10
|
+
# If a new read request for the same (object, bucket) comes, it will cycle through the push/pop cycle.
|
11
|
+
def initialize
|
12
|
+
@public_key = nil
|
13
|
+
@private_key = nil
|
14
|
+
@obj_pools = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def set_keys(public_key, private_key)
|
18
|
+
@public_key ||= public_key
|
19
|
+
@private_key ||= private_key
|
20
|
+
self
|
21
|
+
end
|
22
|
+
|
23
|
+
# Called from clients if due to some reason, they skip over the write part
|
24
|
+
# @param [String] bucket
|
25
|
+
# The name of the bucket
|
26
|
+
# @param [String] object
|
27
|
+
# The name of the object's key
|
28
|
+
# @return [true]
|
29
|
+
def revoke_service(bucket, object)
|
30
|
+
@obj_pools.delete("#{bucket}:#{object}")
|
31
|
+
true
|
32
|
+
end
|
33
|
+
|
34
|
+
# Requests any of :get or :put from S3.
|
35
|
+
# If the object is not being processed now, service it immediately.
|
36
|
+
# If not, push it in a queue and wait for the reactor to take it through.
|
37
|
+
# @param [Symbol] method
|
38
|
+
# The HTTP method in lower case. Either :get or :put
|
39
|
+
# @param [String] bucket
|
40
|
+
# The name of the bucket
|
41
|
+
# @param [String] object
|
42
|
+
# The name of the object's key
|
43
|
+
# @param [String] value
|
44
|
+
# The object's value
|
45
|
+
# @return [true]
|
46
|
+
def request_service(method, bucket, object, value = nil, &blk)
|
47
|
+
# Allow `put`s to go through.
|
48
|
+
# For `get`s, check if the object is not in use and only then, allow to pass though
|
49
|
+
if (!@obj_pools["#{bucket}:#{object}"]) || (method == :put)
|
50
|
+
# Some client is using the agent.
|
51
|
+
@obj_pools["#{bucket}:#{object}"] = true
|
52
|
+
s3i = S3Interface.new(@public_key, @private_key)
|
53
|
+
s3i.callback{|resp, status|
|
54
|
+
# Unblock the other client only if this is a write
|
55
|
+
revoke_service(bucket, object) if method == :put
|
56
|
+
yield resp, status if block_given?
|
57
|
+
}
|
58
|
+
method == :get ? s3i.get_object(bucket, object) : s3i.put_object(bucket, object, value)
|
59
|
+
else
|
60
|
+
push({:bucket => bucket, :object => object, :value => value})
|
61
|
+
pop{|request|
|
62
|
+
request_service(request[:bucket], request[:object], request[:value]){|resp, status|
|
63
|
+
yield resp, status if block_given?
|
64
|
+
}
|
65
|
+
}
|
66
|
+
end
|
67
|
+
true
|
68
|
+
end
|
69
|
+
end
|
data/lib/s3_interface.rb
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
class S3Interface
|
2
|
+
# Do not use the interfaces' errback to define custom events.
|
3
|
+
# Errbacks are used internally to retry.
|
4
|
+
include EM::Deferrable
|
5
|
+
# Credits to http://forrst.com/posts/Basic_Amazon_S3_Upload_via_PUT_Ruby_Class-4t6
|
6
|
+
# Refer to the following for S3 documentation
|
7
|
+
# http://docs.amazonwebservices.com/AmazonS3/latest/dev/RESTAuthentication.html#RESTAuthenticationConstructingCanonicalizedAmzHeaders
|
8
|
+
# http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTObjectPUT.html
|
9
|
+
# http://docs.amazonwebservices.com/AmazonS3/latest/API/RESTObjectGET.html
|
10
|
+
|
11
|
+
attr_accessor :public_key, :private_key
|
12
|
+
|
13
|
+
def initialize(public_key, private_key, options = {})
|
14
|
+
@public_key = public_key
|
15
|
+
@private_key = private_key
|
16
|
+
@options = options.merge({:retry_count => 3})
|
17
|
+
@num_tries = 0
|
18
|
+
errback{|resp, status|
|
19
|
+
if (@num_tries += 1) < retry_count
|
20
|
+
retry_request
|
21
|
+
else
|
22
|
+
succeed resp, status
|
23
|
+
end
|
24
|
+
}
|
25
|
+
end
|
26
|
+
|
27
|
+
# Puts any of the objects into S3 buckets
|
28
|
+
# @param [String] bucket
|
29
|
+
# The name of the bucket
|
30
|
+
# @param [String] object
|
31
|
+
# The name of the object's key
|
32
|
+
# @param [String] value
|
33
|
+
# The object's value
|
34
|
+
# @param [String] content_type
|
35
|
+
# The value's MIME type
|
36
|
+
# @return [self]
|
37
|
+
def put_object(bucket, object, value, content_type = 'binary/octet-stream')
|
38
|
+
date = generate_date
|
39
|
+
sign_string = generate_signed_string('PUT', 'private', bucket, object, content_type)
|
40
|
+
signature = generate_signature(sign_string)
|
41
|
+
auth = generate_auth(signature)
|
42
|
+
headers = generate_put_headers(date, auth, 'private', content_type, value.size)
|
43
|
+
path = "/" << object
|
44
|
+
|
45
|
+
@req_options = {:method => :put, :head => headers, :path => path, :body => value}
|
46
|
+
@bucket = bucket
|
47
|
+
try_request
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
# Gets any of the objects from S3 buckets
|
52
|
+
# @param [String] bucket
|
53
|
+
# The name of the bucket
|
54
|
+
# @param [String] object
|
55
|
+
# The name of the object's key
|
56
|
+
# @return [self]
|
57
|
+
def get_object(bucket, object)
|
58
|
+
date = generate_date
|
59
|
+
sign_string = generate_signed_string('GET', nil, bucket, object, 'text/plain')
|
60
|
+
signature = generate_signature(sign_string)
|
61
|
+
auth = generate_auth(signature)
|
62
|
+
headers = generate_get_headers(date, auth, 'text/plain')
|
63
|
+
path = "/" << object
|
64
|
+
|
65
|
+
@req_options = {:method => :get, :head => headers, :path => path}
|
66
|
+
@bucket = bucket
|
67
|
+
try_request
|
68
|
+
self
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
# Perhaps an inappropriate method name, sonce we call it even the first time
|
74
|
+
def retry_request
|
75
|
+
# Explore persistent connections from within AWS
|
76
|
+
s3_conn = EM::HttpRequest.new("http://#{@bucket}.s3.amazonaws.com")
|
77
|
+
req_method = @req_options[:method]
|
78
|
+
s3_req = s3_conn.send(req_method, @req_options)
|
79
|
+
s3_req.callback{|cli|
|
80
|
+
if cli.response_header.http_status < 500
|
81
|
+
self.succeed cli.response, cli.response_header.http_status
|
82
|
+
else # Some S3 issue
|
83
|
+
self.fail cli.response, cli.response_header.http_status
|
84
|
+
end
|
85
|
+
}
|
86
|
+
s3_req.errback{|cli|
|
87
|
+
self.fail nil, nil
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
alias :try_request :retry_request
|
92
|
+
|
93
|
+
def generate_date
|
94
|
+
Time.now.httpdate
|
95
|
+
end
|
96
|
+
|
97
|
+
def generate_signed_string(request_type, access, bucket, object, content_type = 'binary/octet-stream')
|
98
|
+
signed_string = ""
|
99
|
+
signed_string << request_type << "\n\n"
|
100
|
+
if content_type == nil
|
101
|
+
signed_string << "\n"
|
102
|
+
else
|
103
|
+
signed_string << content_type << "\n"
|
104
|
+
end
|
105
|
+
signed_string << generate_date << "\n"
|
106
|
+
signed_string << "x-amz-acl:" << access << "\n" if access
|
107
|
+
signed_string << "/" << bucket << "/" << object
|
108
|
+
end
|
109
|
+
|
110
|
+
def generate_signature(signed_string)
|
111
|
+
Base64.encode64(OpenSSL::HMAC.digest(OpenSSL::Digest::Digest.new('sha1'), @private_key, signed_string)).gsub("\n", "")
|
112
|
+
end
|
113
|
+
|
114
|
+
def generate_auth(signature)
|
115
|
+
authString = "AWS"
|
116
|
+
authString << " "
|
117
|
+
authString << @public_key
|
118
|
+
authString << ":"
|
119
|
+
authString << signature
|
120
|
+
authString
|
121
|
+
end
|
122
|
+
|
123
|
+
def generate_put_headers(date_string, auth_string, access, content_type = nil, content_length = 0)
|
124
|
+
{ 'Date' => date_string, 'Content-Type' => content_type, 'Content-Length' => content_length.to_s, 'Authorization' => auth_string, 'Expect' => "100-continue", 'x-amz-acl' => access }
|
125
|
+
end
|
126
|
+
|
127
|
+
def generate_get_headers(date_string, auth_string, content_type = 'text/plain')
|
128
|
+
{ 'Date' => date_string, 'Authorization' => auth_string, 'Content-Type' => content_type }
|
129
|
+
end
|
130
|
+
|
131
|
+
def retry_count
|
132
|
+
@options[:retry_count]
|
133
|
+
end
|
134
|
+
end
|
metadata
ADDED
@@ -0,0 +1,75 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: em_s3
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Paul Victor Raj
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-04-11 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: em-http-request
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ! '>='
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 1.0.2
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ! '>='
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 1.0.2
|
30
|
+
description: Paul Victor Raj
|
31
|
+
email:
|
32
|
+
- paulvictor@gmail.com
|
33
|
+
executables: []
|
34
|
+
extensions: []
|
35
|
+
extra_rdoc_files: []
|
36
|
+
files:
|
37
|
+
- .gitignore
|
38
|
+
- .rvmrc
|
39
|
+
- Gemfile
|
40
|
+
- LICENSE
|
41
|
+
- README
|
42
|
+
- README.md
|
43
|
+
- Rakefile
|
44
|
+
- em_s3.gemspec
|
45
|
+
- examples/agent.rb
|
46
|
+
- examples/read_write.rb
|
47
|
+
- lib/em_s3.rb
|
48
|
+
- lib/em_s3/version.rb
|
49
|
+
- lib/s3_agent.rb
|
50
|
+
- lib/s3_interface.rb
|
51
|
+
homepage: ''
|
52
|
+
licenses: []
|
53
|
+
post_install_message:
|
54
|
+
rdoc_options: []
|
55
|
+
require_paths:
|
56
|
+
- lib
|
57
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
58
|
+
none: false
|
59
|
+
requirements:
|
60
|
+
- - ! '>='
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ! '>='
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '0'
|
69
|
+
requirements: []
|
70
|
+
rubyforge_project:
|
71
|
+
rubygems_version: 1.8.21
|
72
|
+
signing_key:
|
73
|
+
specification_version: 3
|
74
|
+
summary: Enables evented access to S3 get and put interface
|
75
|
+
test_files: []
|