s3lib 0.1.0

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