s33r 0.4.1 → 0.4.2

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 (75) hide show
  1. data/examples/cli/simple.rb +17 -0
  2. data/examples/fores33r/app/controllers/browser_controller.rb +32 -8
  3. data/examples/fores33r/app/views/browser/_create_bucket.rhtml +6 -0
  4. data/examples/fores33r/app/views/browser/_upload.rhtml +5 -0
  5. data/examples/fores33r/app/views/browser/index.rhtml +1 -8
  6. data/examples/fores33r/app/views/browser/plain_bucket.rhtml +3 -0
  7. data/examples/fores33r/app/views/browser/s3_index.rhtml +9 -0
  8. data/examples/fores33r/app/views/browser/show_bucket.rhtml +6 -8
  9. data/examples/fores33r/app/views/layouts/application.rhtml +2 -0
  10. data/examples/fores33r/app/views/layouts/s3_layout.rhtml +14 -0
  11. data/examples/fores33r/config/environment.rb +1 -1
  12. data/examples/fores33r/config/routes.rb +2 -0
  13. data/examples/fores33r/public/stylesheets/core.css +2 -2
  14. data/html/classes/Net/HTTPResponse.html +3 -0
  15. data/html/classes/S33r.html +127 -116
  16. data/html/classes/S33r/BucketListing.html +119 -94
  17. data/html/classes/S33r/Client.html +602 -536
  18. data/html/classes/S33r/LoggingResource.html +3 -3
  19. data/html/classes/S33r/NamedBucket.html +235 -191
  20. data/html/classes/S33r/OrderlyXmlMarkup.html +7 -7
  21. data/html/classes/S33r/S33rException.html +1 -0
  22. data/html/classes/S33r/S33rException/TryingToPutEmptyResource.html +117 -0
  23. data/html/classes/S33r/S3ACL/ACLDoc.html +94 -94
  24. data/html/classes/S33r/S3ACL/AmazonCustomer.html +5 -5
  25. data/html/classes/S33r/S3ACL/CanonicalUser.html +12 -12
  26. data/html/classes/S33r/S3ACL/Grant.html +64 -64
  27. data/html/classes/S33r/S3ACL/Grantee.html +36 -36
  28. data/html/classes/S33r/S3ACL/Group.html +8 -8
  29. data/html/classes/S33r/S3Object.html +387 -79
  30. data/html/classes/XML.html +15 -15
  31. data/html/created.rid +1 -1
  32. data/html/files/CHANGELOG.html +7 -1
  33. data/html/files/MIT-LICENSE.html +1 -1
  34. data/html/files/README_txt.html +3 -7
  35. data/html/files/lib/s33r/bucket_listing_rb.html +1 -9
  36. data/html/files/lib/s33r/builder_rb.html +1 -1
  37. data/html/files/lib/s33r/client_rb.html +1 -1
  38. data/html/files/lib/s33r/core_rb.html +1 -2
  39. data/html/files/lib/s33r/libxml_extensions_rb.html +1 -7
  40. data/html/files/lib/s33r/libxml_loader_rb.html +109 -0
  41. data/html/files/lib/s33r/logging_rb.html +1 -2
  42. data/html/files/lib/s33r/mimetypes_rb.html +1 -1
  43. data/html/files/lib/s33r/named_bucket_rb.html +1 -1
  44. data/html/files/lib/s33r/s33r_exception_rb.html +1 -1
  45. data/html/files/lib/s33r/s33r_http_rb.html +1 -1
  46. data/html/files/lib/s33r/s3_acl_rb.html +1 -2
  47. data/html/files/lib/s33r/s3_obj_rb.html +108 -0
  48. data/html/files/lib/s33r/sync_rb.html +1 -1
  49. data/html/files/lib/s33r_rb.html +1 -1
  50. data/html/fr_class_index.html +1 -0
  51. data/html/fr_file_index.html +2 -0
  52. data/html/fr_method_index.html +80 -71
  53. data/lib/s33r/bucket_listing.rb +30 -55
  54. data/lib/s33r/client.rb +70 -28
  55. data/lib/s33r/core.rb +9 -4
  56. data/lib/s33r/libxml_extensions.rb +2 -0
  57. data/lib/s33r/libxml_loader.rb +6 -0
  58. data/lib/s33r/logging.rb +3 -3
  59. data/lib/s33r/named_bucket.rb +33 -15
  60. data/lib/s33r/s33r_exception.rb +4 -0
  61. data/lib/s33r/s33r_http.rb +1 -1
  62. data/lib/s33r/s3_acl.rb +3 -2
  63. data/lib/s33r/s3_obj.rb +186 -0
  64. data/test/cases/spec_bucket_listing.rb +9 -33
  65. data/test/cases/spec_s3_object.rb +35 -0
  66. data/test/files/suspect_bucket_listing.xml +19 -0
  67. metadata +94 -89
  68. data/examples/fores33r/log/development.log +0 -5960
  69. data/examples/fores33r/log/production.log +0 -0
  70. data/examples/fores33r/log/server.log +0 -0
  71. data/examples/fores33r/log/test.log +0 -0
  72. data/examples/fores33r/tmp/sessions/ruby_sess.2ea325f604aa5fb9 +0 -0
  73. data/examples/fores33r/tmp/sessions/ruby_sess.39d37e054d21d545 +0 -0
  74. data/examples/fores33r/tmp/sessions/ruby_sess.acf71fc73aa74983 +0 -0
  75. data/examples/fores33r/tmp/sessions/ruby_sess.c1697b7d6670f3cd +0 -0
