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,39 @@
1
+ # s3_bucket.rb
2
+ require File.join(File.dirname(__FILE__), 's3_authenticator')
3
+ module S3Lib
4
+
5
+ class NotYourBucketError < S3Lib::S3ResponseError
6
+ end
7
+
8
+ class Bucket
9
+
10
+ # Todo:
11
+ # Class methods
12
+ # Bucket::find
13
+ # Bucket::delete (have :force => true)
14
+ # Bucket::delete_all_objects
15
+ # Bucket::objects
16
+ # Bucket::new
17
+ # instance methods
18
+ # Bucket#objects
19
+ # Bucket#delete
20
+ # Bucket#delete_all_objects
21
+ # Bucket#each
22
+
23
+ def self.create(name, params = {})
24
+ params['x-amz-acl'] = params.delete(:access) if params[:access] # translate from :access to 'x-amz-acl'
25
+ begin
26
+ response = S3Lib.request(:put, name, params)
27
+ rescue S3Lib::S3ResponseError => error
28
+ if error.amazon_error_type == "BucketAlreadyExists"
29
+ raise S3Lib::NotYourBucketError.new("The bucket '#{name}' is already owned by somebody else", error.io, error.s3requester)
30
+ else
31
+ raise # re-raise the exception if it's not a BucketAlreadyExists error
32
+ end
33
+ end
34
+ response.status[0] == "200" ? true : false
35
+ end
36
+
37
+ end
38
+
39
+ end
@@ -0,0 +1,41 @@
1
+ # s3_bucket.rb
2
+ require File.join(File.dirname(__FILE__), 's3_authenticator')
3
+ require 'rexml/document'
4
+
5
+ module S3Lib
6
+
7
+ class Bucket
8
+
9
+ attr_reader :xml, :prefix, :marker, :max_keys
10
+
11
+ def self.find(name, params = {})
12
+ response = S3Lib.request(:get, name)
13
+ doc = REXML::Document.new(response)
14
+ Bucket.new(doc)
15
+ end
16
+
17
+ def initialize(doc)
18
+ @xml = doc.root
19
+ @name = @xml.elements['Name'].text
20
+ @max_keys = @xml.elements['MaxKeys'].text.to_i
21
+ @prefix = @xml.elements['Prefix'].text
22
+ @marker = @xml.elements['Marker'].text
23
+ end
24
+
25
+ def is_truncated?
26
+ @xml.elements['IsTruncated'].text == 'true'
27
+ end
28
+
29
+ def objects
30
+ REXML::XPath.match(@xml, '//Contents').collect do |object|
31
+ S3Lib::S3Object.new(object)
32
+ end
33
+ end
34
+
35
+ end
36
+
37
+ end
38
+
39
+ if __FILE__ == $0
40
+ S3Lib::Bucket.find('spatten_syncdemo')
41
+ end
@@ -0,0 +1,103 @@
1
+ # bucket.rb
2
+ module S3Lib
3
+
4
+ class Bucket
5
+
6
+ attr_reader :name, :xml, :prefix, :marker, :max_keys
7
+
8
+ include S3Lib::AclAccess
9
+
10
+ def self.create(name, params = {})
11
+ params['x-amz-acl'] = params.delete(:access) if params[:access] # translate from :access to 'x-amz-acl'
12
+ response = self.bucket_request(:put, name, params)
13
+ response.status[0] == "200" ? true : false
14
+ end
15
+
16
+ # passing :force => true will cause the bucket to be deleted even if it is not empty.
17
+ def self.delete(name, params = {})
18
+ if params.delete(:force)
19
+ self.delete_all(name, params)
20
+ end
21
+ response = self.bucket_request(:delete, name, params)
22
+ end
23
+
24
+ def delete(params = {})
25
+ self.class.delete(@name, @params.merge(params))
26
+ end
27
+
28
+ def self.delete_all(name, params = {})
29
+ bucket = Bucket.find(name, params)
30
+ bucket.delete_all
31
+ end
32
+
33
+ def delete_all
34
+ objects.each do |object|
35
+ object.delete
36
+ end
37
+ end
38
+
39
+ def self.find(name, params = {})
40
+ response = self.bucket_request(:get, name, params)
41
+ doc = REXML::Document.new(response)
42
+ Bucket.new(doc, params)
43
+ end
44
+
45
+ def initialize(doc, params = {})
46
+ @xml = doc.root
47
+ @params = params
48
+ @name = @xml.elements['Name'].text
49
+ @max_keys = @xml.elements['MaxKeys'].text.to_i
50
+ @prefix = @xml.elements['Prefix'].text
51
+ @marker = @xml.elements['Marker'].text
52
+ end
53
+
54
+ def is_truncated?
55
+ @xml.elements['IsTruncated'].text == 'true'
56
+ end
57
+
58
+ def objects(params = {})
59
+ refresh if params[:refresh]
60
+ @objects || get_objects
61
+ end
62
+
63
+ def refresh
64
+ refreshed_bucket = Bucket.find(@name, @params)
65
+ @xml = refreshed_bucket.xml
66
+ @objects = nil
67
+ end
68
+
69
+ def url
70
+ @name
71
+ end
72
+
73
+ # access an object in the bucket by key name
74
+ def [](key)
75
+ objects.detect{|object| object.key == key}
76
+ end
77
+
78
+ private
79
+
80
+ def self.bucket_request(verb, name, params = {})
81
+ begin
82
+ response = S3Lib.request(verb, name, params)
83
+ rescue S3Lib::S3ResponseError => error
84
+ case error.amazon_error_type
85
+ when "NoSuchBucket": raise S3Lib::BucketNotFoundError.new("The bucket '#{name}' does not exist.", error.io, error.s3requester)
86
+ when "NotSignedUp": raise S3Lib::NotYourBucketError.new("The bucket '#{name}' is owned by someone else.", error.io, error.s3requester)
87
+ when "BucketNotEmpty": raise S3Lib::BucketNotEmptyError.new("The bucket '#{name}' is not empty, so you can't delete it.\nTry using Bucket.delete_all('#{name}') first, or Bucket.delete('#{name}', :force => true).", error.io, error.s3requester)
88
+ else # Re-raise the error if it's not one of the above
89
+ raise
90
+ end
91
+ end
92
+ end
93
+
94
+ def get_objects
95
+ @objects = REXML::XPath.match(@xml, '//Contents').collect do |object|
96
+ key = object.elements['Key'].text
97
+ S3Lib::S3Object.new(self, key, :lazy_load => true)
98
+ end
99
+ end
100
+
101
+ end
102
+
103
+ end
@@ -0,0 +1,12 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.join(File.dirname(__FILE__),'s3_authenticator')
4
+
5
+ begin
6
+ req = S3Lib.request(:put, "spatten_sample_bucket/sample_object", :body => "Wheee")
7
+ rescue S3Lib::S3ResponseError => e
8
+ puts "Amazon Error Type: #{e.amazon_error_type}"
9
+ puts "HTTP Status: #{e.status.join(',')}"
10
+ puts "Response from Amazon: #{e.response}"
11
+ puts "canonical string: #{e.s3requester.canonical_string}" if e.amazon_error_type == 'SignatureDoesNotMatch'
12
+ end
data/lib/grant.rb ADDED
@@ -0,0 +1,107 @@
1
+ module S3Lib
2
+ class Grant
3
+ attr_reader :acl, :grantee, :type, :permission
4
+ GRANT_TYPES = {:canonical => 'CanonicalUser',
5
+ :email => 'AmazonCustomerByEmail',
6
+ :all_s3 => 'Group',
7
+ :public => 'Group'}
8
+ GROUP_URIS = {'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' => :all_s3,
9
+ 'http://acs.amazonaws.com/groups/global/AllUsers' => :public}
10
+ PERMISSIONS = [:read, :write, :read_acl, :write_acl, :full_control]
11
+ NAMESPACE_URI = 'http://www.w3.org/2001/XMLSchema-instance'
12
+
13
+ # Create a new grant.
14
+ # permission is one of the PERMISSIONS defined above
15
+ # grantee can be either a REXML::Document object or a Hash
16
+ # The grantee Hash should look like this:
17
+ # {:type => :canonical|:email|:all_s3|:public,
18
+ # :grantee => canonical_user_id | email_address}
19
+ #
20
+ # The :grantee element of the hash is only required (and meaningful)
21
+ # for :canonical and :email Grants
22
+ def initialize(permission, grantee)
23
+ @type = parse_type(grantee)
24
+ @permission = parse_permission(permission)
25
+ @grantee = parse_grantee(grantee)
26
+ end
27
+
28
+ def to_xml
29
+ builder = Builder::XmlMarkup.new(:indent => 2)
30
+ xml = builder.Grant do
31
+ builder.Grantee('xmlns:xsi' => NAMESPACE_URI, 'xsi:type' => GRANT_TYPES[@type]) do
32
+ case type
33
+ when :canonical: builder.ID(@grantee)
34
+ when :email: builder.EmailAddress(@grantee)
35
+ when :all_s3: builder.URI(group_uri_from_group_type(:all_s3))
36
+ when :public: builder.URI(group_uri_from_group_type(:public))
37
+ else
38
+ end
39
+ end
40
+ builder.Permission(@permission.to_s.upcase)
41
+ end
42
+ end
43
+
44
+ def inspect
45
+ "#{@permission} to #{@type}#{" #{@grantee}" if [:email, :canonical].include?(@type)}"
46
+ end
47
+
48
+ private
49
+
50
+ # permission can either be the String provided by S3
51
+ # or a symbol (see the PERMISSIONS array for allowed values)
52
+ def parse_permission(permission)
53
+ if permission.is_a?(String)
54
+ permission.downcase.to_sym
55
+ else
56
+ permission
57
+ end
58
+ end
59
+
60
+ def parse_type(grantee)
61
+ if grantee.is_a?(Hash)
62
+ grantee[:type]
63
+ else # Assume it's a REXML::Doc object
64
+ type = grantee.attributes['xsi:type']
65
+ case type
66
+ when 'CanonicalUser': :canonical
67
+ when 'AmazonCustomerByEmail': :email
68
+ when 'Group'
69
+ group_uri = grantee.elements['URI'].text
70
+ group_type_from_group_uri(group_uri)
71
+ else
72
+ raise BadGrantTypeError
73
+ end
74
+ end
75
+ end
76
+
77
+ def parse_grantee(grantee)
78
+ if grantee.is_a?(Hash)
79
+ if [:canonical, :email].include?(@type)
80
+ grantee[:grantee]
81
+ else
82
+ @type
83
+ end
84
+ else # it's a REXML::Doc object
85
+ case @type
86
+ when :canonical
87
+ grantee.elements['ID'].text
88
+ when :email
89
+ grantee.elements['EmailAddress'].text
90
+ when :all_s3: :all_s3
91
+ when :public: :public
92
+ else
93
+ nil
94
+ end
95
+ end
96
+ end
97
+
98
+ def group_type_from_group_uri(group_uri)
99
+ GROUP_URIS[group_uri]
100
+ end
101
+
102
+ def group_uri_from_group_type(group_type)
103
+ GROUP_URIS.invert[group_type]
104
+ end
105
+
106
+ end
107
+ end
@@ -0,0 +1,103 @@
1
+ module S3Lib
2
+ class Grant
3
+ attr_reader :acl, :grantee, :type, :permission
4
+ GRANT_TYPES = {:canonical => 'CanonicalUser',
5
+ :email => 'AmazonCustomerByEmail',
6
+ :all_s3 => 'Group',
7
+ :public => 'Group'}
8
+ GROUP_URIS = {'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' => :all_s3,
9
+ 'http://acs.amazonaws.com/groups/global/AllUsers' => :public}
10
+ PERMISSIONS = [:read, :write, :read_acl, :write_acl, :full_control]
11
+ NAMESPACE_URI = 'http://www.w3.org/2001/XMLSchema-instance'
12
+
13
+ # Create a new grant.
14
+ # permission is one of the PERMISSIONS defined above
15
+ # grantee can be either a REXML::Document object or a Hash
16
+ # The grantee Hash should look like this:
17
+ # {:type => :canonical|:email|:all_s3|:public,
18
+ # :grantee => canonical_user_id | email_address}
19
+ #
20
+ # The :grantee element of the hash is only required (and meaningful)
21
+ # for :canonical and :email Grants
22
+ def initialize(permission, grantee)
23
+ @type = parse_type(grantee)
24
+ @permission = parse_permission(permission)
25
+ @grantee = parse_grantee(grantee)
26
+ end
27
+
28
+ def to_xml
29
+ builder = Builder::XmlMarkup.new(:indent => 2)
30
+ xml = builder.Grant do
31
+ builder.Grantee('xmlns:xsi' => NAMESPACE_URI, 'xsi:type' => GRANT_TYPES[@type]) do
32
+ case type
33
+ when :canonical: builder.ID(@grantee)
34
+ when :email: builder.EmailAddress(@grantee)
35
+ when :all_s3: builder.URI(group_uri_from_group_type(:all_s3))
36
+ when :public: builder.URI(group_uri_from_group_type(:public))
37
+ else
38
+ end
39
+ end
40
+ builder.Permission(@permission.to_s.upcase)
41
+ end
42
+ end
43
+
44
+ private
45
+
46
+ # permission can either be the String provided by S3
47
+ # or a symbol (see the PERMISSIONS array for allowed values)
48
+ def parse_permission(permission)
49
+ if permission.is_a?(String)
50
+ permission.downcase.to_sym
51
+ else
52
+ permission
53
+ end
54
+ end
55
+
56
+ def parse_type(grantee)
57
+ if grantee.is_a?(Hash)
58
+ grantee[:type]
59
+ else # Assume it's a REXML::Doc object
60
+ type = grantee.attributes['xsi:type']
61
+ case type
62
+ when 'CanonicalUser': :canonical
63
+ when 'AmazonCustomerByEmail': :email
64
+ when 'Group'
65
+ group_uri = grantee.elements['URI'].text
66
+ group_type_from_group_uri(group_uri)
67
+ else
68
+ raise BadGrantTypeError
69
+ end
70
+ end
71
+ end
72
+
73
+ def parse_grantee(grantee)
74
+ if grantee.is_a?(Hash)
75
+ if [:canonical, :email].include?(@type)
76
+ grantee[:grantee]
77
+ else
78
+ @type
79
+ end
80
+ else # it's a REXML::Doc object
81
+ case @type
82
+ when :canonical
83
+ grantee.elements['ID'].text
84
+ when :email
85
+ grantee.elements['EmailAddress'].text
86
+ when :all_s3: :all_s3
87
+ when :public: :public
88
+ else
89
+ nil
90
+ end
91
+ end
92
+ end
93
+
94
+ def group_type_from_group_uri(group_uri)
95
+ GROUP_URIS[group_uri]
96
+ end
97
+
98
+ def group_uri_from_group_type(group_type)
99
+ GROUP_URIS.invert[group_type]
100
+ end
101
+
102
+ end
103
+ end
@@ -0,0 +1,51 @@
1
+ module S3Lib
2
+ class Grant
3
+ attr_reader :acl, :grantee, :type, :permission
4
+ GRANT_TYPES = {:canonical => 'CanonicalUser',
5
+ :email => 'AmazonCustomerByEmail',
6
+ :all_s3 => 'Group',
7
+ :public => 'Group'}
8
+ GROUP_URIS = {'http://acs.amazonaws.com/groups/global/AuthenticatedUsers' => :all_s3,
9
+ 'http://acs.amazonaws.com/groups/global/AllUsers' => :public}
10
+ PERMISSIONS = [:read, :write, :read_acl, :write_acl, :full_control]
11
+ NAMESPACE_URI = 'http://www.w3.org/2001/XMLSchema-instance'
12
+
13
+ # Create a new grant.
14
+ # permission is one of the PERMISSIONS defined above
15
+ # grantee is the REXML::Document Grantee object returned by S3
16
+ def initialize(permission, grantee)
17
+ @permission = permission.downcase.to_sym
18
+ @type = parse_type(grantee)
19
+ @grantee = parse_grantee(grantee)
20
+ end
21
+
22
+ private
23
+
24
+ def parse_type(grantee)
25
+ type = grantee.attributes['xsi:type']
26
+ case type
27
+ when 'CanonicalUser': :canonical
28
+ when 'AmazonCustomerByEmail': :email
29
+ when 'Group'
30
+ group_uri = grantee.elements['URI'].text
31
+ GROUP_URIS[group_uri]
32
+ else
33
+ raise BadGrantTypeError
34
+ end
35
+ end
36
+
37
+ def parse_grantee(grantee)
38
+ case @type
39
+ when :canonical
40
+ grantee.elements['ID'].text
41
+ when :email
42
+ grantee.elements['EmailAddress'].text
43
+ when :all_s3: :all_s3
44
+ when :public: :public
45
+ else
46
+ nil
47
+ end
48
+ end
49
+
50
+ end
51
+ end
data/lib/object.rb ADDED
@@ -0,0 +1,144 @@
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
+ # This is just an alias for S3Object.new
12
+ def self.find(bucket, key, options = {})
13
+ S3Object.new(bucket, key, options)
14
+ end
15
+
16
+ def self.create(bucket, key, value = "", options = {})
17
+ # translate from :access to 'x-amz-acl'
18
+ options['x-amz-acl'] = options.delete(:access) if options[:access]
19
+ options['content-type'] ||= DEFAULT_CONTENT_TYPE
20
+ options[:body] = value || ""
21
+
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 refresh_acl
87
+ get_acl
88
+ end
89
+
90
+ def content_type
91
+ metadata["content-type"]
92
+ end
93
+
94
+ # strip off the leading and trailing double-quotes
95
+ def etag
96
+ metadata["etag"].sub(/\A\"/,'').sub(/\"\Z/, '')
97
+ end
98
+
99
+ def length
100
+ metadata["content-length"].to_i
101
+ end
102
+
103
+ def acl(params = {})
104
+ refresh_acl if params[:refresh]
105
+ @acl || get_acl
106
+ end
107
+
108
+ private
109
+
110
+ def self.object_request(verb, url, options = {})
111
+ begin
112
+ options.delete(:lazy_load)
113
+ response = S3Lib.request(verb, url, options)
114
+ rescue S3Lib::S3ResponseError => error
115
+ case error.amazon_error_type
116
+ when 'NoSuchBucket': raise S3Lib::BucketNotFoundError.new("The bucket '#{bucket}' does not exist.", error.io, error.s3requester)
117
+ when 'NotSignedUp': raise S3Lib::NotYourBucketError.new("The bucket '#{bucket}' is owned by somebody else", error.io, error.s3requester)
118
+ when 'AccessDenied': raise S3Lib::NotYourBucketError.new("The bucket '#{bucket}' is owned by someone else.", error.io, error.s3requester)
119
+ 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)
120
+ else # Re-raise the error if it's not one of the above
121
+ raise
122
+ end
123
+ end
124
+ response
125
+ end
126
+
127
+ def get_metadata
128
+ request = S3Object.object_request(:head, url, @options)
129
+ @metadata = request.meta
130
+ end
131
+
132
+ def get_value
133
+ request = S3Object.object_request(:get, url, @options)
134
+ @metadata = request.meta
135
+ @value = request.read
136
+ end
137
+
138
+ def get_acl
139
+ @acl = Acl.new(self)
140
+ end
141
+
142
+ end
143
+
144
+ end
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require File.join(File.dirname(__FILE__), 'object')
4
+ require 'rexml/document'
5
+ require 'pp'
6
+
7
+ bucket_request = S3Lib.request(:get, 'spatten_syncdemo')
8
+ bucket_doc = REXML::Document.new(bucket_request.read)
9
+ first_object = REXML::XPath.match(bucket_doc, "//Contents").first
10
+
11
+ # puts first_object
12
+
13
+ puts "from bucket xml:"
14
+ pp first_object.to_hash
15
+
16
+ puts "from get on object:"
17
+ object = S3Lib.request(:get, 'spatten_syncdemo/flowers.jpg')
18
+ pp object.meta