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.
Files changed (43) hide show
  1. data/Rakefile +24 -0
  2. data/VERSION.yml +5 -0
  3. data/bin/s3lib +15 -0
  4. data/bin/s3sh_as +15 -0
  5. data/github-test.rb +22 -0
  6. data/lib/acl.rb +134 -0
  7. data/lib/acl_access.rb +20 -0
  8. data/lib/acl_creating_a_grant_recipe.rb +95 -0
  9. data/lib/acl_reading_acl_recipe.rb +59 -0
  10. data/lib/acl_refreshing_cached_grants_recipe.rb +54 -0
  11. data/lib/bucket.rb +116 -0
  12. data/lib/bucket_before_refactoring.rb +120 -0
  13. data/lib/bucket_create.rb +39 -0
  14. data/lib/bucket_find.rb +41 -0
  15. data/lib/bucket_with_acl_mixin.rb +103 -0
  16. data/lib/error_handling.rb +12 -0
  17. data/lib/grant.rb +107 -0
  18. data/lib/grant_creating_a_grant_recipe.rb +103 -0
  19. data/lib/grant_reading_acl_recipe.rb +51 -0
  20. data/lib/object.rb +144 -0
  21. data/lib/object_from_bucket_test.rb +18 -0
  22. data/lib/object_take1.rb +150 -0
  23. data/lib/object_with_acl_mixin.rb +131 -0
  24. data/lib/put_with_curl_test.rb +39 -0
  25. data/lib/s3_authenticator.rb +155 -0
  26. data/lib/s3_authenticator_dev.rb +117 -0
  27. data/lib/s3_authenticator_dev_private.rb +40 -0
  28. data/lib/s3_errors.rb +58 -0
  29. data/lib/s3lib.rb +10 -0
  30. data/lib/s3lib_with_mixin.rb +11 -0
  31. data/lib/service.rb +24 -0
  32. data/lib/service_dev.rb +36 -0
  33. data/s3lib.gemspec +74 -0
  34. data/sample_usage.rb +45 -0
  35. data/test/acl_test.rb +89 -0
  36. data/test/amazon_headers_test.rb +87 -0
  37. data/test/canonical_resource_test.rb +53 -0
  38. data/test/canonical_string_tests.rb +73 -0
  39. data/test/first_test.rb +34 -0
  40. data/test/first_test_private.rb +55 -0
  41. data/test/full_test.rb +84 -0
  42. data/test/s3_authenticator_test.rb +291 -0
  43. metadata +109 -0
@@ -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'