@@ -3,8 +3,8 @@ require 'time'
3
3
  require 'net/http'
4
4
  require 'net/https'
5
5
  require 'openssl'
6
- require 'xml/libxml'
7
6
  require 'parsedate'
7
+ require File.join(File.dirname(__FILE__), 'libxml_loader')
8
8
 
9
9
  # Module to handle S3 operations which don't require an internet connection,
10
10
  # i.e. data validation and request-building operations;
@@ -157,12 +157,17 @@ module S33r
157
157
  headers
158
158
  end
159
159
 
160
- # Add metadata headers, correctly prefixing them first.
160
+ # Add metadata headers, correctly prefixing them first,
161
+ # e.g. you might do metadata_headers({}, {'myname' => 'elliot', 'myage' => 36})
162
+ # to add two headers to the request:
163
+ #
164
+ # x-amz-meta-myname: elliot
165
+ # x-amz-meta-myage: 36
161
166
  #
162
167
  # Returns headers with the metadata headers appended.
163
168
  def metadata_headers(headers, metadata={})
164
169
  unless metadata.empty?
165
- metadata.each { |key, value| headers[METADATA_PREFIX + key] = value }
170
+ metadata.each { |key, value| headers[METADATA_PREFIX + key] = value.to_s }
166
171
  end
167
172
  headers
168
173
  end
@@ -285,7 +290,7 @@ module S33r
285
290
  # Remove the namespace declaration from S3 XML response bodies (libxml
286
291
  # isn't fond of it).
287
292
  def S33r.remove_namespace(xml_in)
288
- namespace = RESPONSE_NAMESPACE_URI.gsub('/', '\/')
293
+ namespace = S33r::RESPONSE_NAMESPACE_URI.gsub('/', '\/')
289
294
  xml_in.gsub(/ xmlns="#{namespace}"/, '')
290
295
  end
291
296
  end
@@ -1,3 +1,5 @@
1
+ require File.join(File.dirname(__FILE__), 'libxml_loader')
2
+
1
3
  # Convenience methods for libxml classes.
2
4
  module XML
3
5
  # Find first element matching +xpath+ and return its content
@@ -0,0 +1,6 @@
1
+ begin
2
+ require 'xml/libxml'
3
+ rescue LoadError
4
+ require 'rubygems'
5
+ require_gem 'libxml-ruby'
6
+ end
@@ -1,13 +1,13 @@
1
1
  require 'rubygems'
2
- require 'xml/libxml'
3
2
  require_gem 'builder'
3
+ require File.join(File.dirname(__FILE__), 'libxml_loader')
4
4
 
5
5
  module S33r
6
6
  # For manipulating logging directives on resources
7
7
  # (see http://docs.amazonwebservices.com/AmazonS3/2006-03-01/LoggingHowTo.html).
8
8
  #
