internuity-awsum 0.2 → 0.3

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/Rakefile +2 -1
  2. data/lib/awsum.rb +1 -1
  3. data/lib/ec2/address.rb +118 -0
  4. data/lib/ec2/availability_zone.rb +68 -0
  5. data/lib/ec2/keypair.rb +75 -0
  6. data/lib/ec2/region.rb +96 -0
  7. data/lib/ec2/security_group.rb +175 -0
  8. data/lib/error.rb +55 -0
  9. data/lib/net_fix.rb +100 -0
  10. data/lib/requestable.rb +2 -0
  11. data/lib/s3/bucket.rb +66 -0
  12. data/lib/s3/headers.rb +24 -0
  13. data/lib/s3/object.rb +138 -0
  14. data/lib/s3/s3.rb +219 -0
  15. data/test/fixtures/ec2/addresses.xml +10 -0
  16. data/test/fixtures/ec2/allocate_address.xml +5 -0
  17. data/test/fixtures/ec2/associate_address.xml +5 -0
  18. data/test/fixtures/ec2/authorize_ip_access.xml +5 -0
  19. data/test/fixtures/ec2/authorize_owner_group_access.xml +5 -0
  20. data/test/fixtures/ec2/authorize_owner_group_access_error.xml +2 -0
  21. data/test/fixtures/ec2/availability_zones.xml +16 -0
  22. data/test/fixtures/ec2/create_key_pair.xml +29 -0
  23. data/test/fixtures/ec2/create_security_group.xml +5 -0
  24. data/test/fixtures/ec2/delete_key_pair.xml +5 -0
  25. data/test/fixtures/ec2/delete_security_group.xml +5 -0
  26. data/test/fixtures/ec2/deregister_image.xml +5 -0
  27. data/test/fixtures/ec2/disassociate_address.xml +5 -0
  28. data/test/fixtures/ec2/internal_error.xml +2 -0
  29. data/test/fixtures/ec2/invalid_amiid_error.xml +2 -0
  30. data/test/fixtures/ec2/invalid_request_error.xml +2 -0
  31. data/test/fixtures/ec2/key_pairs.xml +10 -0
  32. data/test/fixtures/ec2/regions.xml +14 -0
  33. data/test/fixtures/ec2/register_image.xml +5 -0
  34. data/test/fixtures/ec2/release_address.xml +5 -0
  35. data/test/fixtures/ec2/revoke_ip_access.xml +5 -0
  36. data/test/fixtures/ec2/revoke_owner_group_access.xml +5 -0
  37. data/test/fixtures/ec2/security_groups.xml +159 -0
  38. data/test/fixtures/ec2/unassociated_address.xml +10 -0
  39. data/test/fixtures/s3/buckets.xml +2 -0
  40. data/test/fixtures/s3/copy_failure.xml +23 -0
  41. data/test/fixtures/s3/invalid_request_signature.xml +5 -0
  42. data/test/fixtures/s3/keys.xml +2 -0
  43. data/test/units/ec2/test_addresses.rb +60 -0
  44. data/test/units/ec2/test_keypair.rb +87 -0
  45. data/test/units/ec2/test_regions.rb +33 -0
  46. data/test/units/ec2/test_security_group.rb +105 -0
  47. data/test/units/s3/test_bucket.rb +58 -0
  48. data/test/units/s3/test_object.rb +111 -0
  49. data/test/units/s3/test_s3.rb +298 -0
  50. data/test/units/test_error.rb +101 -0
  51. data/test/units/test_requestable.rb +241 -0
  52. data/test/work_out_string_to_sign.rb +7 -0
  53. metadata +132 -43
