rcarvalho-uber-s3 0.2.4
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +153 -0
- data/lib/uber-s3.rb +45 -0
- data/lib/uber-s3/authorization.rb +29 -0
- data/lib/uber-s3/bucket.rb +100 -0
- data/lib/uber-s3/connection.rb +72 -0
- data/lib/uber-s3/connection/em_http.rb +12 -0
- data/lib/uber-s3/connection/em_http_fibered.rb +26 -0
- data/lib/uber-s3/connection/net_http.rb +62 -0
- data/lib/uber-s3/error.rb +89 -0
- data/lib/uber-s3/object.rb +147 -0
- data/lib/uber-s3/operation.rb +7 -0
- data/lib/uber-s3/operation/object/access_policy.rb +29 -0
- data/lib/uber-s3/operation/object/content_disposition.rb +20 -0
- data/lib/uber-s3/operation/object/content_encoding.rb +64 -0
- data/lib/uber-s3/operation/object/content_md5.rb +21 -0
- data/lib/uber-s3/operation/object/content_type.rb +31 -0
- data/lib/uber-s3/operation/object/http_cache.rb +67 -0
- data/lib/uber-s3/operation/object/meta.rb +20 -0
- data/lib/uber-s3/operation/object/storage_class.rb +29 -0
- data/lib/uber-s3/response.rb +49 -0
- data/lib/uber-s3/util/xml_document.rb +48 -0
- data/lib/uber-s3/version.rb +3 -0
- metadata +100 -0
data/README.md
ADDED
@@ -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.
|
data/lib/uber-s3.rb
ADDED
@@ -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,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
|
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: []
|