9
- # Calling LoggingResource.new (no arguments) will generate a blank instance
10
- # which can be used to remove logging from a resource.
9
+ # Creating a LoggingResource instance using new and no arguments will generate a "blank" instance;
10
+ # this can be put to the ?logging URL for a resource to remove logging from it.
11
11
  class LoggingResource
12
12
  attr_reader :log_target, :log_prefix
13
13
 
@@ -2,6 +2,8 @@
2
2
  #-- specify a prefix and/or delimiter
3
3
 
4
4
  require File.join(File.dirname(__FILE__), 'client')
5
+ require File.join(File.dirname(__FILE__), 's3_obj')
6
+ require File.join(File.dirname(__FILE__), 's33r_exception')
5
7
 
6
8
  module S33r
7
9
  # Wraps the S33r::Client class to make it more convenient for use with a single bucket.
@@ -33,10 +35,7 @@ module S33r
33
35
  a :default_bucket option"
34
36
  end
35
37
 
36
- # holds a BucketListing instance
37
- @listing = nil
38
-
39
- # all content should be created as public-read
38
+ # all content inside the bucket should be created as public-read
40
39
  @public_contents = (true == options[:public_contents])
41
40
  @client_headers.merge!(canned_acl_header('public-read')) if @public_contents
42
41
 
@@ -58,14 +57,31 @@ module S33r
58
57
  @strict
59
58
  end
60
59
 
61
- # Get a single object from a bucket as a blob.
62
- def [](key)
63
- get_resource(@name, key).body
60
+ # Get a single object from a bucket as an S3Object.
61
+ #
62
+ # To get a bare object (with no content):
63
+ #
64
+ # bucket['key']
65
+ #
66
+ # To get the object and load its content:
67
+ #
68
+ # bucket['key', :load]
69
+ def [](key, eager=false)
70
+ obj = listing.contents[key]
71
+ obj.named_bucket = self
72
+ obj.load if :load == eager
73
+ obj
74
+ end
75
+
76
+ # Get a raw response for a key inside the bucket.
77
+ def get_raw(key, headers={})
78
+ get_resource(@name, key, headers)
64
79
  end
65
80
 
66
81
  # Get a BucketListing instance for the content of this bucket.
82
+ # Uses the Client.list_bucket method to get the listing.
67
83
  def listing
68
- list_bucket(@name)
84
+ list_bucket(@name)[1]
69
85
  end
70
86
 
71
87
  # Does this bucket exist?
@@ -84,9 +100,12 @@ module S33r
84
100
  listing.pretty
85
101
  end
86
102
 
87
- # List content of the bucket, and attach each item to this bucket as it is yielded.
88
- def each_item
89
- listing.contents.each_value { |item| item.named_bucket = self; yield item }
103
+ # List content of the bucket, and attach each item to this NamedBucket
104
+ # instance as it is yielded (to enable easier manipulation directly from the S3Object).
105
+ # Note that the objects are incomplete, as the data associated with them has not been
106
+ # "got" from S3 yet.
107
+ def each_object
108
+ listing.contents.each_value { |obj| obj.named_bucket = self; yield obj }
90
109
  end
91
110
 
92
111
  # Does the given key exist in the bucket?
@@ -114,9 +133,8 @@ module S33r
114
133
  # NB S3 doesn't discriminate between successfully deleting a key
115
134
  # and trying to delete a non-existent key (both return a 204).
116
135
  # If you want to test for existence first, use key_exists?.
117
- def delete_resource(resource_key, headers={})
118
- super(@name, resource_key, headers)
119
- listing
136
+ def delete(key, headers={})
137
+ delete_resource(@name, key, headers)
120
138
  end
121
139
 
122
140
  # Generate an authenticated URL (see http://docs.amazonwebservices.com/AmazonS3/2006-03-01/)
@@ -127,4 +145,4 @@ module S33r
127
145
  super(@aws_access_key, @aws_secret_access_key, @name, resource_key, expires)
128
146
  end
129
147
  end
130
- end
148
+ end
@@ -41,6 +41,10 @@ module S33r
41
41
  # (i.e. if LogDelivery permissions not set - see S33r::S3ACL::ACLDoc.add_log_target_grants)
42
42
  class BucketNotLogTargetable < Exception
