s33r 0.4.1 → 0.4.2

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