s3lib 0.1.0
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/Rakefile +24 -0
- data/VERSION.yml +5 -0
- data/bin/s3lib +15 -0
- data/bin/s3sh_as +15 -0
- data/github-test.rb +22 -0
- data/lib/acl.rb +134 -0
- data/lib/acl_access.rb +20 -0
- data/lib/acl_creating_a_grant_recipe.rb +95 -0
- data/lib/acl_reading_acl_recipe.rb +59 -0
- data/lib/acl_refreshing_cached_grants_recipe.rb +54 -0
- data/lib/bucket.rb +116 -0
- data/lib/bucket_before_refactoring.rb +120 -0
- data/lib/bucket_create.rb +39 -0
- data/lib/bucket_find.rb +41 -0
- data/lib/bucket_with_acl_mixin.rb +103 -0
- data/lib/error_handling.rb +12 -0
- data/lib/grant.rb +107 -0
- data/lib/grant_creating_a_grant_recipe.rb +103 -0
- data/lib/grant_reading_acl_recipe.rb +51 -0
- data/lib/object.rb +144 -0
- data/lib/object_from_bucket_test.rb +18 -0
- data/lib/object_take1.rb +150 -0
- data/lib/object_with_acl_mixin.rb +131 -0
- data/lib/put_with_curl_test.rb +39 -0
- data/lib/s3_authenticator.rb +155 -0
- data/lib/s3_authenticator_dev.rb +117 -0
- data/lib/s3_authenticator_dev_private.rb +40 -0
- data/lib/s3_errors.rb +58 -0
- data/lib/s3lib.rb +10 -0
- data/lib/s3lib_with_mixin.rb +11 -0
- data/lib/service.rb +24 -0
- data/lib/service_dev.rb +36 -0
- data/s3lib.gemspec +74 -0
- data/sample_usage.rb +45 -0
- data/test/acl_test.rb +89 -0
- data/test/amazon_headers_test.rb +87 -0
- data/test/canonical_resource_test.rb +53 -0
- data/test/canonical_string_tests.rb +73 -0
- data/test/first_test.rb +34 -0
- data/test/first_test_private.rb +55 -0
- data/test/full_test.rb +84 -0
- data/test/s3_authenticator_test.rb +291 -0
- metadata +109 -0
data/lib/object_take1.rb
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
# object.rb
|
2
|
+
require 'rexml/document'
|
3
|
+
|
4
|
+
module S3Lib
|
5
|
+
|
6
|
+
class ObjectDoesNotExist < StandardError
|
7
|
+
end
|
8
|
+
|
9
|
+
class ObjectAccessForbidden < StandardError
|
10
|
+
end
|
11
|
+
|
12
|
+
class NoContentError < S3Lib::S3ResponseError
|
13
|
+
end
|
14
|
+
|
15
|
+
class S3Object
|
16
|
+
|
17
|
+
DEFAULT_CONTENT_TYPE = 'binary/octect-stream'
|
18
|
+
|
19
|
+
attr_reader :key, :bucket
|
20
|
+
|
21
|
+
# This is just an alias for S3Object.new
|
22
|
+
def self.find(bucket, key, options = {})
|
23
|
+
S3Object.new(bucket, key, options)
|
24
|
+
end
|
25
|
+
|
26
|
+
def self.create(bucket, key, value = "", options = {})
|
27
|
+
options.merge!({:body => value || "", 'content-type' => DEFAULT_CONTENT_TYPE})
|
28
|
+
begin
|
29
|
+
response = S3Lib.request(:put, S3Object.url(bucket, key), options)
|
30
|
+
rescue S3Lib::S3ResponseError => error
|
31
|
+
case error.amazon_error_type
|
32
|
+
when 'NoSuchBucket': raise S3Lib::BucketNotFoundError.new("The bucket '#{bucket}' does not exist.", error.io, error.s3requester)
|
33
|
+
when 'AccessDenied': raise S3Lib::NotYourBucketError.new("The bucket '#{bucket}' is owned by someone else.", error.io, error.s3requester)
|
34
|
+
when 'MissingContentLength': raise S3Lib::NoContentError.new("You must provide a value to put in the object.\nUsage: S3Lib::S3Object.create(bucket, key, value, options)", error.io, error.s3requester)
|
35
|
+
else # Re-raise the error if it's not one of the above
|
36
|
+
raise
|
37
|
+
end
|
38
|
+
end
|
39
|
+
response.status[0] == "200" ? S3Object.new(bucket, key) : false
|
40
|
+
end
|
41
|
+
|
42
|
+
# Delete an object given the object's bucket and key.
|
43
|
+
# No error will be raised if the object does not exist.
|
44
|
+
def self.delete(bucket, key, options = {})
|
45
|
+
begin
|
46
|
+
response = S3Lib.request(:delete, S3Object.url(bucket, key), options)
|
47
|
+
rescue S3Lib::S3ResponseError => error
|
48
|
+
case error.amazon_error_type
|
49
|
+
when 'NoSuchBucket': raise S3Lib::BucketNotFoundError.new("The bucket '#{bucket}' does not exist.", error.io, error.s3requester)
|
50
|
+
when 'NotSignedUp': raise S3Lib::NotYourBucketError.new("The bucket '#{bucket}' is owned by somebody else", error.io, error.s3requester)
|
51
|
+
else # Re-raise the error if it's not one of the above
|
52
|
+
raise
|
53
|
+
end
|
54
|
+
end
|
55
|
+
puts response.status
|
56
|
+
end
|
57
|
+
|
58
|
+
def delete
|
59
|
+
S3Object.delete(@bucket, @key, @options)
|
60
|
+
end
|
61
|
+
|
62
|
+
def self.value(bucket, key, options = {})
|
63
|
+
request = S3Object.object_request(:get, S3Object.url(bucket, key), options)
|
64
|
+
request.read
|
65
|
+
end
|
66
|
+
|
67
|
+
# bucket can be either a Bucket object or a string containing the bucket's name
|
68
|
+
def self.url(bucket, key)
|
69
|
+
bucket_name = bucket.respond_to?(:name) ? bucket.name : bucket
|
70
|
+
File.join(bucket_name, key)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Both metadata and value are loaded lazily if options[:lazy_load] is true
|
74
|
+
# This is used by Bucket.find so you don't make a request for every object in the bucket
|
75
|
+
# The bucket can be either a bucket object or a string containing the bucket's name
|
76
|
+
# The key is a string.
|
77
|
+
def initialize(bucket, key, options = {})
|
78
|
+
options.merge!(:lazy_load => false)
|
79
|
+
bucket = Bucket.find(bucket) unless bucket.respond_to?(:name)
|
80
|
+
@bucket = bucket
|
81
|
+
@key = key
|
82
|
+
@options = options
|
83
|
+
get_metadata unless options.delete(:lazy_load)
|
84
|
+
end
|
85
|
+
|
86
|
+
def url
|
87
|
+
S3Object.url(@bucket.name, @key)
|
88
|
+
end
|
89
|
+
|
90
|
+
def metadata
|
91
|
+
@metadata || get_metadata
|
92
|
+
end
|
93
|
+
|
94
|
+
def value
|
95
|
+
@value || get_value
|
96
|
+
end
|
97
|
+
|
98
|
+
def value=(value)
|
99
|
+
S3Object.object_request(:put, value)
|
100
|
+
@value = value
|
101
|
+
refresh_metadata
|
102
|
+
end
|
103
|
+
|
104
|
+
def refresh
|
105
|
+
get_value
|
106
|
+
end
|
107
|
+
|
108
|
+
def refresh_metadata
|
109
|
+
get_metadata
|
110
|
+
end
|
111
|
+
|
112
|
+
def content_type
|
113
|
+
metadata["content-type"]
|
114
|
+
end
|
115
|
+
|
116
|
+
def etag
|
117
|
+
metadata["etag"]
|
118
|
+
end
|
119
|
+
|
120
|
+
private
|
121
|
+
|
122
|
+
def self.object_request(verb, url, options = {})
|
123
|
+
begin
|
124
|
+
S3Lib.request(verb, url, options)
|
125
|
+
rescue S3Lib::S3ResponseError => error
|
126
|
+
case error.amazon_error_type
|
127
|
+
when 'NoSuchBucket': raise S3Lib::BucketNotFoundError.new("The bucket '#{bucket}' does not exist.", error.io, error.s3requester)
|
128
|
+
when 'NotSignedUp': raise S3Lib::NotYourBucketError.new("The bucket '#{bucket}' is owned by somebody else", error.io, error.s3requester)
|
129
|
+
when 'AccessDenied': raise S3Lib::NotYourBucketError.new("The bucket '#{bucket}' is owned by someone else.", error.io, error.s3requester)
|
130
|
+
when 'MissingContentLength': raise S3Lib::NoContentError.new("You must provide a value to put in the object.\nUsage: S3Lib::S3Object.create(bucket, key, value, options)", error.io, error.s3requester)
|
131
|
+
else # Re-raise the error if it's not one of the above
|
132
|
+
raise
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def get_metadata
|
138
|
+
request = S3Object.object_request(:head, url)
|
139
|
+
@metadata = request.meta
|
140
|
+
end
|
141
|
+
|
142
|
+
def get_value
|
143
|
+
request = S3Object.object_request(:get, url)
|
144
|
+
@metadata = request.meta
|
145
|
+
@value = request.read
|
146
|
+
end
|
147
|
+
|
148
|
+
end
|
149
|
+
|
150
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
# s3_object.rb
|
2
|
+
|
3
|
+
module S3Lib
|
4
|
+
|
5
|
+
class S3Object
|
6
|
+
|
7
|
+
DEFAULT_CONTENT_TYPE = 'binary/octect-stream'
|
8
|
+
|
9
|
+
attr_reader :key, :bucket
|
10
|
+
|
11
|
+
include S3Lib::AclAccess
|
12
|
+
|
13
|
+
# This is just an alias for S3Object.new
|
14
|
+
def self.find(bucket, key, options = {})
|
15
|
+
S3Object.new(bucket, key, options)
|
16
|
+
end
|
17
|
+
|
18
|
+
def self.create(bucket, key, value = "", options = {})
|
19
|
+
# translate from :access to 'x-amz-acl'
|
20
|
+
options['x-amz-acl'] = options.delete(:access) if options[:access]
|
21
|
+
options.merge!({:body => value || "", 'content-type' => DEFAULT_CONTENT_TYPE})
|
22
|
+
response = S3Object.object_request(:put, S3Object.url(bucket, key), options)
|
23
|
+
response.status[0] == "200" ? S3Object.new(bucket, key, options) : false
|
24
|
+
end
|
25
|
+
|
26
|
+
# Delete an object given the object's bucket and key.
|
27
|
+
# No error will be raised if the object does not exist.
|
28
|
+
def self.delete(bucket, key, options = {})
|
29
|
+
S3Object.object_request(:delete, S3Object.url(bucket, key), options)
|
30
|
+
end
|
31
|
+
|
32
|
+
def delete
|
33
|
+
S3Object.delete(@bucket, @key, @options)
|
34
|
+
end
|
35
|
+
|
36
|
+
def self.value(bucket, key, options = {})
|
37
|
+
request = S3Object.object_request(:get, S3Object.url(bucket, key), options)
|
38
|
+
request.read
|
39
|
+
end
|
40
|
+
|
41
|
+
# Both metadata and value are loaded lazily if options[:lazy_load] is true
|
42
|
+
# This is used by Bucket.find so you don't make a request for every object in the bucket
|
43
|
+
# The bucket can be either a bucket object or a string containing the bucket's name
|
44
|
+
# The key is a string.
|
45
|
+
def initialize(bucket, key, options = {})
|
46
|
+
bucket = Bucket.find(bucket) unless bucket.respond_to?(:name)
|
47
|
+
@bucket = bucket
|
48
|
+
@key = key
|
49
|
+
@options = options
|
50
|
+
get_metadata unless options[:lazy_load]
|
51
|
+
end
|
52
|
+
|
53
|
+
# bucket can be either a Bucket object or a string containing the bucket's name
|
54
|
+
def self.url(bucket, key)
|
55
|
+
bucket_name = bucket.respond_to?(:name) ? bucket.name : bucket
|
56
|
+
File.join(bucket_name, key)
|
57
|
+
end
|
58
|
+
|
59
|
+
def url
|
60
|
+
S3Object.url(@bucket.name, @key)
|
61
|
+
end
|
62
|
+
|
63
|
+
def metadata
|
64
|
+
@metadata || get_metadata
|
65
|
+
end
|
66
|
+
|
67
|
+
def value(params = {})
|
68
|
+
refresh if params[:refresh]
|
69
|
+
@value || get_value
|
70
|
+
end
|
71
|
+
|
72
|
+
def value=(value)
|
73
|
+
S3Object.create(@bucket, @key, value, @options)
|
74
|
+
@value = value
|
75
|
+
refresh_metadata
|
76
|
+
end
|
77
|
+
|
78
|
+
def refresh
|
79
|
+
get_value
|
80
|
+
end
|
81
|
+
|
82
|
+
def refresh_metadata
|
83
|
+
get_metadata
|
84
|
+
end
|
85
|
+
|
86
|
+
def content_type
|
87
|
+
metadata["content-type"]
|
88
|
+
end
|
89
|
+
|
90
|
+
# strip off the leading and trailing double-quotes
|
91
|
+
def etag
|
92
|
+
metadata["etag"].sub(/\A\"/,'').sub(/\"\Z/, '')
|
93
|
+
end
|
94
|
+
|
95
|
+
def length
|
96
|
+
metadata["content-length"].to_i
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def self.object_request(verb, url, options = {})
|
102
|
+
begin
|
103
|
+
options.delete(:lazy_load)
|
104
|
+
response = S3Lib.request(verb, url, options)
|
105
|
+
rescue S3Lib::S3ResponseError => error
|
106
|
+
case error.amazon_error_type
|
107
|
+
when 'NoSuchBucket': raise S3Lib::BucketNotFoundError.new("The bucket '#{bucket}' does not exist.", error.io, error.s3requester)
|
108
|
+
when 'NotSignedUp': raise S3Lib::NotYourBucketError.new("The bucket '#{bucket}' is owned by somebody else", error.io, error.s3requester)
|
109
|
+
when 'AccessDenied': raise S3Lib::NotYourBucketError.new("The bucket '#{bucket}' is owned by someone else.", error.io, error.s3requester)
|
110
|
+
when 'MissingContentLength': raise S3Lib::NoContentError.new("You must provide a value to put in the object.\nUsage: S3Lib::S3Object.create(bucket, key, value, options)", error.io, error.s3requester)
|
111
|
+
else # Re-raise the error if it's not one of the above
|
112
|
+
raise
|
113
|
+
end
|
114
|
+
end
|
115
|
+
response
|
116
|
+
end
|
117
|
+
|
118
|
+
def get_metadata
|
119
|
+
request = S3Object.object_request(:head, url, @options)
|
120
|
+
@metadata = request.meta
|
121
|
+
end
|
122
|
+
|
123
|
+
def get_value
|
124
|
+
request = S3Object.object_request(:get, url, @options)
|
125
|
+
@metadata = request.meta
|
126
|
+
@value = request.read
|
127
|
+
end
|
128
|
+
|
129
|
+
end
|
130
|
+
|
131
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 's3lib'
|
3
|
+
|
4
|
+
module S3Lib
|
5
|
+
|
6
|
+
class AuthenticatedRequest
|
7
|
+
|
8
|
+
def public_authorization_string
|
9
|
+
authorization_string
|
10
|
+
end
|
11
|
+
|
12
|
+
end
|
13
|
+
|
14
|
+
end
|
15
|
+
|
16
|
+
value = 'testing'
|
17
|
+
key = 'test.txt'
|
18
|
+
auth_string = nil
|
19
|
+
date = Time.now.httpdate
|
20
|
+
begin
|
21
|
+
S3Lib.request(:put, "spatten_test_bucket/#{key}", :body => value, 'date' => date)
|
22
|
+
rescue => e
|
23
|
+
puts e.response
|
24
|
+
puts "authorization string:"
|
25
|
+
puts e.s3requester.public_authorization_string
|
26
|
+
auth_string = e.s3requester.public_authorization_string
|
27
|
+
end
|
28
|
+
|
29
|
+
puts "date: #{date}"
|
30
|
+
puts "Auth String:"
|
31
|
+
puts auth_string
|
32
|
+
|
33
|
+
puts "doing curl"
|
34
|
+
puts `curl -X PUT -d body=#{value} -d 'Authorization=#{auth_string}' -d 'date=#{date}' http://s3.amazonaws.com/spatten_test_bucket/#{key}`
|
35
|
+
puts "end of curl"
|
36
|
+
|
37
|
+
obj = S3Lib::S3Object.find('spatten_test_bucket', key)
|
38
|
+
puts "Content type: #{obj.content_type}"
|
39
|
+
|
@@ -0,0 +1,155 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rubygems'
|
3
|
+
require 'rest-open-uri'
|
4
|
+
|
5
|
+
require 'base64'
|
6
|
+
require 'digest/sha1'
|
7
|
+
require 'openssl'
|
8
|
+
require 'pp'
|
9
|
+
|
10
|
+
class Hash
|
11
|
+
|
12
|
+
def downcase_keys
|
13
|
+
res = {}
|
14
|
+
each do |key, value|
|
15
|
+
key = key.downcase if key.respond_to?(:downcase)
|
16
|
+
res[key] = value
|
17
|
+
end
|
18
|
+
res
|
19
|
+
end
|
20
|
+
|
21
|
+
def join_values(separator = ',')
|
22
|
+
res = {}
|
23
|
+
each do |key, value|
|
24
|
+
res[key] = value.respond_to?(:join) ? value.join(separator) : value
|
25
|
+
end
|
26
|
+
res
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
module S3Lib
|
32
|
+
|
33
|
+
def self.request(verb, request_path, headers = {})
|
34
|
+
begin
|
35
|
+
s3requester = AuthenticatedRequest.new()
|
36
|
+
req = s3requester.make_authenticated_request(verb, request_path, headers)
|
37
|
+
rescue OpenURI::HTTPError=> e
|
38
|
+
raise S3Lib::S3ResponseError.new(e.message, e.io, s3requester)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
class AuthenticatedRequest
|
43
|
+
|
44
|
+
POSITIONAL_HEADERS = ['content-md5', 'content-type', 'date']
|
45
|
+
AMAZON_HEADER_PREFIX = 'x-amz-'
|
46
|
+
HOST = 's3.amazonaws.com'
|
47
|
+
BUCKET_LIST_PARAMS = [:max_keys, :prefix, :marker, :delimiter]
|
48
|
+
SUB_RESOURCE_TYPES = ['acl', 'torrent', 'logging']
|
49
|
+
|
50
|
+
def make_authenticated_request(verb, request_path, headers = {})
|
51
|
+
@verb = verb
|
52
|
+
@request_path = request_path.gsub(/^\//,'') # Strip off the leading '/'
|
53
|
+
|
54
|
+
@amazon_id = ENV['AMAZON_ACCESS_KEY_ID']
|
55
|
+
@amazon_secret = ENV['AMAZON_SECRET_ACCESS_KEY']
|
56
|
+
|
57
|
+
@headers = headers.downcase_keys.join_values
|
58
|
+
get_bucket_list_params
|
59
|
+
get_bucket_name
|
60
|
+
fix_date
|
61
|
+
|
62
|
+
req = open(uri_with_bucket_list_params, @headers.merge(:method => @verb, 'Authorization' => authorization_string))
|
63
|
+
end
|
64
|
+
|
65
|
+
def canonical_string
|
66
|
+
"#{@verb.to_s.upcase}\n#{canonicalized_headers}#{canonicalized_resource}"
|
67
|
+
end
|
68
|
+
|
69
|
+
def get_bucket_name
|
70
|
+
@bucket = ""
|
71
|
+
return unless @headers.has_key?('host')
|
72
|
+
@headers['host'] = @headers['host'].downcase
|
73
|
+
return if @headers['host'] == 's3.amazonaws.com'
|
74
|
+
if @headers['host'] =~ /^([^.]+)(:\d\d\d\d)?\.#{HOST}$/
|
75
|
+
@bucket = $1.gsub(/\/$/,'') + '/'
|
76
|
+
else
|
77
|
+
@bucket = @headers['host'].gsub(/(:\d\d\d\d)$/, '').gsub(/\/$/,'') + '/'
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def fix_date
|
82
|
+
@headers['date'] ||= Time.now.httpdate
|
83
|
+
@headers.delete('date') if @headers.has_key?('x-amz-date')
|
84
|
+
end
|
85
|
+
|
86
|
+
def uri
|
87
|
+
host = @headers['host'] || HOST
|
88
|
+
"http://" + File.join(host, URI.escape(@request_path))
|
89
|
+
end
|
90
|
+
|
91
|
+
def get_bucket_list_params
|
92
|
+
@bucket_list_params = {}
|
93
|
+
@headers.each do |key, value|
|
94
|
+
@bucket_list_params[key] = @headers.delete(key) if BUCKET_LIST_PARAMS.include?(key)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def uri_with_bucket_list_params
|
99
|
+
return uri if @bucket_list_params.empty?
|
100
|
+
uri_with_params = uri
|
101
|
+
bucket_list_string = @bucket_list_params.collect {|key, value| "#{key.to_s.gsub('_', '-')}=#{value}"}.join('&')
|
102
|
+
uri_with_params.sub(/\/$/, '') # remove trailing slash
|
103
|
+
uri_with_params += '?' unless uri =~ /\?$/ # Add trailing ?
|
104
|
+
uri_with_params += bucket_list_string # add bucket list params
|
105
|
+
uri_with_params
|
106
|
+
end
|
107
|
+
|
108
|
+
def authorization_string
|
109
|
+
generator = OpenSSL::Digest::Digest.new('sha1')
|
110
|
+
encoded_canonical = Base64.encode64(OpenSSL::HMAC.digest(generator, @amazon_secret, canonical_string)).strip
|
111
|
+
|
112
|
+
"AWS #{@amazon_id}:#{encoded_canonical}"
|
113
|
+
end
|
114
|
+
|
115
|
+
def canonicalized_headers
|
116
|
+
canonicalized_positional_headers + canonicalized_amazon_headers
|
117
|
+
end
|
118
|
+
|
119
|
+
def canonicalized_positional_headers
|
120
|
+
POSITIONAL_HEADERS.collect do |header|
|
121
|
+
(@headers[header] || "") + "\n"
|
122
|
+
end.join
|
123
|
+
end
|
124
|
+
|
125
|
+
def canonicalized_amazon_headers
|
126
|
+
|
127
|
+
# select all headers that start with x-amz-
|
128
|
+
amazon_headers = @headers.select do |header, value|
|
129
|
+
header =~ /^x-amz-/
|
130
|
+
end
|
131
|
+
|
132
|
+
# Sort them alpabetically by key
|
133
|
+
amazon_headers = amazon_headers.sort do |a, b|
|
134
|
+
a[0] <=> b[0]
|
135
|
+
end
|
136
|
+
|
137
|
+
# Collect all of the amazon headers like this:
|
138
|
+
# {key}:{value}\n
|
139
|
+
# The value has to have any whitespace on the left stripped from it
|
140
|
+
# and any new-lines replaced by a single space.
|
141
|
+
# Finally, return the headers joined together as a single string and return it.
|
142
|
+
amazon_headers.collect do |header, value|
|
143
|
+
"#{header}:#{value.lstrip.gsub("\n"," ")}\n"
|
144
|
+
end.join
|
145
|
+
end
|
146
|
+
|
147
|
+
def canonicalized_resource
|
148
|
+
canonicalized_resource_string = "/"
|
149
|
+
canonicalized_resource_string += @bucket
|
150
|
+
canonicalized_resource_string += @request_path
|
151
|
+
canonicalized_resource_string
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,117 @@
|
|
1
|
+
class Hash
|
2
|
+
|
3
|
+
def downcase_keys
|
4
|
+
res = {}
|
5
|
+
each do |key, value|
|
6
|
+
key = key.downcase if key.respond_to?(:downcase)
|
7
|
+
res[key] = value
|
8
|
+
end
|
9
|
+
res
|
10
|
+
end
|
11
|
+
|
12
|
+
def join_values(separator = ',')
|
13
|
+
res = {}
|
14
|
+
each do |key, value|
|
15
|
+
res[key] = value.respond_to?(:join) ? value.join(separator) : value
|
16
|
+
end
|
17
|
+
res
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
|
22
|
+
module S3Lib
|
23
|
+
require 'time'
|
24
|
+
require 'base64'
|
25
|
+
require 'digest/sha1'
|
26
|
+
require 'openssl'
|
27
|
+
|
28
|
+
def self.request(verb, request_path, headers = {})
|
29
|
+
s3requester = AuthenticatedRequest.new()
|
30
|
+
s3requester.make_authenticated_request(verb, request_path, headers)
|
31
|
+
end
|
32
|
+
|
33
|
+
class AuthenticatedRequest
|
34
|
+
|
35
|
+
attr_reader :headers
|
36
|
+
POSITIONAL_HEADERS = ['content-md5', 'content-type', 'date']
|
37
|
+
HOST = 's3.amazonaws.com'
|
38
|
+
|
39
|
+
def make_authenticated_request(verb, request_path, headers = {})
|
40
|
+
@verb = verb
|
41
|
+
@request_path = request_path.gsub(/^\//,'') # Strip off the leading '/'
|
42
|
+
@amazon_id = ENV['AMAZON_ACCESS_KEY_ID']
|
43
|
+
@amazon_secret = ENV['AMAZON_SECRET_ACCESS_KEY']
|
44
|
+
@headers = headers.downcase_keys.join_values
|
45
|
+
fix_date
|
46
|
+
get_bucket_name
|
47
|
+
end
|
48
|
+
|
49
|
+
def fix_date
|
50
|
+
@headers['date'] ||= Time.now.httpdate
|
51
|
+
@headers.delete('date') if @headers.has_key?('x-amz-date')
|
52
|
+
end
|
53
|
+
|
54
|
+
def canonical_string
|
55
|
+
"#{@verb.to_s.upcase}\n#{canonicalized_headers}#{canonicalized_resource}"
|
56
|
+
end
|
57
|
+
|
58
|
+
def canonicalized_headers
|
59
|
+
"#{canonicalized_positional_headers}#{canonicalized_amazon_headers}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def canonicalized_positional_headers
|
63
|
+
POSITIONAL_HEADERS.collect do |header|
|
64
|
+
(@headers[header] || "") + "\n"
|
65
|
+
end.join
|
66
|
+
end
|
67
|
+
|
68
|
+
def canonicalized_amazon_headers
|
69
|
+
|
70
|
+
# select all headers that start with x-amz-
|
71
|
+
amazon_headers = @headers.select do |header, value|
|
72
|
+
header =~ /^x-amz-/
|
73
|
+
end
|
74
|
+
|
75
|
+
# Sort them alpabetically by key
|
76
|
+
amazon_headers = amazon_headers.sort do |a, b|
|
77
|
+
a[0] <=> b[0]
|
78
|
+
end
|
79
|
+
|
80
|
+
# Collect all of the amazon headers like this:
|
81
|
+
# {key}:{value}\n
|
82
|
+
# The value has to have any whitespace on the left stripped from it
|
83
|
+
# and any new-lines replaced by a single space.
|
84
|
+
# Finally, return the headers joined together as a single string and return it.
|
85
|
+
amazon_headers.collect do |header, value|
|
86
|
+
"#{header}:#{value.lstrip.gsub("\n"," ")}\n"
|
87
|
+
end.join
|
88
|
+
end
|
89
|
+
|
90
|
+
def canonicalized_resource
|
91
|
+
canonicalized_resource_string = "/"
|
92
|
+
canonicalized_resource_string += @bucket
|
93
|
+
canonicalized_resource_string += @request_path
|
94
|
+
canonicalized_resource_string
|
95
|
+
end
|
96
|
+
|
97
|
+
def get_bucket_name
|
98
|
+
@bucket = ""
|
99
|
+
return unless @headers.has_key?('host')
|
100
|
+
@headers['host'] = @headers['host'].downcase
|
101
|
+
return if @headers['host'] == 's3.amazonaws.com'
|
102
|
+
if @headers['host'] =~ /^([^.]+)(:\d\d\d\d)?\.#{HOST}$/ # Virtual hosting
|
103
|
+
@bucket = $1.gsub(/\/$/,'') + '/'
|
104
|
+
else
|
105
|
+
@bucket = @headers['host'].gsub(/(:\d\d\d\d)$/, '').gsub(/\/$/,'') + '/' # CNAME Virtual hosting
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def authorization_string
|
110
|
+
generator = OpenSSL::Digest::Digest.new('sha1')
|
111
|
+
encoded_canonical = Base64.encode64(OpenSSL::HMAC.digest(generator, @amazon_secret, canonical_string)).strip
|
112
|
+
|
113
|
+
"AWS #{@amazon_id}:#{encoded_canonical}"
|
114
|
+
end
|
115
|
+
|
116
|
+
end
|
117
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module S3Lib
|
2
|
+
require 'time'
|
3
|
+
|
4
|
+
def self.request(verb, request_path, headers = {})
|
5
|
+
s3requester = AuthenticatedRequest.new()
|
6
|
+
s3requester.make_authenticated_request(verb, request_path, headers)
|
7
|
+
end
|
8
|
+
|
9
|
+
class AuthenticatedRequest
|
10
|
+
|
11
|
+
POSITIONAL_HEADERS = ['content-md5', 'content-type', 'date']
|
12
|
+
|
13
|
+
def make_authenticated_request(verb, request_path, headers = {})
|
14
|
+
@verb = verb
|
15
|
+
@headers = headers
|
16
|
+
fix_date
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def fix_date
|
22
|
+
@headers['date'] ||= Time.now.httpdate
|
23
|
+
end
|
24
|
+
|
25
|
+
def canonical_string
|
26
|
+
"#{@verb.to_s.upcase}\n#{canonicalized_headers}"
|
27
|
+
end
|
28
|
+
|
29
|
+
def canonicalized_headers
|
30
|
+
"#{canonicalized_positional_headers}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def canonicalized_positional_headers
|
34
|
+
POSITIONAL_HEADERS.collect do |header|
|
35
|
+
(@headers[header] || "") + "\n"
|
36
|
+
end.join
|
37
|
+
end
|
38
|
+
|
39
|
+
end
|
40
|
+
end
|
data/lib/s3_errors.rb
ADDED
@@ -0,0 +1,58 @@
|
|
1
|
+
module S3Lib
|
2
|
+
|
3
|
+
|
4
|
+
class S3ResponseError < StandardError
|
5
|
+
attr_reader :response, :amazon_error_type, :status, :s3requester, :io
|
6
|
+
def initialize(message, io, s3requester)
|
7
|
+
@io = io
|
8
|
+
# Get the response and status from the IO object
|
9
|
+
@io.rewind
|
10
|
+
@response = @io.read
|
11
|
+
@io.rewind
|
12
|
+
@status = io.status
|
13
|
+
|
14
|
+
# The Amazon Error type will always look like <Code>AmazonErrorType</Code>. Find it with a RegExp.
|
15
|
+
@response =~ /<Code>(.*)<\/Code>/
|
16
|
+
@amazon_error_type = $1
|
17
|
+
|
18
|
+
# Make the AuthenticatedRequest instance available as well
|
19
|
+
@s3requester = s3requester
|
20
|
+
|
21
|
+
# Call the standard Error initializer
|
22
|
+
# if you put '%s' in the message it will be replaced by the amazon_error_type
|
23
|
+
message += "\namazon error type: %s" unless message =~ /\%s/
|
24
|
+
super(message % @amazon_error_type)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# Bucket errors
|
29
|
+
|
30
|
+
class NotYourBucketError < S3Lib::S3ResponseError
|
31
|
+
end
|
32
|
+
|
33
|
+
class BucketNotFoundError < S3Lib::S3ResponseError
|
34
|
+
end
|
35
|
+
|
36
|
+
class BucketNotEmptyError < S3Lib::S3ResponseError
|
37
|
+
end
|
38
|
+
|
39
|
+
# Object errors
|
40
|
+
|
41
|
+
class ObjectDoesNotExist < StandardError
|
42
|
+
end
|
43
|
+
|
44
|
+
class ObjectAccessForbidden < StandardError
|
45
|
+
end
|
46
|
+
|
47
|
+
class NoContentError < S3Lib::S3ResponseError
|
48
|
+
end
|
49
|
+
|
50
|
+
# ACL errors
|
51
|
+
class MalformedACLError < S3Lib::S3ResponseError
|
52
|
+
end
|
53
|
+
|
54
|
+
# Grant errors
|
55
|
+
class BadGrantTypeError < StandardError
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
data/lib/s3lib.rb
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
require File.join(File.dirname(__FILE__), 's3_authenticator')
|
2
|
+
require File.join(File.dirname(__FILE__), 's3_errors')
|
3
|
+
require File.join(File.dirname(__FILE__), 'service')
|
4
|
+
require File.join(File.dirname(__FILE__), 'object')
|
5
|
+
require File.join(File.dirname(__FILE__), 'bucket')
|
6
|
+
require File.join(File.dirname(__FILE__), 'acl')
|
7
|
+
require File.join(File.dirname(__FILE__), 'grant')
|
8
|
+
require 'rexml/document'
|
9
|
+
require 'rubygems'
|
10
|
+
require 'builder'
|