43
43
  end
44
+
45
+ # Raised if you try to do a put with empty body
46
+ class TryingToPutEmptyResource < Exception
47
+ end
44
48
 
45
49
  end
46
50
  end
@@ -2,7 +2,7 @@ require 'net/http'
2
2
  require File.join(File.dirname(__FILE__), 's33r_exception')
3
3
 
4
4
  # Addtions to the Net::HTTP classes
5
-
5
+ #
6
6
  # Adds some convenience functions for checking response status.
7
7
  class Net::HTTPResponse
8
8
  attr_accessor :success
@@ -1,7 +1,8 @@
1
1
  require 'rubygems'
2
- require 'xml/libxml'
3
2
  require_gem 'builder'
4
- require File.join(File.dirname(__FILE__), 's33r_exception')
3
+ base = File.dirname(__FILE__)
4
+ require File.join(base, 'libxml_loader')
5
+ require File.join(base, 's33r_exception')
5
6
 
6
7
  module S33r
7
8
  # S3 ACL handling.
@@ -0,0 +1,186 @@
1
+ require 'date'
2
+
3
+ # Representation of an object stored in a bucket.
4
+ module S33r
5
+ class S3Object
6
+ attr_accessor :key, :last_modified, :etag, :size, :owner, :storage_class, :value, :named_bucket,
7
+ :content_type, :mime_type
8
+
9
+ # Metadata set by x-amz-meta- style headers. Note that the bit after x-amz-meta-
10
+ # is stored for each key, rather than the full key.
11
+ attr_accessor :meta
12
+
13
+ def initialize(key, metadata={}, amz_meta={}, value=nil)
14
+ @key = key
15
+ @meta = amz_meta
16
+ @value = value
17
+ set_properties(metadata) unless metadata.empty?
18
+ end
19
+
20
+ # Set the properties of the object from some metadata name-value pairs.
21
+ #
22
+ # +metadata+ is a hash of properties and their values, used to set the
23
+ # corresponding properties on the object.
24
+ #
25
+ # +value+ is the data associated with the object on S3.
26
+ def set_properties(metadata)
27
+ # required properties
28
+ @etag = metadata[:etag].gsub("\"", "") if metadata[:etag]
29
+ @last_modified = DateTime.parse(metadata[:last_modified]) if metadata[:last_modified]
30
+ @size = metadata[:size].to_i if metadata[:size]
31
+
32
+ # only set if creating object from XML (not available otherwise)
33
+ @owner = metadata[:owner]
34
+
35
+ # only set if creating object from HTTP response
36
+ @content_type = metadata[:content_type]
37
+ end
38
+
39
+ # To create an object which reads the content in from a file;
40
+ # this is not very efficient - it's actually better to use NamedBucket.put_file,
41
+ # as this will stream out of a file to S3, rather than load the file in
42
+ # memory first.
43
+ def self.from_file(key, filename)
44
+ mime_type = guess_mime_type(filename)
45
+ content_type = mime_type.simplified
46
+ value = File.open(filename).read
47
+ self.new(key, { :content_type => content_type }, {}, value)
48
+ end
49
+
50
+ # Set properties of the object from an XML string.
51
+ #
52
+ # +xml_str+ should be a string representing a full XML document,
53
+ # containing a <Contents> element as its root element.
54
+ def self.from_xml_string(xml_str)
55
+ self.from_xml_node(XML.get_xml_doc(xml_str))
56
+ end
57
+
58
+ # Create a new instance from an XML document.
59
+ def self.from_xml_node(doc)
60
+ metadata = self.parse_xml_node(doc)
61
+ self.new(metadata[:key], metadata)
62
+ end
63
+
64
+ # Get properties of the object from an XML document, e.g. as returned in a bucket listing.
65
+ #
66
+ # +doc+: XML::Document instance to parse to get properties for this object.
67
+ #
68
+ # Returns the metadata relating to the object, as stored on S3.
69
+ #-- TODO: include amz-meta elements
70
+ def self.parse_xml_node(doc)
71
+ metadata = {}
72
+ metadata[:key] = doc.xget('Key')
73
+ metadata[:last_modified] = doc.xget('LastModified')
74
+ metadata[:etag] = doc.xget('ETag')
75
+ metadata[:size] = doc.xget('Size')
76
+
77
+ # Build representation of the owner.
78
+ user_xml_doc = doc.find('Owner').to_a.first
79
+ metadata[:owner] = S3ACL::CanonicalUser.from_xml(user_xml_doc)
80
+
81
+ metadata
82
+ end
83
+
84
+ # Create a new instance from a HTTP response.
85
+ # This is useful if you do a GET for a resource key and
86
+ # want to convert the response into an object; NB the response
87
+ # doesn't necessarily contain all the metadata you might want - you need to
88
+ # do a HEAD for that.
89
+ #
90
+ # +key+ is the key for the resource (not part of the response).
91
+ #
92
+ # Note that if the resp returns nil, a blank object is created.
93
+ def self.from_response(key, resp)
94
+ result = self.parse_response(resp)
95
+ if result
96
+ metadata, amz_meta, value = result
97
+ else
98
+ metadata = {}
99
+ amz_meta = {}
100
+ value = nil
101
+ end
102
+ self.new(key, metadata, amz_meta, value)
103
+ end
104
+
105
+ # Parse the response returned by GET on a resource key
106
+ # within a bucket.
107
+ #
108
+ # +resp+ is a Net::HTTPResponse instance.
109
+ #
110
+ # Returns an array [+metadata+, +response.body+]; or nil if the object
111
+ # doesn't exist.
112
+ def self.parse_response(resp)
113
+ resp_headers = resp.to_hash
114
+
115
+ # If there's no etag, there's no content in the resource.
116
+ if resp_headers['etag']
117
+ metadata = {}
118
+ metadata[:last_modified] = resp_headers['last-modified'][0]
119
+ metadata[:etag] = resp_headers['etag'][0]
120
+ metadata[:size] = resp_headers['content-length'][0]
121
+ metadata[:content_type] = resp_headers['content-type'][0]
122
+
123
+ # x-amz-meta- response headers.
124
+ interesting_header = Regexp.new(METADATA_PREFIX)
125
+ amz_meta = {}
126
+ resp.each_header do |key, value|
127
+ amz_meta[key.gsub(interesting_header, '')] = value if interesting_header =~ key
128
+ end
129
+
130
+ # The actual content of the S3 object.
131
+ value = resp.body
132
+
133
+ [metadata, amz_meta, value]
134
+ else
135
+ nil
136
+ end
137
+ end
138
+
139
+ # Load content into this object from S3; will perform
140
+ # an HTTP request to "refresh" the object (providing the object
141
+ # has an association with a bucket it can use for piggybacking).
142
+ #
143
+ # Returns false if value cannot be retrieved.
144
+ def load
145
+ if @named_bucket and @named_bucket.key_exists?(@key)
146
+ resp = @named_bucket.get_raw(@key)
147
+ if resp.ok?
148
+ @value = resp.body
149
+ @content_type = resp.to_hash['content-type']
150
+ return true
151
+ else
152
+ return false
153
+ end
154
+ else
155
+ return false
156
+ end
157
+ end
158
+
159
+ # Remove this object from its associated NamedBucket.
160
+ #
161
+ # Returns false if this object is not associated with a bucket;
162
+ # otherwise returns the response from S3.
163
+ def delete
164
+ if @named_bucket.nil?
165
+ return false
166
+ else
167
+ @named_bucket.delete_resource(@key)
168
+ end
169
+ end
170
+
171
+ # Save this object back into its bucket.
172
+ #
173
+ # Only works if the object has an associated NamedBucket;
174
+ # returns false if it doesn't.
175
+ def save
176
+ if @named_bucket.nil?
177
+ return false
178
+ else
179
+ headers = {}
180
+ headers["Content-Type"] = @content_type || ''
181
+ headers = metadata_headers(headers, meta)
182
+ @named_bucket.put_stream(@value, @key, headers)
183
+ end
184
+ end
185
+ end
186
+ end
@@ -12,6 +12,8 @@ context 'S33r bucket listing' do
12
12
  @with_empty_bucket_listing_xml = File.open(xml_file3) { |f| f.read }
