rcarvalho-uber-s3 0.2.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,153 @@
1
+ # Uber-S3
2
+
3
+ A simple, but very fast, S3 client for Ruby supporting
4
+ synchronous (net-http) and asynchronous (em+fibers) io.
5
+
6
+
7
+ ## Examples
8
+
9
+ ```ruby
10
+ require 'uber-s3'
11
+
12
+ ##########################################################################
13
+ # Connecting to S3
14
+ # adapter can be :net_http or :em_http_fibered
15
+ s3 = UberS3.new({
16
+ :access_key => 'abc',
17
+ :secret_access_key => 'def',
18
+ :bucket => 'funbucket',
19
+ :adapter => :em_http_fibered
20
+ })
21
+
22
+
23
+
24
+ ##########################################################################
25
+ # Saving objects
26
+ s3.store('/test.txt', 'Look ma no hands')
27
+ s3.store('test2.txt', 'Hey hey', :access => :public_read)
28
+
29
+ o = s3.object('/test.txt')
30
+ o.value = 'Look ma no hands'
31
+ o.save
32
+
33
+ # or..
34
+
35
+ o = UberS3::Object.new(s3.bucket, '/test.txt', 'heyo')
36
+ o.save # => true
37
+
38
+
39
+
40
+ ##########################################################################
41
+ # Reading objects
42
+ s3['/test.txt'].class # => UberS3::Object
43
+ s3['/test.txt'].value # => 'heyo'
44
+ s3.get('/test.txt').value # => 'heyo'
45
+
46
+ s3.exists?('/anotherone') # => false
47
+
48
+
49
+
50
+ ##########################################################################
51
+ # Object access control
52
+
53
+ o.access = :private
54
+ o.access = :public_read
55
+ # etc.
56
+
57
+ # Valid options:
58
+ # :private, :public_read, :public_read_write, :authenticated_read
59
+
60
+ # See http://docs.amazonwebservices.com/AmazonS3/2006-03-01/dev/index.html?RESTAccessPolicy.html
61
+ # NOTE: default object access level is :private
62
+
63
+
64
+
65
+ ##########################################################################
66
+ # Deleting objects
67
+ o.delete # => true
68
+
69
+
70
+ ##########################################################################
71
+ # Save optional parameters
72
+ # See http://docs.amazonwebservices.com/AmazonS3/latest/API/index.html?RESTObjectPUT.html
73
+
74
+ options = { :access => :public_read, :content_type => 'text/plain' }
75
+ o = UberS3::Object.new(client.bucket, '/test.txt', 'heyo', options)
76
+ o.save
77
+
78
+ # or..
79
+
80
+ o = s3.object('/test.txt')
81
+ o.value = 'Look ma no hands'
82
+ o.access = :public_read
83
+ o.content_type = 'text/plain'
84
+ o.save
85
+
86
+ # List of parameter methods:
87
+ # :access -- Object access control
88
+ # :cache_control -- http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.9
89
+ # :content_disposition -- http://www.w3.org/Protocols/rfc2616/rfc2616-sec19.html#sec19.5.1
90
+ # :content_encoding -- http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11
91
+ # :content_md5 -- End-to-end integrity check
92
+ # :content_type -- http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.17
93
+ # :expires -- Number of milliseconds before expiration
94
+ # :storage_class -- Amazon S3's storage levels (redundancy for price)
95
+
96
+
97
+ ##########################################################################
98
+ # Iterating objects in a bucket
99
+ s3.objects('/path').each {|obj| puts obj }
100
+
101
+ ```
102
+
103
+ ## Ruby version notes
104
+
105
+ * Tested on MRI 1.9.2, MRI 1.9.3 (net_http / em_http_fibered adapters)
106
+ * Tested on JRuby 1.7-dev in 1.9 mode (net_http)
107
+ * Ruby 1.8.7 works for net/http clients, em_http_fibered adapter requires fibers
108
+
109
+ ## Other notes
110
+
111
+ * If Nokogiri is available, it will be automatically used instead of REXML
112
+
113
+ ## TODO
114
+
115
+ * Refactor UberS3::Object class, consider putting the operations/headers into separate classes
116
+ * Better exception handling and reporting
117
+ * Query string authentication -- neat feature for providing temporary public access to a private object
118
+ * Object versioning support
119
+
120
+ ## Benchmarks
121
+
122
+ Benchmarks were run with a speedy MBP on a 10Mbit connection
123
+
124
+ ### Saving lots of 1KB files
125
+
126
+ <pre>
127
+ user system total real
128
+ saving 100x1024 byte objects (net-http) 0.160000 0.080000 0.240000 ( 26.128499)
129
+ saving 100x1024 byte objects (em-http-fibered) 0.080000 0.030000 0.110000 ( 0.917334)
130
+ </pre>
131
+
132
+ ### Saving lots of 500KB files
133
+
134
+ <pre>
135
+ user system total real
136
+ saving 100x512000 byte objects (net-http) 0.190000 0.740000 0.930000 ( 91.559123)
137
+ saving 100x512000 byte objects (em-http-fibered) 0.230000 0.700000 0.930000 ( 45.119033)
138
+ </pre>
139
+
140
+ ### Conclusion
141
+
142
+ Yea... async adapter dominates. The 100x1KB files were 29x faster to upload, and the 100x500KB files were only 2x faster, but that is because my upload bandwidth was tapped.
143
+
144
+
145
+ ## S3 API Docs
146
+
147
+ - S3 REST API: http://docs.amazonwebservices.com/AmazonS3/latest/API/
148
+ - S3 Request Authorization: http://docs.amazonwebservices.com/AmazonS3/latest/dev/RESTAuthentication.html
149
+
150
+
151
+ ## License
152
+
153
+ MIT License - Copyright (c) 2012 Nulayer Inc.
@@ -0,0 +1,45 @@
1
+ require 'cgi'
2
+ require 'time'
3
+ require 'openssl'
4
+ require 'forwardable'
5
+ require 'base64'
6
+ require 'digest/md5'
7
+ require 'zlib'
8
+ require 'stringio'
9
+ require 'mime/types'
10
+
11
+ class UberS3
12
+ extend Forwardable
13
+
14
+ attr_reader :connection
15
+ attr_accessor :bucket, :options
16
+ def_delegators :@bucket, :store, :set, :object, :get, :head, :[], :exists?, :objects
17
+
18
+ def initialize(options={})
19
+ self.options = options
20
+ self.bucket = options[:bucket]
21
+ end
22
+
23
+ def inspect
24
+ "#<UberS3 client v#{UberS3::VERSION}>"
25
+ end
26
+
27
+ def connection
28
+ Thread.current['[uber-s3]:connection'] ||= Connection.open(self, options)
29
+ end
30
+
31
+ def bucket=(bucket)
32
+ @bucket = bucket.is_a?(String) ? Bucket.new(self, bucket) : bucket
33
+ end
34
+ end
35
+
36
+ require 'uber-s3/version'
37
+ require 'uber-s3/error'
38
+ require 'uber-s3/response'
39
+ require 'uber-s3/connection'
40
+ require 'uber-s3/authorization'
41
+ require 'uber-s3/operation'
42
+ require 'uber-s3/bucket'
43
+ require 'uber-s3/object'
44
+
45
+ require 'uber-s3/util/xml_document'
@@ -0,0 +1,29 @@
1
+ class UberS3
2
+ module Authorization
3
+
4
+ def sign(client, verb, path, headers={})
5
+ req_verb = verb.to_s.upcase
6
+ req_content_md5 = headers['Content-MD5']
7
+ req_content_type = headers['Content-Type']
8
+ req_date = headers['Date']
9
+ req_canonical_resource = "/#{client.bucket}/#{path}".split('?').first
10
+ req_canonical_amz_headers = ''
11
+
12
+ headers.keys.sort.select {|k| k =~ /^x-amz-/ }.each do |amz_key|
13
+ req_canonical_amz_headers << amz_key+':'+headers[amz_key]+"\n"
14
+ end
15
+
16
+ canonical_string_to_sign = "#{req_verb}\n"+
17
+ "#{req_content_md5}\n"+
18
+ "#{req_content_type}\n"+
19
+ "#{req_date}\n"+
20
+ "#{req_canonical_amz_headers}"+
21
+ "#{req_canonical_resource}"
22
+
23
+ digest = OpenSSL::Digest::Digest.new('sha1')
24
+ [OpenSSL::HMAC.digest(digest, client.connection.secret_access_key, canonical_string_to_sign)].pack("m").strip
25
+ end
26
+
27
+ extend self
28
+ end
29
+ end
@@ -0,0 +1,100 @@
1
+ class UberS3
2
+ class Bucket
3
+ attr_accessor :s3, :name
4
+
5
+ def initialize(s3, name)
6
+ self.s3 = s3
7
+ self.name = name
8
+ end
9
+
10
+ def to_s
11
+ name.to_s
12
+ end
13
+
14
+ def connection
15
+ s3.connection
16
+ end
17
+
18
+ def store(key, value, options={})
19
+ Object.new(self, key, value, options).save
20
+ end
21
+ alias_method :set, :store
22
+
23
+ def object(key)
24
+ Object.new(self, key)
25
+ end
26
+ alias_method :[], :object
27
+
28
+ def get(key)
29
+ object(key).fetch
30
+ end
31
+
32
+ def head(key)
33
+ object(key).head
34
+ end
35
+
36
+ def exists?(key)
37
+ object(key).exists?
38
+ end
39
+
40
+ def objects(key, options={})
41
+ ObjectList.new(self, key, options)
42
+ end
43
+
44
+
45
+ class ObjectList
46
+ include Enumerable
47
+
48
+ attr_accessor :bucket, :key, :options, :objects
49
+
50
+ def initialize(bucket, key, options={})
51
+ self.bucket = bucket
52
+ self.key = key.gsub(/^\//, '')
53
+ self.options = options
54
+ self.objects = []
55
+ end
56
+
57
+ def fetch(marker=nil)
58
+ @objects = []
59
+
60
+ default_max_keys = 500
61
+ response = bucket.connection.get("/?prefix=#{CGI.escape(key)}&marker=#{marker}&max-keys=#{default_max_keys}")
62
+
63
+ @objects = parse_contents(response.body)
64
+ end
65
+
66
+ def parse_contents(xml)
67
+ objects = []
68
+ doc = Util::XmlDocument.new(xml)
69
+
70
+ # TODO: can use more error checking on the xml stuff
71
+
72
+ @is_truncated = doc.xpath('//ListBucketResult/IsTruncated').first.text == "true"
73
+ contents = doc.xpath('//ListBucketResult/Contents')
74
+
75
+ contents.each do |content|
76
+ h = {}
77
+ content.elements.each {|el| h[el.name] = el.text }
78
+ objects << ::UberS3::Object.new(bucket, h['Key'], nil, { :size => h['Size'].to_i })
79
+ end if contents.any?
80
+
81
+ objects
82
+ end
83
+
84
+ def each(&block)
85
+ loop do
86
+ marker = objects.last.key rescue nil
87
+ fetch(marker)
88
+
89
+ objects.each {|obj| block.call(obj) }
90
+ break if @is_truncated == false
91
+ end
92
+ end
93
+
94
+ def to_a
95
+ fetch
96
+ end
97
+ end
98
+
99
+ end
100
+ end
@@ -0,0 +1,72 @@
1
+ class UberS3
2
+ module Connection
3
+
4
+ def self.open(s3, options={})
5
+ adapter = options.delete(:adapter) || :net_http
6
+
7
+ begin
8
+ require "uber-s3/connection/#{adapter}"
9
+ klass = instance_eval(adapter.to_s.split('_').map {|x| x.capitalize}.join(""))
10
+ rescue LoadError
11
+ raise "Cannot load #{adapter} adapter class"
12
+ end
13
+
14
+ klass.new(s3, options)
15
+ end
16
+
17
+
18
+ class Adapter
19
+
20
+ attr_accessor :s3, :http, :uri, :access_key, :secret_access_key, :defaults
21
+
22
+ def initialize(s3, options={})
23
+ self.s3 = s3
24
+ self.http = nil
25
+ self.uri = nil
26
+ self.access_key = options[:access_key]
27
+ self.secret_access_key = options[:secret_access_key]
28
+ self.defaults = options[:defaults] || {}
29
+ end
30
+
31
+ [:get, :post, :put, :delete, :head].each do |verb|
32
+ define_method(verb) do |*args|
33
+ path, headers, body = args
34
+ path = path.gsub(/^\//, '')
35
+ headers ||= {}
36
+
37
+ # Default headers
38
+ headers['Date'] = Time.now.httpdate if !headers.keys.include?('Date')
39
+ headers['User-Agent'] ||= "UberS3 v#{UberS3::VERSION}"
40
+ headers['Connection'] ||= 'keep-alive'
41
+
42
+ if body
43
+ headers['Content-Length'] ||= body.bytesize.to_s
44
+ end
45
+
46
+ # Authorize the request
47
+ signature = Authorization.sign(s3, verb, path, headers)
48
+ headers['Authorization'] = "AWS #{access_key}:#{signature}"
49
+
50
+ # Make the request
51
+ url = "http://#{s3.bucket}.s3.amazonaws.com/#{path}"
52
+ request(verb, url, headers, body)
53
+ end
54
+ end
55
+
56
+ def uri=(uri)
57
+ # Reset the http connection if the host/port change
58
+ if !@uri.nil? && !(uri.host == @uri.host && uri.port == @uri.port)
59
+ self.http = nil
60
+ end
61
+
62
+ @uri = uri
63
+ end
64
+
65
+ def request(verb, url, headers={}, body=nil)
66
+ raise "Abstract method"
67
+ end
68
+
69
+ end
70
+
71
+ end
72
+ end
@@ -0,0 +1,12 @@
1
+ require 'eventmachine'
2
+ require 'em-http'
3
+
4
+ module UberS3::Connection
5
+ class EmHttp < Adapter
6
+
7
+ # NOTE: this will be very difficult to support
8
+ # with our interface.. will need lots of work.
9
+ # We may only want to support async with fibers.
10
+
11
+ end
12
+ end
@@ -0,0 +1,26 @@
1
+ require 'eventmachine'
2
+ require 'em-http'
3
+ require 'em-synchrony'
4
+ require 'em-synchrony/em-http'
5
+
6
+ module UberS3::Connection
7
+ class EmHttpFibered < Adapter
8
+
9
+ def request(verb, url, headers={}, body=nil)
10
+ params = {}
11
+ params[:head] = headers
12
+ params[:body] = body if body
13
+ # params[:keepalive] = true if persistent # causing issues ...?
14
+
15
+ r = EM::HttpRequest.new(url).send(verb, params)
16
+
17
+ UberS3::Response.new({
18
+ :status => r.response_header.status,
19
+ :header => r.response_header,
20
+ :body => r.response,
21
+ :raw => r
22
+ })
23
+ end
24
+
25
+ end
26
+ end
@@ -0,0 +1,62 @@
1
+ require 'net/http'
2
+
3
+ module UberS3::Connection
4
+ class NetHttp < Adapter
5
+
6
+ def request(verb, url, headers={}, body=nil)
7
+ if verb == :get
8
+ # Support fetching compressed data
9
+ headers['Accept-Encoding'] = 'gzip, deflate'
10
+ end
11
+
12
+ self.uri = URI.parse(url)
13
+
14
+ # Init and open a HTTP connection
15
+ http_connect! if http.nil? || !http.started?
16
+
17
+ req_klass = instance_eval("Net::HTTP::"+verb.to_s.capitalize)
18
+ req = req_klass.new(uri.to_s, headers)
19
+
20
+ req.body = body if !body.nil? && !body.empty?
21
+
22
+ # Make HTTP request
23
+ retries = 2
24
+ begin
25
+ r = http.request(req)
26
+ rescue EOFError, Errno::EPIPE
27
+ # Something happened to our connection, lets try this again
28
+ http_connect!
29
+ retries -= 1
30
+ retry if retries >= 0
31
+ end
32
+
33
+ # Auto-decode any gzipped objects
34
+ if verb == :get && r.header['Content-Encoding'] == 'gzip'
35
+ gz = Zlib::GzipReader.new(StringIO.new(r.body))
36
+ response_body = gz.read
37
+ else
38
+ response_body = r.body
39
+ end
40
+
41
+ UberS3::Response.new({
42
+ :status => r.code.to_i,
43
+ :header => r.header.to_hash,
44
+ :body => response_body,
45
+ :raw => r
46
+ })
47
+ end
48
+
49
+ private
50
+
51
+ def http_connect!
52
+ self.http = Net::HTTP.new(uri.host, uri.port)
53
+ http.start
54
+
55
+ if Socket.const_defined?(:TCP_NODELAY)
56
+ socket = http.instance_variable_get(:@socket)
57
+ socket.io.setsockopt(Socket::IPPROTO_TCP, Socket::TCP_NODELAY, true)
58
+ end
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,89 @@
1
+ class UberS3
2
+
3
+ module Error
4
+
5
+ class Standard < StandardError
6
+ def initialize(key, message)
7
+ super("#{key}: #{message}")
8
+ end
9
+ end
10
+
11
+ class Unknown < StandardError; end
12
+
13
+ class AccessDenied < Standard; end
14
+ class AccountProblem < Standard; end
15
+ class AmbiguousGrantByEmailAddress < Standard; end
16
+ class BadDigest < Standard; end
17
+ class BucketAlreadyExists < Standard; end
18
+ class BucketAlreadyOwnedByYou < Standard; end
19
+ class BucketNotEmpty < Standard; end
20
+ class CredentialsNotSupported < Standard; end
21
+ class CrossLocationLoggingProhibited < Standard; end
22
+ class EntityTooSmall < Standard; end
23
+ class EntityTooLarge < Standard; end
24
+ class ExpiredToken < Standard; end
25
+ class IllegalVersioningConfigurationException < Standard; end
26
+ class IncompleteBody < Standard; end
27
+ class IncorrectNumberOfFilesInPostRequest < Standard; end
28
+ class InlineDataTooLarge < Standard; end
29
+ class InternalError < Standard; end
30
+ class InvalidAccessKeyId < Standard; end
31
+ class InvalidAddressingHeader < Standard; end
32
+ class InvalidArgument < Standard; end
33
+ class InvalidBucketName < Standard; end
34
+ class InvalidDigest < Standard; end
35
+ class InvalidLocationConstraint < Standard; end
36
+ class InvalidPart < Standard; end
37
+ class InvalidPartOrder < Standard; end
38
+ class InvalidPayer < Standard; end
39
+ class InvalidPolicyDocument < Standard; end
40
+ class InvalidRange < Standard; end
41
+ class InvalidRequest < Standard; end
42
+ class InvalidSecurity < Standard; end
43
+ class InvalidSOAPRequest < Standard; end
44
+ class InvalidStorageClass < Standard; end
45
+ class InvalidTargetBucketForLogging < Standard; end
46
+ class InvalidToken < Standard; end
47
+ class InvalidURI < Standard; end
48
+ class KeyTooLong < Standard; end
49
+ class MalformedACLError < Standard; end
50
+ class MalformedPOSTRequest < Standard; end
51
+ class MalformedXML < Standard; end
52
+ class MaxMessageLengthExceeded < Standard; end
53
+ class MaxPostPreDataLengthExceededError < Standard; end
54
+ class MetadataTooLarge < Standard; end
55
+ class MethodNotAllowed < Standard; end
56
+ class MissingAttachment < Standard; end
57
+ class MissingContentLength < Standard; end
58
+ class MissingRequestBodyError < Standard; end
59
+ class MissingSecurityElement < Standard; end
60
+ class MissingSecurityHeader < Standard; end
61
+ class NoLoggingStatusForKey < Standard; end
62
+ class NoSuchBucket < Standard; end
63
+ class NoSuchKey < Standard; end
64
+ class NoSuchUpload < Standard; end
65
+ class NoSuchVersion < Standard; end
66
+ class NotImplemented < Standard; end
67
+ class NotSignedUp < Standard; end
68
+ class NotSuchBucketPolicy < Standard; end
69
+ class OperationAborted < Standard; end
70
+ class PermanentRedirect < Standard; end
71
+ class PreconditionFailed < Standard; end
72
+ class Redirect < Standard; end
73
+ class RequestIsNotMultiPartContent < Standard; end
74
+ class RequestTimeout < Standard; end
75
+ class RequestTimeTooSkewed < Standard; end
76
+ class RequestTorrentOfBucketError < Standard; end
77
+ class SignatureDoesNotMatch < Standard; end
78
+ class ServiceUnavailable < Standard; end
79
+ class SlowDown < Standard; end
80
+ class TemporaryRedirect < Standard; end
81
+ class TokenRefreshRequired < Standard; end
82
+ class TooManyBuckets < Standard; end
83
+ class UnexpectedContent < Standard; end
84
+ class UnresolvableGrantByEmailAddress < Standard; end
85
+ class UserKeyMustBeSpecified < Standard; end
86
+
87
+ end
88
+
89
+ end
@@ -0,0 +1,147 @@
1
+ class UberS3
2
+ class Object
3
+ include Operation::Object::AccessPolicy
4
+ include Operation::Object::ContentDisposition
5
+ include Operation::Object::ContentEncoding
6
+ include Operation::Object::ContentMd5
7
+ include Operation::Object::ContentType
8
+ include Operation::Object::HttpCache
9
+ include Operation::Object::Meta
10
+ include Operation::Object::StorageClass
11
+
12
+ attr_accessor :bucket, :key, :response, :value, :size, :error
13
+
14
+ def initialize(bucket, key, value=nil, options={})
15
+ self.bucket = bucket
16
+ self.key = key
17
+ self.value = value
18
+
19
+ # Init current state
20
+ infer_content_type!
21
+
22
+ # Call operation methods based on options passed
23
+ bucket.connection.defaults.merge(options).each {|k,v| self.send((k.to_s+'=').to_sym, v) }
24
+ end
25
+
26
+ def to_s
27
+ "#<#{self.class} @key=\"#{self.key}\">"
28
+ end
29
+
30
+ def exists?
31
+ # TODO.. refactor this as if we've already called head
32
+ # on the object, there is no need to do it again..
33
+ # perhaps move some things into class methods?
34
+ bucket.connection.head(key).status == 200
35
+ end
36
+
37
+ def head
38
+ self.response = bucket.connection.head(key)
39
+
40
+ parse_response_header!
41
+ self
42
+ end
43
+
44
+ def fetch
45
+ self.response = bucket.connection.get(key)
46
+ self.value = response.body
47
+
48
+ parse_response_header!
49
+ self
50
+ end
51
+
52
+ def save
53
+ headers = {}
54
+
55
+ # Encode data if necessary
56
+ gzip_content!
57
+
58
+ # Standard pass through values
59
+ headers['Content-Disposition'] = content_disposition
60
+ headers['Content-Encoding'] = content_encoding
61
+ headers['Content-Length'] = size.to_s
62
+ headers['Content-Type'] = content_type
63
+ headers['Cache-Control'] = cache_control
64
+ headers['Expires'] = expires
65
+ headers['Pragma'] = pragma
66
+
67
+ headers.each {|k,v| headers.delete(k) if v.nil? || v.empty? }
68
+
69
+ # Content MD5 integrity check
70
+ if !content_md5.nil?
71
+ self.content_md5 = Digest::MD5.hexdigest(value) if content_md5 == true
72
+
73
+ # We expect a md5 hex digest here
74
+ md5_digest = content_md5.unpack('a2'*16).collect {|i| i.hex.chr }.join
75
+ headers['Content-MD5'] = Base64.encode64(md5_digest).strip
76
+ end
77
+
78
+ # ACL
79
+ if !access.nil?
80
+ headers['x-amz-acl'] = access.to_s.gsub('_', '-')
81
+ end
82
+
83
+ # Storage class
84
+ if !storage_class.nil?
85
+ headers['x-amz-storage-class'] = storage_class.to_s.upcase
86
+ end
87
+
88
+ # Meta
89
+ if !meta.nil? && !meta.empty?
90
+ meta.each {|k,v| headers["x-amz-meta-#{k}"] = v.to_s }
91
+ end
92
+
93
+ # Let's do it
94
+ response = bucket.connection.put(key, headers, value)
95
+
96
+ # Update error state
97
+ self.error = response.error
98
+
99
+ # Test for success....
100
+ response.status == 200
101
+ end
102
+
103
+ def delete
104
+ bucket.connection.delete(key).status == 204
105
+ end
106
+
107
+ def value
108
+ fetch if !@value
109
+ @value
110
+ end
111
+
112
+ def persisted?
113
+ # TODO
114
+ end
115
+
116
+ def url
117
+ # TODO
118
+ end
119
+
120
+ def key=(key)
121
+ @key = key.gsub(/^\//,'')
122
+ end
123
+
124
+ def value=(value)
125
+ self.size = value.to_s.bytesize
126
+ @value = value
127
+ end
128
+
129
+ # TODO..
130
+ # Add callback support so Operations can hook into that ... cleaner. ie. on_save { ... }
131
+
132
+ private
133
+
134
+ def parse_response_header!
135
+ # Meta..
136
+ self.meta ||= {}
137
+ response.header.keys.sort.select {|k| k =~ /^x.amz.meta./i }.each do |amz_key|
138
+
139
+ # TODO.. value is an array.. meaning a meta attribute can have multiple values
140
+ meta[amz_key.gsub(/^x.amz.meta./i, '')] = [response.header[amz_key]].flatten.first
141
+
142
+ # TODO.. em-http adapters return headers that look like X_AMZ_META_ .. very annoying.
143
+ end
144
+ end
145
+
146
+ end
147
+ end
@@ -0,0 +1,7 @@
1
+ class UberS3
2
+ module Operation
3
+ end
4
+ end
5
+
6
+ # Load object operation modules
7
+ Dir[File.dirname(__FILE__) + '/operation/object/*.rb'].each {|f| require f }
@@ -0,0 +1,29 @@
1
+ module UberS3::Operation::Object
2
+ module AccessPolicy
3
+
4
+ def self.included(base)
5
+ base.send :extend, ClassMethods
6
+ base.send :include, InstanceMethods
7
+
8
+ base.instance_eval do
9
+ attr_accessor :access
10
+ end
11
+ end
12
+
13
+ module ClassMethods
14
+ end
15
+
16
+ module InstanceMethods
17
+ def access=(access)
18
+ valid_values = [:private, :public_read, :public_read_write, :authenticated_read]
19
+
20
+ if valid_values.include?(access)
21
+ @access = access
22
+ else
23
+ raise "Invalid access value"
24
+ end
25
+ end
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,20 @@
1
+ module UberS3::Operation::Object
2
+ module ContentDisposition
3
+
4
+ def self.included(base)
5
+ base.send :extend, ClassMethods
6
+ base.send :include, InstanceMethods
7
+
8
+ base.instance_eval do
9
+ attr_accessor :content_disposition
10
+ end
11
+ end
12
+
13
+ module ClassMethods
14
+ end
15
+
16
+ module InstanceMethods
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,64 @@
1
+ module UberS3::Operation::Object
2
+ module ContentEncoding
3
+
4
+ def self.included(base)
5
+ base.send :extend, ClassMethods
6
+ base.send :include, InstanceMethods
7
+
8
+ base.instance_eval do
9
+ attr_accessor :gzip, :content_encoding
10
+ end
11
+ end
12
+
13
+ module ClassMethods
14
+ end
15
+
16
+ module InstanceMethods
17
+
18
+ # Default mime-types to be auto-gzipped if gzip == :web
19
+ WEB_GZIP_TYPES = ['text/html', 'text/plain', 'text/css', 'application/json',
20
+ 'application/javascript', 'application/x-javascript',
21
+ 'text/xml', 'application/xml', 'application/xml+rss']
22
+
23
+ private
24
+
25
+ def gzip_content!
26
+ return if gzip.nil?
27
+
28
+ if gzip == true
29
+ encode = true
30
+ elsif gzip == :web && WEB_GZIP_TYPES.include?(content_type)
31
+ encode = true
32
+ elsif gzip.is_a?(String) && gzip == content_type
33
+ encode = true
34
+ elsif gzip.is_a?(Array) && gzip.include?(content_type)
35
+ encode = true
36
+ else
37
+ encode = false
38
+ end
39
+
40
+ if encode
41
+ self.value = gzip_encoder(value)
42
+ self.content_encoding = 'gzip'
43
+ end
44
+ end
45
+
46
+ def gzip_encoder(data)
47
+ begin
48
+ gz_stream = StringIO.new
49
+ gz = Zlib::GzipWriter.new(gz_stream)
50
+ gz.write(value)
51
+ gz.close
52
+
53
+ gz_stream.string
54
+ rescue => ex
55
+ # TODO ...
56
+ $stderr.puts "Gzip failed: #{ex.message}"
57
+ raise ex
58
+ end
59
+ end
60
+
61
+ end
62
+
63
+ end
64
+ end
@@ -0,0 +1,21 @@
1
+ module UberS3::Operation::Object
2
+ module ContentMd5
3
+
4
+ def self.included(base)
5
+ base.send :extend, ClassMethods
6
+ base.send :include, InstanceMethods
7
+
8
+ base.instance_eval do
9
+ attr_accessor :content_md5
10
+ end
11
+ end
12
+
13
+ module ClassMethods
14
+ end
15
+
16
+ module InstanceMethods
17
+ # TODO.. move stuff from object.rb to here... need callback / chaining stuff...
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,31 @@
1
+ module UberS3::Operation::Object
2
+ module ContentType
3
+
4
+ def self.included(base)
5
+ base.send :extend, ClassMethods
6
+ base.send :include, InstanceMethods
7
+
8
+ base.instance_eval do
9
+ # attr_accessor :content_type
10
+ end
11
+ end
12
+
13
+ module ClassMethods
14
+ end
15
+
16
+ module InstanceMethods
17
+
18
+ def infer_content_type!
19
+ mime_type = MIME::Types.type_for(key).first
20
+
21
+ self.content_type ||= mime_type.content_type if mime_type
22
+ self.content_type ||= 'binary/octet-stream'
23
+ end
24
+
25
+ def content_type=(x); @content_type = x; end
26
+ def content_type; @content_type; end
27
+
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,67 @@
1
+ module UberS3::Operation::Object
2
+ module HttpCache
3
+
4
+ def self.included(base)
5
+ # TODO: .. strange behaviour.. can't override these methods in below modules.
6
+ # requires some metaprogramming debugging
7
+ # base.instance_eval do
8
+ # attr_accessor :cache_control, :expires, :pragma, :ttl
9
+ # end
10
+
11
+ base.send :extend, ClassMethods
12
+ base.send :include, InstanceMethods
13
+ end
14
+
15
+ module ClassMethods
16
+ end
17
+
18
+ module InstanceMethods
19
+
20
+ # TODO: shouldn't need this junk... see above comment
21
+ def cache_control=(x); @cache_control = x; end
22
+ def cache_control; @cache_control; end
23
+
24
+ def expires=(x); @expires = x; end
25
+ def expires; @expires; end
26
+
27
+ def pragma=(x); @pragma = x; end
28
+ def pragma; @pragma; end
29
+
30
+ def ttl=(x); @ttl = x; end
31
+ def ttl; @ttl; end
32
+
33
+ #----
34
+
35
+ # Helper method that will set the max-age for cache-control
36
+ def ttl=(val)
37
+ (@ttl = val).tap { infer_cache_control! }
38
+ end
39
+
40
+ # TODO... there are some edge cases here.. ie. someone sets their own self.content_type ...
41
+ def infer_cache_control!
42
+ return if ttl.nil? || content_type.nil?
43
+
44
+ if ttl.is_a?(Hash)
45
+ mime_types = ttl.keys
46
+ match = mime_types.find {|pattern| !(content_type =~ /#{pattern}/i).nil? }
47
+ self.cache_control = "public,max-age=#{ttl[match]}" if match
48
+ else
49
+ self.cache_control = "public,max-age=#{ttl}"
50
+ end
51
+ end
52
+
53
+
54
+ # Expires can take a time or string
55
+ def expires=(val)
56
+ if val.is_a?(String)
57
+ self.expires = val
58
+ elsif val.is_a?(Time)
59
+ # RFC 1123 format
60
+ self.expires = val.strftime("%a, %d %b %Y %H:%I:%S %Z")
61
+ end
62
+ end
63
+
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,20 @@
1
+ module UberS3::Operation::Object
2
+ module Meta
3
+
4
+ def self.included(base)
5
+ base.send :extend, ClassMethods
6
+ base.send :include, InstanceMethods
7
+
8
+ base.instance_eval do
9
+ attr_accessor :meta
10
+ end
11
+ end
12
+
13
+ module ClassMethods
14
+ end
15
+
16
+ module InstanceMethods
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,29 @@
1
+ module UberS3::Operation::Object
2
+ module StorageClass
3
+
4
+ def self.included(base)
5
+ base.send :extend, ClassMethods
6
+ base.send :include, InstanceMethods
7
+
8
+ base.instance_eval do
9
+ attr_accessor :storage_class
10
+ end
11
+ end
12
+
13
+ module ClassMethods
14
+ end
15
+
16
+ module InstanceMethods
17
+ def storage_class=(storage_class)
18
+ valid_values = [:standard, :reduced_redundancy]
19
+
20
+ if valid_values.include?(storage_class)
21
+ @storage_class = storage_class
22
+ else
23
+ raise "Invalid storage_class value"
24
+ end
25
+ end
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,49 @@
1
+ class UberS3
2
+ class Response
3
+
4
+ attr_accessor :status, :header, :body, :raw, :error_key, :error_message
5
+
6
+ def initialize(options={})
7
+ if !([:status, :header, :body, :raw] - options.keys).empty?
8
+ raise "Expecting keys :status, :header, :body and :raw"
9
+ end
10
+
11
+ self.status = options[:status]
12
+ self.header = options[:header]
13
+ self.body = options[:body]
14
+ self.raw = options[:raw]
15
+
16
+ success?
17
+ end
18
+
19
+ # TODO: can/should we normalize the keys..? downcase.. etc.?
20
+ # def header=(header)
21
+ # end
22
+
23
+ def success?
24
+ return if status < 400 || body.to_s.empty?
25
+
26
+ # Errors are XML
27
+ doc = Util::XmlDocument.new(body)
28
+
29
+ self.error_key = doc.xpath('//Error/Code').first.text
30
+ self.error_message = doc.xpath('//Error/Message').first.text
31
+
32
+ error_klass = instance_eval("Error::#{error_key}") rescue nil
33
+
34
+ if error_klass.nil?
35
+ raise Error::Unknown, "HTTP Response: #{status}, Body: #{body}"
36
+ else
37
+ raise error_klass.new(error_key, error_message)
38
+ end
39
+ end
40
+
41
+ def error
42
+ if !error_key.nil?
43
+ "#{error_key}: #{error_message}"
44
+ end
45
+ end
46
+
47
+
48
+ end
49
+ end
@@ -0,0 +1,48 @@
1
+ begin
2
+ require 'nokogiri'
3
+ rescue LoadError
4
+ require 'rexml/document'
5
+ end
6
+
7
+ module UberS3::Util
8
+ class XmlDocument
9
+
10
+ def initialize(xml)
11
+ if defined?(Nokogiri)
12
+ @parser = NokogiriParser.new(xml)
13
+ else
14
+ @parser = RexmlParser.new(xml)
15
+ end
16
+ end
17
+
18
+ def method_missing(sym, *args, &block)
19
+ @parser.send sym, *args, &block
20
+ end
21
+
22
+ class RexmlParser
23
+ attr_accessor :doc
24
+
25
+ def initialize(xml)
26
+ self.doc = REXML::Document.new(xml)
27
+ end
28
+
29
+ def xpath(path)
30
+ REXML::XPath.match(doc.root, path)
31
+ end
32
+ end
33
+
34
+ class NokogiriParser
35
+ attr_accessor :doc
36
+
37
+ def initialize(xml)
38
+ self.doc = Nokogiri::XML(xml)
39
+ doc.remove_namespaces!
40
+ end
41
+
42
+ def xpath(path)
43
+ doc.xpath(path).to_a
44
+ end
45
+ end
46
+
47
+ end
48
+ end
@@ -0,0 +1,3 @@
1
+ class UberS3
2
+ VERSION = '0.2.4'
3
+ end
metadata ADDED
@@ -0,0 +1,100 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rcarvalho-uber-s3
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.4
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Peter Kieltyka
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2012-07-20 00:00:00.000000000Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: mime-types
16
+ requirement: &2156690680 !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: '1.17'
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: *2156690680
25
+ - !ruby/object:Gem::Dependency
26
+ name: rake
27
+ requirement: &2156690300 !ruby/object:Gem::Requirement
28
+ none: false
29
+ requirements:
30
+ - - ! '>='
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :development
34
+ prerelease: false
35
+ version_requirements: *2156690300
36
+ - !ruby/object:Gem::Dependency
37
+ name: rspec
38
+ requirement: &2156689740 !ruby/object:Gem::Requirement
39
+ none: false
40
+ requirements:
41
+ - - ~>
42
+ - !ruby/object:Gem::Version
43
+ version: 2.7.0
44
+ type: :development
45
+ prerelease: false
46
+ version_requirements: *2156689740
47
+ description: A simple & very fast S3 client supporting sync / async HTTP adapters
48
+ email:
49
+ - peter@nulayer.com
50
+ executables: []
51
+ extensions: []
52
+ extra_rdoc_files: []
53
+ files:
54
+ - README.md
55
+ - lib/uber-s3/authorization.rb
56
+ - lib/uber-s3/bucket.rb
57
+ - lib/uber-s3/connection/em_http.rb
58
+ - lib/uber-s3/connection/em_http_fibered.rb
59
+ - lib/uber-s3/connection/net_http.rb
60
+ - lib/uber-s3/connection.rb
61
+ - lib/uber-s3/error.rb
62
+ - lib/uber-s3/object.rb
63
+ - lib/uber-s3/operation/object/access_policy.rb
64
+ - lib/uber-s3/operation/object/content_disposition.rb
65
+ - lib/uber-s3/operation/object/content_encoding.rb
66
+ - lib/uber-s3/operation/object/content_md5.rb
67
+ - lib/uber-s3/operation/object/content_type.rb
68
+ - lib/uber-s3/operation/object/http_cache.rb
69
+ - lib/uber-s3/operation/object/meta.rb
70
+ - lib/uber-s3/operation/object/storage_class.rb
71
+ - lib/uber-s3/operation.rb
72
+ - lib/uber-s3/response.rb
73
+ - lib/uber-s3/util/xml_document.rb
74
+ - lib/uber-s3/version.rb
75
+ - lib/uber-s3.rb
76
+ homepage: http://github.com/nulayer/uber-s3
77
+ licenses: []
78
+ post_install_message:
79
+ rdoc_options: []
80
+ require_paths:
81
+ - lib
82
+ required_ruby_version: !ruby/object:Gem::Requirement
83
+ none: false
84
+ requirements:
85
+ - - ! '>='
86
+ - !ruby/object:Gem::Version
87
+ version: '0'
88
+ required_rubygems_version: !ruby/object:Gem::Requirement
89
+ none: false
90
+ requirements:
91
+ - - ! '>='
92
+ - !ruby/object:Gem::Version
93
+ version: 1.3.6
94
+ requirements: []
95
+ rubyforge_project:
96
+ rubygems_version: 1.8.6
97
+ signing_key:
98
+ specification_version: 3
99
+ summary: A simple & very fast S3 client supporting sync / async HTTP adapters
100
+ test_files: []