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 ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.rvmrc ADDED
@@ -0,0 +1 @@
1
+ rvm 'ruby-1.9.3-p125@em_s3'
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in em_s3.gemspec
4
+ gemspec
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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
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
+ }
@@ -0,0 +1,3 @@
1
+ module EmS3
2
+ VERSION = "0.0.1"
3
+ end
data/lib/em_s3.rb ADDED
@@ -0,0 +1,4 @@
1
+ require "em_s3/version"
2
+ require 'base64'
3
+ require_relative 's3_interface'
4
+ require_relative 's3_agent'
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
@@ -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: []