13
13
  xml_file4 = File.join(base, '../files/bucket_listing_broken.xml')
14
14
  @with_broken_bucket_listing_xml = File.open(xml_file4) { |f| f.read }
15
+ xml_file5 = File.join(base, '../files/suspect_bucket_listing.xml')
16
+ @with_suspect_bl_xml = File.open(xml_file5) { |f| f.read }
15
17
 
16
18
  @bucket_listing = BucketListing.new(@with_bucket_listing_xml)
17
19
  @bucket_properties = %w(name prefix marker max_keys is_truncated)
@@ -85,38 +87,12 @@ context 'S33r bucket listing' do
85
87
  specify 'should provide easy access to <CommonPrefixes> elements as a hash' do
86
88
  todo
87
89
  end
88
- end
89
-
90
- context 'S3 object' do
91
- setup do
92
- @s3_object_xml = File.open(File.join(base, '../files/s3_object.xml')).read
93
- @s3obj = S3Object.new
94
- @s3obj.set_from_xml_string(@s3_object_xml)
95
- end
96
-
97
- specify 'can be initialised from XML fragment with correct data types' do
98
- @s3obj.key.should.equal '/home/ell/dir1/four.txt'
99
- d = @s3obj.last_modified
100
- [d.year, d.month, d.day, d.hour, d.min, d.sec].should.equal [2006, 8, 19, 22, 53, 29]
101
- @s3obj.etag.should.equal '24ce59274b89287b3960c184153ac24b'
102
- @s3obj.size.should.equal 14
103
- end
104
-
105
- specify 'should treat the owner as an object in his/her own right' do
106
- [@s3obj.owner.user_id, @s3obj.owner.display_name].should.equal \
107
- ['56efddfead5aa65da942f156fb2b294f44d78fd932d701331edc5fba19620fd4', 'elliotsmith3']
108
- @s3obj.owner.should_be_instance_of S3ACL::CanonicalUser
109
- end
110
-
111
- specify 'can be associated with a NamedBucket' do
112
- todo
113
- end
114
-
115
- specify 'can be saved by proxing through the NamedBucket it is associated with' do
116
- todo
117
- end
118
-
119
- specify 'cannot be saved unless associated with a NamedBucket' do
120
- todo
90
+
91
+ # attempting to fix ListBucketResult errors reported by Alex Payne
92
+ specify 'should handle suspect bucket listing' do
93
+ puts @with_suspect_bl_xml
94
+ bl = BucketListing.new(@with_suspect_bl_xml)
95
+ bl.contents.size.should_be 1
96
+ bl.contents.keys.should.include 'orly.jpg'
121
97
  end
