s3lib 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- 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'
|