@@ -0,0 +1,55 @@
1
+ require 'parser'
2
+
3
+ module Awsum
4
+ class Error < StandardError
5
+ attr_reader :response_code, :code, :message, :request_id, :additional
6
+
7
+ def initialize(response)
8
+ @response_code = response.code
9
+ parser = ErrorParser.new
10
+ parser.parse(response.body)
11
+ @code = parser.code
12
+ @message = parser.message
13
+ @request_id = parser.request_id
14
+ @additional = parser.additional
15
+ end
16
+
17
+ def inspect
18
+ "#<Awsum::Error response_code=#{@response_code} code=#{@code} request_id=#{@request_id} message=#{@message}>"
19
+ end
20
+
21
+ private
22
+ class ErrorParser < Awsum::Parser #:nodoc:
23
+ attr_reader :code, :message, :request_id, :additional
24
+
25
+ def initialize
26
+ @additional = {}
27
+ @text = ""
28
+ end
29
+
30
+ def tag_start(tag, attributes)
31
+ end
32
+
33
+ def text(text)
34
+ @text << text unless @text.nil?
35
+ end
36
+
37
+ def tag_end(tag)
38
+ text = @text.strip
39
+ return if text.blank?
40
+
41
+ case tag
42
+ when 'Code'
43
+ @code = text
44
+ when 'Message'
45
+ @message = text
46
+ when 'RequestID', 'RequestId'
47
+ @request_id = text
48
+ else
49
+ @additional[tag] = text
50
+ end
51
+ @text = ''
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,100 @@
1
+ # Some fixes for the net/http libraries to better suppport S3
2
+ module Net
3
+ class BufferedIO
4
+ #Increase the default read size for streaming from the socket
5
+ def rbuf_fill
6
+ timeout(@read_timeout) {
7
+ @rbuf << @io.sysread(1024 * 16)
8
+ }
9
+ end
10
+ end
11
+
12
+ class HTTPGenericRequest
13
+ @@local_read_size = 1024 * 16
14
+
15
+ # Added limit which can be one of :headers or :body to limit the sending of
16
+ # a request to either the headers or the body in order to make use of
17
+ # 100-continue processing for S3
18
+ def exec(sock, ver, path, limit = nil) #:nodoc: internal use only
19
+ if @body
20
+ send_request_with_body sock, ver, path, @body, limit
21
+ elsif @body_stream
22
+ send_request_with_body_stream sock, ver, path, @body_stream, limit
23
+ else
24
+ write_header sock, ver, path
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ # Will send both headers and body unless limit is set to either
31
+ # :headers or :body to restrict to one
32
+ def send_request_with_body(sock, ver, path, body, limit = nil)
33
+ self.content_length = body.length
34
+ delete 'Transfer-Encoding'
35
+ supply_default_content_type
36
+ write_header sock, ver, path unless limit == :body
37
+ sock.write body unless limit == :headers
38
+ end
39
+
40
+ # Will send both headers and body unless limit is set to either
41
+ # :headers or :body to restrict to one
42
+ #
43
+ # Increased the default read size for streaming from local streams to 1MB
44
+ def send_request_with_body_stream(sock, ver, path, f, limit = nil)
45
+ unless content_length() or chunked?
46
+ raise ArgumentError,
47
+ "Content-Length not given and Transfer-Encoding is not `chunked'"
48
+ end
49
+ supply_default_content_type
50
+ write_header sock, ver, path unless limit == :body
51
+ if limit != :headers
52
+ if chunked?
53
+ while s = f.read(1024 * 1024)
54
+ sock.write(sprintf("%x\r\n", s.length) << s << "\r\n")
55
+ end
56
+ sock.write "0\r\n\r\n"
57
+ else
58
+ while s = f.read(1024 * 1024)
59
+ sock.write s
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+
66
+ class HTTP < Protocol
67
+ # Patched to handle 100-continue processing for S3
68
+ def request(req, body = nil, &block) # :yield: +response+
69
+ unless started?
70
+ start {
71
+ req['connection'] ||= 'close'
72
+ return request(req, body, &block)
73
+ }
74
+ end
75
+ if proxy_user()
76
+ unless use_ssl?
77
+ req.proxy_basic_auth proxy_user(), proxy_pass()
78
+ end
79
+ end
80
+
81
+ req.set_body_internal body
82
+ begin_transport req
83
+ # Send only the headers if a 100-continue request
84
+ limit = ((req.is_a?(Post) || req.is_a?(Put)) && req['expect'] == '100-continue') ? :headers : nil
85
+ req.exec @socket, @curr_http_version, edit_path(req.path), limit
86
+ begin
87
+ res = HTTPResponse.read_new(@socket)
88
+ if res.is_a?(HTTPContinue) && limit && req['content-length'].to_i > 0
89
+ req.exec @socket, @curr_http_version, edit_path(req.path), :body
90
+ end
91
+ end while res.kind_of?(HTTPContinue)
92
+ res.reading_body(@socket, req.response_body_permitted?) {
93
+ yield res if block_given?
94
+ }
95
+ end_transport req, res
96
+
97
+ res
98
+ end
99
+ end
100
+ end
@@ -190,6 +190,8 @@ module Awsum
190
190
  uri.host = new_uri.host
