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