rcarvalho-uber-s3 0.2.4

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.
@@ -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: []