191
191
  uri.path = "#{new_uri.path}#{uri.path unless uri.path = '/'}"
192
192
  process_request(method, uri.to_s, headers, data, &block)
193
+ else
194
+ response
193
195
  end
194
196
  end
195
197
 
@@ -0,0 +1,66 @@
1
+ module Awsum
2
+ class S3
3
+ class Bucket
4
+ attr_reader :name, :creation_date
5
+
6
+ def initialize(s3, name, creation_date = nil)
7
+ @s3 = s3
8
+ @name = name
9
+ @creation_date = creation_date
10
+ end
11
+
12
+ # Delete this Bucket
13
+ def delete
14
+ @s3.delete_bucket(@name)
15
+ end
16
+
17
+ # Delete this Bucket, recursively deleting all keys first
18
+ def delete!
19
+ @s3.keys(@name).each do |key|
20
+ key.delete
21
+ end
22
+ delete
23
+ end
24
+ end
25
+
26
+ class BucketParser < Awsum::Parser #:nodoc:
27
+ def initialize(s3)
28
+ @s3 = s3
29
+ @buckets = []
30
+ @text = nil
31
+ end
32
+
33
+ def tag_start(tag, attributes)
34
+ case tag
35
+ when 'Bucket'
36
+ @current = {}
37
+ @text = ''
38
+ end
39
+ end
40
+
41
+ def text(text)
42
+ @text << text unless @text.nil?
43
+ end
44
+
45
+ def tag_end(tag)
46
+ case tag
47
+ when 'Bucket'
48
+ @buckets << Bucket.new(
49
+ @s3,
50
+ @current['Name'],
51
+ Time.parse(@current['CreationDate'])
52
+ )
53
+ @text = nil
54
+ @current = nil
55
+ else
56
+ text = @text.strip unless @text.nil?
57
+ @current[tag] = (text == '' ? nil : text) unless @current.nil?
58
+ end
59
+ end
60
+
61
+ def result
62
+ @buckets
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,24 @@
1
+ module Awsum
2
+ class S3
3
+ class Headers #:nodoc:
4
+ def initialize(response)
5
+ @response = response
6
+ end
7
+
8
+ # Locking down to HTTPHeader methods only
9
+ def method_missing(method, *args, &block)
10
+ if !%w(body body_permitted? entity inspect read_body to_ary value).include?(method.to_s) && @response.respond_to?(method)
11
+ @response.send(method, *args, &block)
12
+ else
13
+ raise NoMethodError.new("undefined method `#{method}' for #{inspect}")
14
+ end
15
+ end
16
+
17
+ def inspect
18
+ headers = []
19
+ @response.canonical_each do |h,v| headers << h end
20
+ "#<Awsum::S3::Headers \"#{headers.join('", "')}\">"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,138 @@
1
+ module Awsum
2
+ class S3
3
+ class Object
4
+ attr_reader :key, :bucket, :last_modified, :etag, :size, :owner, :storage_class
5
+
6
+ def initialize(s3, bucket, key, last_modified, etag, size, owner, storage_class)
7
+ @s3 = s3
8
+ @bucket = bucket
9
+ @key = key
10
+ @last_modified = last_modified
11
+ @etag = etag
12
+ @size = size
13
+ @owner = owner
14
+ @storage_class = storage_class
15
+ end
16
+
17
+ # Get the headers for this Object
18
+ #
19
+ # All header methods map directly to the Net::HTTPHeader module
20
+ def headers
21
+ @headers ||= @s3.object_headers(@bucket, @key)
22
+ end
23
+
24
+ # Retrieve the data stored for this Object
25
+ #
26
+ # You can get the data as a single call or add a block to retrieve the data in chunks
27
+ # ==Examples
28
+ # content = object.data
29
+ #
30
+ # or
31
+ #
32
+ # object.data do |chunk|
33
+ # # handle chunk
34
+ # puts chunk
35
+ # end
36
+ def data(&block)
37
+ @s3.object_data @bucket, @key, &block
38
+ end
39
+
40
+ # Delete this Key
41
+ def delete
42
+ @s3.delete_object(@bucket, @key)
43
+ end
44
+
45
+ # Make a copy of this Object with a new key
46
+ def copy(new_key, headers = nil, meta_headers = nil)
47
+ @s3.copy_object(@bucket, @key, nil, new_key, headers, meta_headers)
48
+ end
49
+
50
+ # Rename or move this Object to a new key
51
+ def rename(new_key, headers = nil, meta_headers = nil)
52
+ copied = @s3.copy_object(@bucket, @key, nil, new_key, headers, meta_headers)
53
+ @s3.delete_object(@bucket, @key) if copied
54
+ end
55
+ alias_method :move, :rename
56
+
57
+ # Copy this Object to another Bucket
58
+ #
59
+ def copy_to(new_bucket, new_key = nil, headers = nil, meta_headers = nil)
60
+ @s3.copy_object(@bucket, @key, new_bucket, new_key, headers, meta_headers)
61
+ end
62
+
63
+ # Move this Object to another Bucket
64
+ def move_to(new_bucket, new_key = nil, headers = nil, meta_headers = nil)
65
+ copied = @s3.copy_object(@bucket, @key, new_bucket, new_key, headers, meta_headers)
66
+ @s3.delete_object(@bucket, @key) if copied
67
+ end
68
+ end
69
+
70
+ #TODO: Create a more advanced array which can deal with pagination
71
+ class ObjectParser < Awsum::Parser #:nodoc:
72
+ def initialize(s3)
73
+ @s3 = s3
74
+ @bucket = ''
75
+ @objects = []
76
+ @text = nil
77
+ @stack = []
78
+ end
79
+
80
+ def tag_start(tag, attributes)
81
+ case tag
82
+ when 'ListBucketResult'
83
+ @stack << tag
84
+ @text = ''
85
+ when 'Contents'
86
+ @stack << tag
87
+ @current = {}
88
+ @text = ''
89
+ when 'Owner'
90
+ @owner = {}
91
+ @stack << tag
92
+ end
93
+ end
94
+
95
+ def text(text)
96
+ @text << text unless @text.nil?
97
+ end
98
+
99
+ def tag_end(tag)
100
+ case tag
101
+ when 'Name'
102
+ if @stack[-1] == 'ListBucketResult'
103
+ @bucket = @text.strip
104
+ end
105
+ when 'Contents'
106
+ @objects << Object.new(
107
+ @s3,
108
+ @bucket,
109
+ @current['Key'],
110
+ Time.parse(@current['LastModified']),
111
+ @current['ETag'],
112
+ @current['Size'].to_i,
113
+ {'id' => @owner['ID'], 'name' => @owner['DisplayName']},
114
+ @current['StorageClass']
115
+ )
116
+ @current = nil
117
+ @text = nil
118
+ @stack.pop
119
+ when 'Owner'
120
+ @stack.pop
121
+ else
122
+ text = @text.strip unless @text.nil?
123
+ case @stack[-1]
124
+ when 'Owner'
125
+ @owner[tag] = (text == '' ? nil : text) unless @owner.nil?
126
+ when 'Contents'
127
+ @current[tag] = (text == '' ? nil : text) unless @current.nil?
128
+ end
129
+ @text = ''
130
+ end
131
+ end
132
+
133
+ def result
134
+ @objects
135
+ end
136
+ end
137
+ end
138
+ end
@@ -0,0 +1,219 @@
1
+ require 's3/bucket'
2
+ require 's3/object'
3
+ require 's3/headers'
4
+
5
+ module Awsum
6
+ # Handles all interaction with Amazon S3
7
+ #
8
+ #--
9
+ # TODO: Change this to S3
10
+ # ==Getting Started
11
+ # Create an Awsum::Ec2 object and begin calling methods on it.
12
+ # require 'rubygems'
13
+ # require 'awsum'
14
+ # ec2 = Awsum::Ec2.new('your access id', 'your secret key')
15
+ # images = ec2.my_images
16
+ # ...
17
+ #
18
+ # All calls to EC2 can be done directly in this class, or through a more
19
+ # object oriented way through the various returned classes
20
+ #
21
+ # ==Examples
22
+ # ec2.image('ami-ABCDEF').run
23
+ #
24
+ # ec2.instance('i-123456789').volumes.each do |vol|
25
+ # vol.create_snapsot
26
+ # end
27
+ #
28
+ # ec2.regions.each do |region|
29
+ # region.use
30
+ # images.each do |img|
31
+ # puts "#{img.id} - #{region.name}"
32
+ # end
33
+ # end
34
+ # end
35
+ #
36
+ # ==Errors
37
+ # All methods will raise an Awsum::Error if an error is returned from Amazon
38
+ #
39
+ # ==Missing Methods
40
+ # If you need any of this functionality, please consider getting involved
41
+ # and help complete this library.
42
+ class S3
43
+ include Awsum::Requestable
44
+
45
+ # Create an new S3 instance
46
+ #
47
+ # The access_key and secret_key are both required to do any meaningful work.
48
+ #
49
+ # If you want to get these keys from environment variables, you can do that
50
+ # in your code as follows:
51
+ # s3 = Awsum::S3.new(ENV['AWS_ACCESS_KEY_ID'], ENV['AWS_SECRET_ACCESS_KEY'])
52
+ def initialize(access_key = nil, secret_key = nil)
53
+ @access_key = access_key
54
+ @secret_key = secret_key
55
+ end
56
+
57
+ # List all the Bucket(s)
58
+ def buckets
59
+ response = send_s3_request
60
+ parser = Awsum::S3::BucketParser.new(self)
61
+ parser.parse(response.body)
62
+ end
63
+
64
+ def bucket(name)
65
+ Bucket.new(self, name)
66
+ end
67
+
68
+ # Create a new Bucket
69
+ #
70
+ # ===Parameters
71
+ # * <tt>bucket_name</tt> - The name of the new bucket
72
+ # * <tt>location</tt> <i>(optional)</i> - Can be <tt>:default</tt>, <tt>:us</tt> or <tt>:eu</tt>
73
+ def create_bucket(bucket_name, location = :default)
74
+ raise ArgumentError.new('Bucket name cannot be in an ip address style') if bucket_name =~ /^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/
75
+ raise ArgumentError.new('Bucket name can only have lowercase letters, numbers, periods (.), underscores (_) and dashes (-)') unless bucket_name =~ /^[\w\d][-a-z\d._]+[a-z\d._]$/
76
+ raise ArgumentError.new('Bucket name cannot contain a dash (-) next to a period (.)') if bucket_name =~ /\.-|-\./
77
+ raise ArgumentError.new('Bucket name must be between 3 and 63 characters') if bucket_name.size < 3 || bucket_name.size > 63
78
+
79
+ data = nil
80
+ if location == :eu
81
+ data = '<CreateBucketConfiguration><LocationConstraint>EU</LocationConstraint></CreateBucketConfiguration>'
82
+ end
83
+
84
+ response = send_s3_request('PUT', :bucket => bucket_name, :data => data)
85
+ response.is_a?(Net::HTTPSuccess)
86
+ end
87
+
88
+ def delete_bucket(bucket_name)
89
+ response = send_s3_request('DELETE', :bucket => bucket_name)
90
+ response.is_a?(Net::HTTPSuccess)
91
+ end
92
+
93
+ # List the Key(s) of a Bucket
94
+ #
95
+ # ===Parameters
96
+ # * <tt>bucket_name</tt> - The name of the bucket to search for keys
97
+ # ====Options
98
+ # * <tt>:prefix</tt> - Limits the response to keys which begin with the indicated prefix. You can use prefixes to separate a bucket into different sets of keys in a way similar to how a file system uses folders.
99
+ # * <tt>:marker</tt> - Indicates where in the bucket to begin listing. The list will only include keys that occur lexicographically after marker. This is convenient for pagination: To get the next page of results use the last key of the current page as the marker.
100
+ # * <tt>:max_keys</tt> - The maximum number of keys you'd like to see in the response body. The server might return fewer than this many keys, but will not return more.
101
+ # * <tt>:delimeter</tt> - Causes keys that contain the same string between the prefix and the first occurrence of the delimiter to be rolled up into a single result element in the CommonPrefixes collection. These rolled-up keys are not returned elsewhere in the response.
102
+ def keys(bucket_name, options = {})
103
+ paramters = {}
104
+ paramters['prefix'] = options[:prefix] if options[:prefix]
105
+ paramters['marker'] = options[:marker] if options[:marker]
106
+ paramters['max_keys'] = options[:max_keys] if options[:max_keys]
107
+ paramters['prefix'] = options[:prefix] if options[:prefix]
108
+
109
+ response = send_s3_request('GET', :bucket => bucket_name, :paramters => paramters)
110
+ parser = Awsum::S3::ObjectParser.new(self)
111
+ parser.parse(response.body)
112
+ end
113
+
114
+ # Create a new Object in the specified Bucket
115
+ #
116
+ # ===Parameters
117
+ # * <tt>bucket_name</tt> - The name of the Bucket in which to store the Key
118
+ # * <tt>key</tt> - The name/path of the Key to store
119
+ # * <tt>data</tt> - The data to be stored in this Object
120
+ # * <tt>headers</tt> - Standard HTTP headers to be sent along
121
+ # * <tt>meta_headers</tt> - Meta headers to be stored along with the key
122
+ # * <tt>acl</tt> - A canned access policy, can be one of <tt>:private</tt>, <tt>:public_read</tt>, <tt>:public_read_write</tt> or <tt>:authenticated_read</tt>
123
+ def create_object(bucket_name, key, data, headers = {}, meta_headers = {}, acl = :private)
124
+ headers = headers.dup
125
+ meta_headers.each do |k,v|
126
+ headers[k =~ /^x-amz-meta-/i ? k : "x-amz-meta-#{k}"] = v
127
+ end
128
+ headers['x-amz-acl'] = acl.to_s.gsub(/_/, '-')
129
+
130
+ response = send_s3_request('PUT', :bucket => bucket_name, :key => key, :headers => headers, :data => data)
131
+ response.is_a?(Net::HTTPSuccess)
132
+ end
133
+
134
+ # Retrieve the headers for this Object
135
+ #
136
+ # All header methods map directly to the Net::HTTPHeader module
137
+ def object_headers(bucket_name, key)
138
+ response = send_s3_request('HEAD', :bucket => bucket_name, :key => key)
139
+ Headers.new(response)
140
+ end
141
+
142
+ # Retrieve the data stored for the specified Object
143
+ #
144
+ # You can get the data as a single call or add a block to retrieve the data in chunks
145
+ # ==Examples
146
+ # data = s3.object_data('test-bucket', 'key')
147
+ #
148
+ # or
149
+ #
150
+ # s3.object_data('test-bucket', 'key') do |chunk|
151
+ # # handle chunk
152
+ # puts chunk
153
+ # end
154
+ def object_data(bucket_name, key, &block)
155
+ send_s3_request('GET', :bucket => bucket_name, :key => key) do |response|
156
+ if block_given?
157
+ response.read_body &block
158
+ return true
159
+ else
160
+ return response.body
161
+ end
162
+ end
163
+ end
164
+
165
+ # Deletes an Object from a Bucket
166
+ def delete_object(bucket_name, key)
167
+ response = send_s3_request('DELETE', :bucket => bucket_name, :key => key)
168
+ response.is_a?(Net::HTTPSuccess)
169
+ end
170
+
171
+ # Copy the contents of an Object to another key and/or bucket
172
+ #
173
+ # ===Parameters
174
+ # * <tt>source_bucket_name</tt> - The name of the Bucket from which to copy
175
+ # * <tt>source_key</tt> - The name of the Key from which to copy
176
+ # * <tt>destination_bucket_name</tt> - The name of the Bucket to which to copy (Can be nil if copying within the same bucket, or updating header data of existing Key)
177
+ # * <tt>destination_key</tt> - The name of the Key to which to copy (Can be nil if copying to a new bucket with same key, or updating header data of existing Key)
178
+ # * <tt>headers</tt> - If not nil, the headers are replaced with this information
179
+ # * <tt>meta_headers</tt> - If not nil, the meta headers are replaced with this information
180
+ #
181
+ #--
182
+ # TODO: Need to handle copy-if-... headers
183
+ def copy_object(source_bucket_name, source_key, destination_bucket_name = nil, destination_key= nil, headers = nil, meta_headers = nil)
184
+ raise ArgumentError.new('You must include one of destination_bucket_name, destination_key or headers to be replaced') if destination_bucket_name.nil? && destination_key.nil? && headers.nil? && meta_headers.nil?
185
+
186
+ headers = {
187
+ 'x-amz-copy-source' => "/#{source_bucket_name}/#{source_key}",
188
+ 'x-amz-metadata-directive' => (((destination_bucket_name.nil? && destination_key.nil?) || !(headers.nil? || meta_headers.nil?)) ? 'REPLACE' : 'COPY')
189
+ }.merge(headers||{})
190
+ meta_headers.each do |k,v|
191
+ headers[k =~ /^x-amz-meta-/i ? k : "x-amz-meta-#{k}"] = v
192
+ end unless meta_headers.nil?
193
+
194
+ destination_bucket_name ||= source_bucket_name
195
+ destination_key ||= source_key
196
+
197
+ response = send_s3_request('PUT', :bucket => destination_bucket_name, :key => destination_key, :headers => headers, :data => nil)
198
+ if response.is_a?(Net::HTTPSuccess)
199
+ #Check for delayed error (See http://docs.amazonwebservices.com/AmazonS3/2006-03-01/RESTObjectCOPY.html#RESTObjectCOPY_Response)
200
+ response_body = response.body
201
+ if response_body =~ /<Error>/i
202
+ raise Awsum::Error.new(response)
203
+ else
204
+ true
205
+ end
206
+ end
207
+ end
208
+
209
+ #private
210
+ #The host to make all requests against
211
+ def host
212
+ @host ||= 's3.amazonaws.com'
213
+ end
214
+
215
+ def host=(host)
216
+ @host = host
217
+ end
218
+ end
219
+ end