122
98
  end
@@ -0,0 +1,35 @@
1
+ base = File.dirname(__FILE__)
2
+ require base + '/../test_setup'
3
+
4
+ context 'S3 object' do
5
+ setup do
6
+ @s3_object_xml = File.open(File.join(base, '../files/s3_object.xml')).read
7
+ @s3obj = S3Object.from_xml_string(@s3_object_xml)
8
+ end
9
+
10
+ specify 'can be initialised from XML fragment with correct data types' do
11
+ @s3obj.key.should.equal '/home/ell/dir1/four.txt'
12
+ d = @s3obj.last_modified
13
+ [d.year, d.month, d.day, d.hour, d.min, d.sec].should.equal [2006, 8, 19, 22, 53, 29]
14
+ @s3obj.etag.should.equal '24ce59274b89287b3960c184153ac24b'
15
+ @s3obj.size.should.equal 14
16
+ end
17
+
18
+ specify 'should treat the owner as an object in his/her own right' do
19
+ [@s3obj.owner.user_id, @s3obj.owner.display_name].should.equal \
20
+ ['56efddfead5aa65da942f156fb2b294f44d78fd932d701331edc5fba19620fd4', 'elliotsmith3']
21
+ @s3obj.owner.should_be_instance_of S3ACL::CanonicalUser
22
+ end
23
+
24
+ specify 'can be associated with a NamedBucket' do
25
+ todo
26
+ end
27
+
28
+ specify 'can be saved by proxing through the NamedBucket it is associated with' do
29
+ todo
30
+ end
31
+
32
+ specify 'cannot be saved unless associated with a NamedBucket' do
33
+ todo
34
+ end
35
+ end