s33r 0.4.2 → 0.5

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 (114) hide show
  1. data/examples/cli/instant_download_server.rb +88 -0
  2. data/examples/cli/s3cli.rb +31 -52
  3. data/examples/cli/simple.rb +16 -6
  4. data/examples/fores33r/app/controllers/browser_controller.rb +12 -10
  5. data/examples/fores33r/app/helpers/application_helper.rb +2 -1
  6. data/examples/fores33r/app/views/browser/_upload.rhtml +1 -1
  7. data/examples/fores33r/app/views/browser/index.rhtml +4 -4
  8. data/examples/fores33r/config/environment.rb +5 -3
  9. data/examples/fores33r/log/development.log +2259 -0
  10. data/examples/fores33r/log/mongrel.log +59 -0
  11. data/examples/s3.yaml +2 -6
  12. data/lib/s33r/bucket.rb +103 -0
  13. data/lib/s33r/bucket_listing.rb +33 -76
  14. data/lib/s33r/client.rb +305 -446
  15. data/lib/s33r/networking.rb +197 -0
  16. data/lib/s33r/s33r_exception.rb +29 -18
  17. data/lib/s33r/s33r_http.rb +36 -18
  18. data/lib/s33r/s3_acl.rb +32 -52
  19. data/lib/s33r/s3_logging.rb +117 -0
  20. data/lib/s33r/s3_obj.rb +124 -69
  21. data/lib/s33r/utility.rb +447 -0
  22. data/test/cases/spec_acl.rb +10 -40
  23. data/test/cases/spec_bucket_listing.rb +12 -32
  24. data/test/cases/spec_logging.rb +47 -0
  25. data/test/cases/spec_networking.rb +11 -0
  26. data/test/cases/spec_s3_object.rb +44 -5
  27. data/test/cases/spec_utility.rb +264 -0
  28. data/test/files/acl.xml +0 -6
  29. data/test/files/config.yaml +5 -0
  30. data/test/files/logging_status_disabled.xml +3 -0
  31. data/test/files/logging_status_enabled.xml +7 -0
  32. data/test/test_setup.rb +7 -2
  33. metadata +16 -94
  34. data/examples/cli/acl_x.rb +0 -41
  35. data/examples/cli/logging_x.rb +0 -20
  36. data/examples/fores33r/README +0 -183
  37. data/html/classes/MIME.html +0 -120
  38. data/html/classes/MIME/InvalidContentType.html +0 -119
  39. data/html/classes/MIME/Type.html +0 -1173
  40. data/html/classes/MIME/Types.html +0 -566
  41. data/html/classes/Net.html +0 -108
  42. data/html/classes/Net/HTTPGenericRequest.html +0 -233
  43. data/html/classes/Net/HTTPResponse.html +0 -271
  44. data/html/classes/S33r.html +0 -986
  45. data/html/classes/S33r/BucketListing.html +0 -434
  46. data/html/classes/S33r/Client.html +0 -1575
  47. data/html/classes/S33r/LoggingResource.html +0 -222
  48. data/html/classes/S33r/NamedBucket.html +0 -693
  49. data/html/classes/S33r/OrderlyXmlMarkup.html +0 -165
  50. data/html/classes/S33r/S33rException.html +0 -124
  51. data/html/classes/S33r/S33rException/BucketListingMaxKeysError.html +0 -111
  52. data/html/classes/S33r/S33rException/BucketNotLogTargetable.html +0 -119
  53. data/html/classes/S33r/S33rException/InvalidBucketListing.html +0 -111
  54. data/html/classes/S33r/S33rException/InvalidPermission.html +0 -111
  55. data/html/classes/S33r/S33rException/InvalidS3GroupType.html +0 -111
  56. data/html/classes/S33r/S33rException/MalformedBucketName.html +0 -111
  57. data/html/classes/S33r/S33rException/MethodNotAvailable.html +0 -111
  58. data/html/classes/S33r/S33rException/MissingBucketName.html +0 -111
  59. data/html/classes/S33r/S33rException/MissingRequiredHeaders.html +0 -111
  60. data/html/classes/S33r/S33rException/MissingResource.html +0 -111
  61. data/html/classes/S33r/S33rException/S3FallenOver.html +0 -111
  62. data/html/classes/S33r/S33rException/TryingToPutEmptyResource.html +0 -117
  63. data/html/classes/S33r/S33rException/UnsupportedCannedACL.html +0 -111
  64. data/html/classes/S33r/S33rException/UnsupportedHTTPMethod.html +0 -111
  65. data/html/classes/S33r/S3ACL.html +0 -125
  66. data/html/classes/S33r/S3ACL/ACLDoc.html +0 -521
  67. data/html/classes/S33r/S3ACL/AmazonCustomer.html +0 -168
  68. data/html/classes/S33r/S3ACL/CanonicalUser.html +0 -212
  69. data/html/classes/S33r/S3ACL/Grant.html +0 -403
  70. data/html/classes/S33r/S3ACL/Grantee.html +0 -239
  71. data/html/classes/S33r/S3ACL/Group.html +0 -178
  72. data/html/classes/S33r/S3Object.html +0 -618
  73. data/html/classes/S33r/Sync.html +0 -152
  74. data/html/classes/XML.html +0 -202
  75. data/html/classes/XML/Document.html +0 -125
  76. data/html/classes/XML/Node.html +0 -124
  77. data/html/created.rid +0 -1
  78. data/html/files/CHANGELOG.html +0 -107
  79. data/html/files/MIT-LICENSE.html +0 -129
  80. data/html/files/README_txt.html +0 -259
  81. data/html/files/lib/s33r/bucket_listing_rb.html +0 -101
  82. data/html/files/lib/s33r/builder_rb.html +0 -108
  83. data/html/files/lib/s33r/client_rb.html +0 -111
  84. data/html/files/lib/s33r/core_rb.html +0 -113
  85. data/html/files/lib/s33r/libxml_extensions_rb.html +0 -101
  86. data/html/files/lib/s33r/libxml_loader_rb.html +0 -109
  87. data/html/files/lib/s33r/logging_rb.html +0 -108
  88. data/html/files/lib/s33r/mimetypes_rb.html +0 -120
  89. data/html/files/lib/s33r/named_bucket_rb.html +0 -101
  90. data/html/files/lib/s33r/s33r_exception_rb.html +0 -101
  91. data/html/files/lib/s33r/s33r_http_rb.html +0 -108
  92. data/html/files/lib/s33r/s3_acl_rb.html +0 -108
  93. data/html/files/lib/s33r/s3_obj_rb.html +0 -108
  94. data/html/files/lib/s33r/sync_rb.html +0 -101
  95. data/html/files/lib/s33r_rb.html +0 -101
  96. data/html/fr_class_index.html +0 -66
  97. data/html/fr_file_index.html +0 -44
  98. data/html/fr_method_index.html +0 -183
  99. data/html/index.html +0 -24
  100. data/html/rdoc-style.css +0 -208
  101. data/lib/s33r/core.rb +0 -296
  102. data/lib/s33r/logging.rb +0 -43
  103. data/lib/s33r/named_bucket.rb +0 -148
  104. data/lib/s33r/sync.rb +0 -13
  105. data/test/cases/spec_all_buckets.rb +0 -28
  106. data/test/cases/spec_client.rb +0 -101
  107. data/test/cases/spec_core.rb +0 -128
  108. data/test/cases/spec_namedbucket.rb +0 -46
  109. data/test/cases/spec_sync.rb +0 -34
  110. data/test/files/all_buckets.xml +0 -21
  111. data/test/files/client_config.yml +0 -5
  112. data/test/files/namedbucket_config.yml +0 -8
  113. data/test/files/namedbucket_config2.yml +0 -8
  114. data/test/test_bucket_setup.rb +0 -41
@@ -0,0 +1,117 @@
1
+ require 'rubygems'
2
+ require_gem 'builder'
3
+ require File.join(File.dirname(__FILE__), 'libxml_loader')
4
+ require File.join(File.dirname(__FILE__), 's3_acl')
5
+
6
+ module S33r
7
+ module S3Logging
8
+
9
+ # For manipulating logging directives on resources
10
+ # (see http://docs.amazonwebservices.com/AmazonS3/2006-03-01/LoggingHowTo.html).
11
+ #
12
+ # Creating a LoggingResource instance using new and no arguments will generate a "blank" instance;
13
+ # this can be put to the ?logging URL for a resource to remove logging from it.
14
+ #
15
+ # To set a Bucket up for logging, create a LoggingResource with the correct log_target and
16
+ # log_prefix settings and put that to the ?logging URL for a bucket.
17
+ class LoggingResource
18
+ attr_reader :log_target, :log_prefix
19
+
20
+ # +log_target+ is the bucket to put logs into.
21
+ # +log_prefix+ is the prefix for log files put into the +log_target+ bucket.
22
+ def initialize(log_target=nil, log_prefix=nil)
23
+ @log_target = log_target
24
+ @log_prefix = log_prefix
25
+ end
26
+
27
+ # Generate a BucketLoggingStatus XML document for putting to the ?logging
28
+ # URL for a resource.
29
+ def to_xml
30
+ xml_str = ""
31
+ xml = Builder::XmlMarkup.new(:target => xml_str, :indent => 0)
32
+
33
+ xml.instruct!
34
+
35
+ # BucketLoggingStatus XML.
36
+ xml.BucketLoggingStatus({"xmlns" => RESPONSE_NAMESPACE_URI}) {
37
+ unless @log_target.nil? and @log_prefix.nil?
38
+ xml.LoggingEnabled {
39
+ xml.TargetBucket @log_target
40
+ xml.TargetPrefix @log_prefix
41
+ }
42
+ end
43
+ }
44
+
45
+ xml_str
46
+ end
47
+
48
+ # Convert XML from S3 response into a LoggingResource
49
+ #
50
+ #-- TODO: tests
51
+ def self.from_xml(logging_xml)
52
+ return nil if logging_xml.nil?
53
+
54
+ logging_xml = S33r.remove_namespace(logging_xml)
55
+ doc = XML.get_xml_doc(logging_xml)
56
+
57
+ log_target = doc.xget('//TargetBucket')
58
+ log_prefix = doc.xget('//TargetPrefix')
59
+
60
+ self.new(log_target, log_prefix)
61
+ end
62
+
63
+ end
64
+ end
65
+
66
+ # Extensions to the S3ACL module to cover logging.
67
+ module S3ACL
68
+ class Policy
69
+ # Does the ACL make the associated resource available as a log target?
70
+ def log_targetable?
71
+ log_target_grants = Grant.log_target_grants
72
+ log_target_grants.each { |g| return false if !grants.include?(g) }
73
+ return true
74
+ end
75
+
76
+ # Add permissions to an instances which give READ_ACL
77
+ # and WRITE permissions to the LogDelivery group. Used
78
+ # to enable a bucket as a logging destination.
79
+ #
80
+ # Returns true if grants added, false otherwise
81
+ # (if already a log target).
82
+ def add_log_target_grants
83
+ if log_targetable?
84
+ return false
85
+ else
86
+ Grant.log_target_grants.each { |g| add_grant(g) }
87
+ return true
88
+ end
89
+ end
90
+
91
+ # Remove log target ACLs from the document.
92
+ #
93
+ # Returns true if all log target grants were removed;
94
+ # false otherwise.
95
+ #
96
+ # NB even if this method returns false, that doesn't mean
97
+ # the bucket is still a log target. Use log_targetable? to check
98
+ # whether a bucket can be used as a log target.
99
+ def remove_log_target_grants
100
+ ok = true
101
+ Grant.log_target_grants.each { |g| ok = ok and remove_grant(g) }
102
+ ok
103
+ end
104
+ end
105
+
106
+ class Grant
107
+ # Generator for a grant which gives the LogDelivery group
108
+ # write and read_acl permissions on a bucket.
109
+ #
110
+ # Returns an array with the two required Grant instances.
111
+ def Grant.log_target_grants
112
+ log_delivery_group = Group.new(:log_delivery)
113
+ [Grant.new(log_delivery_group, :read_acl), Grant.new(log_delivery_group, :write)]
114
+ end
115
+ end
116
+ end
117
+ end
data/lib/s33r/s3_obj.rb CHANGED
@@ -1,64 +1,105 @@
1
1
  require 'date'
2
+ require File.join(File.dirname(__FILE__), 's3_acl')
3
+ require File.join(File.dirname(__FILE__), 'client')
2
4
 
3
5
  # Representation of an object stored in a bucket.
4
6
  module S33r
5
- class S3Object
6
- attr_accessor :key, :last_modified, :etag, :size, :owner, :storage_class, :value, :named_bucket,
7
- :content_type, :mime_type
7
+ class S3Object < Client
8
+ attr_accessor :key, :last_modified, :etag, :size, :owner, :storage_class, :value,
9
+ :content_type, :render_as_attachment
8
10
 
9
- # Metadata set by x-amz-meta- style headers. Note that the bit after x-amz-meta-
11
+ # Name of bucket this object is attached to.
12
+ attr_reader :bucket
13
+
14
+ # Metadata to set with x-amz-meta- style headers. Note that the bit after x-amz-meta-
10
15
  # is stored for each key, rather than the full key.
11
- attr_accessor :meta
16
+ attr_accessor :amz_meta
17
+ alias :meta :amz_meta
12
18
 
13
- def initialize(key, metadata={}, amz_meta={}, value=nil)
19
+ # +options+ can include:
20
+ # * <tt>:bucket => Bucket</tt>: Bucket this object is attached to.
21
+ # * <tt>:metadata => Hash</tt>: metadata to use in building the object.
22
+ # * <tt>:amz_meta => Hash</tt>: metadata specific to Amazon.
23
+ def initialize(key, value=nil, options={})
14
24
  @key = key
15
- @meta = amz_meta
25
+
16
26
  @value = value
27
+ @content_type = 'text/plain'
28
+ @render_as_attachment = false
29
+ @amz_meta = options[:amz_meta] || {}
30
+
31
+ set_bucket(options[:bucket])
32
+
33
+ metadata = options[:metadata] || {}
17
34
  set_properties(metadata) unless metadata.empty?
18
35
  end
19
36
 
37
+ # Set a bucket instance as the default bucket for this object.
38
+ def set_bucket(bucket_instance)
39
+ if bucket_instance
40
+ @bucket = bucket_instance
41
+ set_options(@bucket.settings)
42
+ extend(InBucket)
43
+ end
44
+ end
45
+ alias :bucket= :set_bucket
46
+
20
47
  # Set the properties of the object from some metadata name-value pairs.
21
48
  #
22
49
  # +metadata+ is a hash of properties and their values, used to set the
23
50
  # corresponding properties on the object.
24
- #
25
- # +value+ is the data associated with the object on S3.
26
51
  def set_properties(metadata)
27
52
  # required properties
28
53
  @etag = metadata[:etag].gsub("\"", "") if metadata[:etag]
29
54
  @last_modified = DateTime.parse(metadata[:last_modified]) if metadata[:last_modified]
30
55
  @size = metadata[:size].to_i if metadata[:size]
56
+ @render_as_attachment = metadata[:render_as_attachment] || false
31
57
 
32
58
  # only set if creating object from XML (not available otherwise)
33
- @owner = metadata[:owner]
59
+ @owner ||= metadata[:owner]
34
60
 
35
61
  # only set if creating object from HTTP response
36
- @content_type = metadata[:content_type]
62
+ @content_type = metadata[:content_type] if metadata[:content_type]
37
63
  end
38
64
 
39
65
  # 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)
66
+ # you can then save the object to its associated bucket (if you like).
67
+ def self.from_file(filename, options={})
44
68
  mime_type = guess_mime_type(filename)
45
69
  content_type = mime_type.simplified
46
70
  value = File.open(filename).read
47
- self.new(key, { :content_type => content_type }, {}, value)
71
+ key = options[:key] || filename
72
+
73
+ options.merge!(:metadata => {:content_type => content_type})
74
+ self.new(key, value, options)
75
+ end
76
+
77
+ # Create a new instance from a string.
78
+ def self.from_text(key, text, options={})
79
+ content_type = 'text/plain'
80
+
81
+ options.merge!(:metadata => {:content_type => content_type})
82
+ self.new(key, text, options)
48
83
  end
49
84
 
50
85
  # Set properties of the object from an XML string.
51
86
  #
52
87
  # +xml_str+ should be a string representing a full XML document,
53
88
  # containing a <Contents> element as its root element.
54
- def self.from_xml_string(xml_str)
89
+ def self.from_xml_string(xml_str, options={})
55
90
  self.from_xml_node(XML.get_xml_doc(xml_str))
56
91
  end
57
92
 
58
- # Create a new instance from an XML document.
59
- def self.from_xml_node(doc)
93
+ # Create a new instance from an XML document;
94
+ # N.B. this instance will have no value associated with it (yet).
95
+ # Call the load method to populate it.
96
+ #
97
+ # +options+ is passed to the constructor.
98
+ def self.from_xml_node(doc, options={})
60
99
  metadata = self.parse_xml_node(doc)
61
- self.new(metadata[:key], metadata)
100
+
101
+ options.merge!(:metadata => metadata)
102
+ self.new(metadata[:key], nil, options)
62
103
  end
63
104
 
64
105
  # Get properties of the object from an XML document, e.g. as returned in a bucket listing.
@@ -66,7 +107,6 @@ module S33r
66
107
  # +doc+: XML::Document instance to parse to get properties for this object.
67
108
  #
68
109
  # Returns the metadata relating to the object, as stored on S3.
69
- #-- TODO: include amz-meta elements
70
110
  def self.parse_xml_node(doc)
71
111
  metadata = {}
72
112
  metadata[:key] = doc.xget('Key')
@@ -88,18 +128,20 @@ module S33r
88
128
  # do a HEAD for that.
89
129
  #
90
130
  # +key+ is the key for the resource (not part of the response).
131
+ # +resp+ is a Net::HTTPResponse instance to parse.
132
+ # +options+ is passed through to the constructor (see initialize).
91
133
  #
92
134
  # Note that if the resp returns nil, a blank object is created.
93
- def self.from_response(key, resp)
135
+ def self.from_response(key, resp, options={})
94
136
  result = self.parse_response(resp)
95
137
  if result
96
138
  metadata, amz_meta, value = result
139
+ options.merge!(:metadata => metadata, :amz_meta => amz_meta)
97
140
  else
98
- metadata = {}
99
- amz_meta = {}
100
141
  value = nil
101
142
  end
102
- self.new(key, metadata, amz_meta, value)
143
+
144
+ self.new(key, value, options)
103
145
  end
104
146
 
105
147
  # Parse the response returned by GET on a resource key
@@ -120,67 +162,80 @@ module S33r
120
162
  metadata[:size] = resp_headers['content-length'][0]
121
163
  metadata[:content_type] = resp_headers['content-type'][0]
122
164
 
165
+ content_disposition = resp_headers['content-disposition']
166
+ if content_disposition
167
+ content_disposition = content_disposition[0]
168
+ if /^attachment/ =~ content_disposition
169
+ metadata[:render_as_attachment] = true
170
+ end
171
+ end
172
+
123
173
  # x-amz-meta- response headers.
124
174
  interesting_header = Regexp.new(METADATA_PREFIX)
125
- amz_meta = {}
175
+ new_amz_meta = {}
126
176
  resp.each_header do |key, value|
127
- amz_meta[key.gsub(interesting_header, '')] = value if interesting_header =~ key
177
+ new_amz_meta[key.gsub(interesting_header, '')] = value if interesting_header =~ key
128
178
  end
129
179
 
130
180
  # The actual content of the S3 object.
131
181
  value = resp.body
132
182
 
133
- [metadata, amz_meta, value]
183
+ [metadata, new_amz_meta, value]
134
184
  else
135
185
  nil
136
186
  end
137
187
  end
138
188
 
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
189
+ # Set metadata on the object.
190
+ def []=(key, value)
191
+ amz_meta[key] = value
157
192
  end
158
193
 
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
194
+ # Get metadata on the object.
195
+ def [](key)
196
+ amz_meta[key]
197
+ end
198
+ end
199
+
200
+ module InBucket
201
+ # Send requests using the bucket's options.
202
+ def request_defaults
203
+ bucket.request_defaults.merge(:key => key)
204
+ end
205
+
206
+ # Fetch an object's metadata and content from S3.
207
+ def fetch(options={})
208
+ resp = do_get(options)
209
+
210
+ metadata, amz_meta, data = S3Object.parse_response(resp)
211
+ @amz_meta = amz_meta
212
+ @value = data
213
+ set_properties(metadata)
214
+ true
169
215
  end
170
216
 
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
217
+ # Save an object to S3.
218
+ def save(options={})
219
+ bucket.put(self, options)
184
220
  end
221
+
222
+ # Delete the object from S3.
223
+ def delete(options={})
224
+ bucket.delete(key, options)
225
+ end
226
+
227
+ # Change the object's name on S3.
228
+ def rename(new_key, options={})
229
+ # Delete the object from S3.
230
+ bucket.delete(key, options)
231
+
232
+ # Set the new key.
233
+ self.key = new_key
234
+ options[:key] = new_key
235
+
236
+ save(options)
237
+ end
238
+ alias :mv :rename
239
+
185
240
  end
186
241
  end
@@ -0,0 +1,447 @@
1
+ require 'base64'
2
+ require 'time'
3
+ require 'yaml'
4
+ require 'erb'
5
+ require 'cgi'
6
+
7
+ base = File.dirname(__FILE__)
8
+ require File.join(base, 'libxml_loader')
9
+ require File.join(base, 'libxml_extensions')
10
+
11
+ # Module to handle S3 operations which don't require an internet connection,
12
+ # i.e. data validation and request-building operations;
13
+ # also holds all the constants relating to S3.
14
+ #
15
+ # Parts of this code are heavily based on Amazon's code. Here's their license:
16
+ #
17
+ # This software code is made available "AS IS" without warranties of any
18
+ # kind. You may copy, display, modify and redistribute the software
19
+ # code either by itself or as incorporated into your code; provided that
20
+ # you do not remove any proprietary notices. Your use of this software
21
+ # code is at your own risk and you waive any claim against Amazon
22
+ # Digital Services, Inc. or its affiliates with respect to your use of
23
+ # this software code. (c) 2006 Amazon Digital Services, Inc. or its
24
+ # affiliates.
25
+ module S33r
26
+ HOST = 's3.amazonaws.com'
27
+ PORT = 443
28
+ NON_SSL_PORT = 80
29
+ METADATA_PREFIX = 'x-amz-meta-'
30
+ # Size of each chunk (in bytes) to be sent per request when putting files (1Mb).
31
+ DEFAULT_CHUNK_SIZE = 1048576
32
+ AWS_HEADER_PREFIX = 'x-amz-'
33
+ AWS_AUTH_HEADER_VALUE = "AWS %s:%s"
34
+ INTERESTING_HEADERS = ['content-md5', 'content-type', 'date']
35
+ # Headers which must be included with every request to S3.
36
+ REQUIRED_HEADERS = ['Content-Type', 'Date']
37
+ # Canned ACLs made available by S3.
38
+ CANNED_ACLS = ['private', 'public-read', 'public-read-write', 'authenticated-read']
39
+ # HTTP methods which S3 will respond to.
40
+ METHOD_VERBS = ['GET', 'PUT', 'HEAD', 'DELETE']
41
+ # Maximum number which can be passed in max-keys parameter when GETting bucket list.
42
+ BUCKET_LIST_MAX_MAX_KEYS = 1000
43
+ # Default number of seconds an authenticated URL will last for (15 minutes).
44
+ DEFAULT_EXPIRY_SECS = 60 * 15
45
+ # Number of years to use for expiry date when :expires is set to :far_flung_future.
46
+ FAR_FUTURE = 20
47
+ # The namespace used for response body XML documents.
48
+ RESPONSE_NAMESPACE_URI = "http://s3.amazonaws.com/doc/2006-03-01/"
49
+
50
+ # Permissions which can be set within a <Grant>
51
+ # (see http://docs.amazonwebservices.com/AmazonS3/2006-03-01/UsingPermissions.html).
52
+ #
53
+ # NB I've missed out the WRITE_ACP permission as this is functionally
54
+ # equivalent to FULL_CONTROL.
55
+ PERMISSIONS = {
56
+ :read => 'READ', # permission to read
57
+ :write => 'WRITE', # permission to write
58
+ :read_acl => 'READ_ACP', # permission to read ACL settings
59
+ :all => 'FULL_CONTROL' # do anything
60
+ }
61
+
62
+ # Used for generating ACL XML documents.
63
+ NAMESPACE = 'xsi'
64
+ NAMESPACE_URI = 'http://www.w3.org/2001/XMLSchema-instance'
65
+ GRANTEE_TYPES = {
66
+ :amazon_customer => 'AmazonCustomerByEmail',
67
+ :canonical_user => 'CanonicalUser',
68
+ :group => 'Group'
69
+ }
70
+ S3_GROUP_TYPES = {
71
+ :all_users => 'global/AllUsers',
72
+ :authenticated_users => 'global/AuthenticatedUsers',
73
+ :log_delivery => 's3/LogDelivery'
74
+ }
75
+ GROUP_ACL_URI_BASE = 'http://acs.amazonaws.com/groups/'
76
+
77
+ include S3Exception
78
+
79
+ # Load YAML config. file for S33r operations. The config. file looks like this:
80
+ #
81
+ # :include: test/files/config.yaml
82
+ #
83
+ # The +options+ section of the YAML file is optional, and can be used
84
+ # to add application-specific settings for your application.
85
+ #
86
+ # Note that the loader also runs the config. file through ERB, so you can
87
+ # add dynamic blocks of ERB (Ruby) code into your files.
88
+ #
89
+ # +config_file+ is the path to the configuration file.
90
+ #
91
+ # Returns a <tt>[config, options]</tt>, where +config+ is a hash of standard S33r
92
+ # options (:access, :secret), and +options+ is a hash of general application options.
93
+ #
94
+ # The keys for both hashes are converted from strings into symbols.
95
+ def self.load_config(config_file)
96
+ config = YAML::load(ERB.new(IO.read(config_file)).result)
97
+
98
+ options = config.delete('options')
99
+ options = S33r.keys_to_symbols(options)
100
+
101
+ config = S33r.keys_to_symbols(config)
102
+
103
+ [config, options]
104
+ end
105
+
106
+ # Build canonical string for signing;
107
+ # modified (slightly) from the Amazon sample code.
108
+ #
109
+ # * +method+ is one of the available METHOD_VERBS.
110
+ # * +path+ is the path part of the URL to generate the canonical string for.
111
+ # * +headers+ is a hash of headers which are going to be sent with the request.
112
+ # * +expires+ is the expiry time set in the querystring for authenticated URLs:
113
+ # if supplied, it is used for the +date+ header.
114
+ def generate_canonical_string(method, path, headers={}, expires=nil)
115
+ interesting_headers = {}
116
+ headers.each do |key, value|
117
+ lk = key.downcase
118
+ if (INTERESTING_HEADERS.include?(lk) or lk =~ /^#{AWS_HEADER_PREFIX}/o)
119
+ interesting_headers[lk] = value
120
+ end
121
+ end
122
+
123
+ # These fields get empty strings if they don't exist.
124
+ interesting_headers['content-type'] ||= ''
125
+ interesting_headers['content-md5'] ||= ''
126
+
127
+ # If you're using expires for query string auth, then it trumps date.
128
+ if not expires.nil?
129
+ interesting_headers['date'] = expires
130
+ end
131
+
132
+ buf = "#{method}\n"
133
+ interesting_headers.sort { |a, b| a[0] <=> b[0] }.each do |key, value|
134
+ if key =~ /^#{AWS_HEADER_PREFIX}/o
135
+ buf << "#{key}:#{value}\n"
136
+ else
137
+ buf << "#{value}\n"
138
+ end
139
+ end
140
+
141
+ # Ignore everything after the question mark...
142
+ buf << path.gsub(/\?.*$/, '')
143
+
144
+ # ...unless there is an acl, logging or torrent parameter
145
+ if path =~ /[&?]acl($|&|=)/
146
+ buf << '?acl'
147
+ elsif path =~ /[&?]torrent($|&|=)/
148
+ buf << '?torrent'
149
+ elsif path =~ /[&?]logging($|&|=)/
150
+ buf << '?logging'
151
+ end
152
+
153
+ buf
154
+ end
155
+
156
+ # Get the value for the AWS authentication header.
157
+ def generate_auth_header_value(method, path, headers, aws_access_key, aws_secret_access_key)
158
+ raise MethodNotAllowed, "Method %s not available" % method if !METHOD_VERBS.include?(method)
159
+
160
+ # check the headers needed for authentication have been set
161
+ missing_headers = REQUIRED_HEADERS - headers.keys
162
+ if !(missing_headers.empty?)
163
+ raise MissingRequiredHeaders,
164
+ "Headers required for AWS auth value are missing: " + missing_headers.join(', ')
165
+ end
166
+
167
+ raise KeysIncomplete, "Access key or secret access key nil" \
168
+ if aws_access_key.nil? or aws_secret_access_key.nil?
169
+
170
+ # get the AWS header
171
+ canonical_string = generate_canonical_string(method, path, headers)
172
+ signature = generate_signature(aws_secret_access_key, canonical_string)
173
+ AWS_AUTH_HEADER_VALUE % [aws_access_key, signature]
174
+ end
175
+
176
+ # Encode the given string with the aws_secret_access_key, by taking the
177
+ # hmac sha1 sum, and then base64 encoding it.
178
+ def generate_signature(aws_secret_access_key, str)
179
+ digest = OpenSSL::HMAC::digest(OpenSSL::Digest::Digest.new("SHA1"), aws_secret_access_key, str)
180
+ Base64.encode64(digest).strip
181
+ end
182
+
183
+ # Build the headers required with every S3 request (Date and Content-Type);
184
+ # options hash can contain extra header settings;
185
+ # +:date+ and +:content_type+ are required headers, and set to
186
+ # defaults if not supplied.
187
+ def default_headers(existing_headers, options={})
188
+ headers = {}
189
+
190
+ # which default headers required by AWS are missing?
191
+ missing_headers = REQUIRED_HEADERS - existing_headers.keys
192
+
193
+ if missing_headers.include?('Content-Type')
194
+ headers['Content-Type'] = options[:content_type] || ''
195
+ end
196
+
197
+ if missing_headers.include?('Date')
198
+ date = options[:date] || Time.now
199
+ headers['Date'] = date.httpdate
200
+ end
201
+
202
+ headers
203
+ end
204
+
205
+ # Add metadata headers, correctly prefixing them first,
206
+ # e.g. you might do metadata_headers({'myname' => 'elliot', 'myage' => 36})
207
+ # to add two headers to the request:
208
+ #
209
+ # x-amz-meta-myname: elliot
210
+ # x-amz-meta-myage: 36
211
+ #
212
+ # Keys shouldn't have spaces; they can also be represented using symbols.
213
+ #
214
+ # Returns metadata headers appended, with both keys and values as strings.
215
+ def metadata_headers(metadata={})
216
+ headers = {}
217
+ unless metadata.empty?
218
+ metadata.each { |key, value| headers[METADATA_PREFIX + key.to_s] = value.to_s }
219
+ end
220
+ headers
221
+ end
222
+
223
+ # Content transfer headers: set Content-Type, Content-Transfer-Encoding
224
+ # and Content-Disposition headers.
225
+ #
226
+ # <tt>content_type</tt>: content type string to send in the header, e.g. 'text/html'.
227
+ #
228
+ # +key+ is the key for the object: used as the filename if the file is downloaded; defaults to
229
+ # 'download' if not set. If you use a path (e.g. '/home/you/photos/me.jpg'), just the last part
230
+ # ('me.jpg') is used as the name of the download file.
231
+ #
232
+ # <tt>render_as_attachment</tt>: set to true if you want to add a content disposition header
233
+ # which enables the object to be downloaded, rather than shown inline, when fetched by a browser.
234
+ def content_headers(content_type, key='download', render_as_attachment=false)
235
+ headers = {}
236
+
237
+ headers['Content-Type'] = content_type || 'text/plain'
238
+ mime_type = MIME::Types[content_type][0]
239
+ if mime_type
240
+ headers['Content-Transfer-Encoding'] = 'binary' if mime_type.binary?
241
+ end
242
+ if render_as_attachment
243
+ headers['Content-Disposition'] = "attachment; filename=#{File.basename(key)}"
244
+ end
245
+
246
+ headers
247
+ end
248
+
249
+ # Get a canned ACL setter header.
250
+ def canned_acl_header(canned_acl)
251
+ headers = {}
252
+ unless canned_acl.nil?
253
+ unless CANNED_ACLS.include?(canned_acl)
254
+ raise S3Exception::UnsupportedCannedACL, "The canned ACL #{canned_acl} is not supported"
255
+ end
256
+ headers[AWS_HEADER_PREFIX + 'acl'] = canned_acl
257
+ end
258
+ headers
259
+ end
260
+
261
+ # Guess a file's mime type.
262
+ # If the mime_type for a file cannot be guessed, "text/plain" is used.
263
+ def guess_mime_type(file_name)
264
+ mime_type = MIME::Types.type_for(file_name)[0]
265
+ mime_type ||= MIME::Types['text/plain'][0]
266
+ mime_type
267
+ end
268
+
269
+ # Ensure that a bucket_name is well-formed (no leading or trailing slash).
270
+ def bucket_name_valid?(bucket_name)
271
+ if ('/' == bucket_name[0,1] || '/' == bucket_name[-1,1])
272
+ raise S3Exception::MalformedBucketName, "Bucket name cannot have a leading or trailing slash"
273
+ end
274
+ end
275
+
276
+ # Convert a hash of name/value pairs to querystring variables.
277
+ # Name for a variable can be a string or symbol.
278
+ def generate_querystring(pairs=nil)
279
+ str = ''
280
+ pairs ||= {}
281
+ if pairs.size > 0
282
+ name_value_pairs = pairs.map do |key, value|
283
+ if value.nil?
284
+ key
285
+ else
286
+ "#{key}=#{CGI::escape(value.to_s)}"
287
+ end
288
+ end
289
+ str += name_value_pairs.join('&')
290
+ end
291
+ str
292
+ end
293
+
294
+ # Returns the path for this bucket and key.
295
+ #
296
+ # +options+:
297
+ # * <tt>:bucket => 'my-bucket'</tt>: get a path which includes the bucket (unless
298
+ # :subdomain => true is also passed in)
299
+ # * <tt>:key => 'my-key'</tt>: get a path including a key
300
+ # * <tt>:querystring => {'acl' => nil, 'page' => 2, ...}</tt>: adds a querystring to path
301
+ # (when generating a signature for a URL, any '?acl' or '?logging' parameters must be
302
+ # included as part of the path before hashing)
303
+ # * <tt>:subdomain => true</tt>: don't include the bucket name in the path.
304
+ # * <tt>:acl => true</tt>: append ?acl to the front of the querystring.
305
+ # * <tt>:logging => true</tt>: append ?logging to the start of the querystring.
306
+ def s3_path(options={})
307
+ bucket = options[:bucket]
308
+ key = options[:key]
309
+
310
+ qstring_pairs = options[:querystring] || {}
311
+ if options[:acl]
312
+ qstring_pairs = {:acl => nil}.merge(qstring_pairs)
313
+ elsif options[:logging]
314
+ qstring_pairs = {:logging => nil}.merge(qstring_pairs)
315
+ end
316
+
317
+ qstring = generate_querystring(qstring_pairs)
318
+
319
+ path = '/'
320
+ path += (bucket + '/') if bucket and !options[:subdomain]
321
+ path += key if key
322
+ path += '?' + qstring unless '' == qstring
323
+ path
324
+ end
325
+
326
+ # Build a URL for a bucket or object on S3.
327
+ #
328
+ # +options+ are passed through to either s3_authenticated_url or s3_public_url
329
+ # (if :authenticated, :access and :secret options are passed, s3_authenticated_url is used):
330
+ # * <tt>:bucket => 'my-bucket'</tt>: bucket the URL is for.
331
+ # * <tt>:key => 'my-key'</tt>: the key to produce a URL for.
332
+ # * <tt>:use_ssl => true</tt>: return an https:// URL.
333
+ # * <tt>:subdomain => true</tt>: use :bucket as the subdomain
334
+ # to produce a bucket URL like 'elliot.s3.amazonaws.com' instead of
335
+ # 's3.amazonaws.com/elliot'. Note that this
336
+ # is NOT SUPPORTED for authenticated requests or SSL requests.
337
+ # * <tt>:path => '/bucket/key'</tt>: include given path on end of URL; if not set,
338
+ # a path is generated from any bucket and/or key given
339
+ # * <tt>:access => 'aws access key'</tt>: Generate authenticated URL.
340
+ # * <tt>:secret => 'aws secret access key'</tt>: Generate authenticated URL.
341
+ # * <tt>:authenticated => true</tt>: Produce an authenticated URL.
342
+ # * <tt>:querystring => {'name' => 'value', 'test' => nil, ...}</tt>: add querystring
343
+ # parameters to the URL; NB any keys with a nil value are added to the querystring
344
+ # as keys without values. Note that querystring parameters are just appended in the order they
345
+ # are returned by the map iterator for a hash.
346
+ # * <tt>:acl => true</tt>: append ?acl to the front of the querystring.
347
+ # * <tt>:logging => true</tt>: append ?logging to the start of the querystring.
348
+ def s3_url(options={})
349
+ # Turn off the subdomain option if using SSL.
350
+ options[:subdomain] = false if options[:use_ssl]
351
+
352
+ access = options[:access]
353
+ secret = options[:secret]
354
+ if access and secret and options[:authenticated]
355
+ # Turn off the subdomain option (it doesn't work with authenticated URLs).
356
+ options[:subdomain] = false
357
+ s3_authenticated_url(access, secret, options)
358
+ else
359
+ s3_public_url(options)
360
+ end
361
+ end
362
+
363
+ # Public readable URL for a bucket and resource.
364
+ #
365
+ # +options+ are passed through from s3_url; only :access and :secret are irrelevant of the options
366
+ # available to s3_url.
367
+ #
368
+ # Note that if a :path option is not set, a path is generated from any :bucket and/or :path
369
+ # parameters supplied.
370
+ def s3_public_url(options)
371
+ scheme = options[:use_ssl] ? 'https' : 'http'
372
+ path = options[:path]
373
+ path ||= s3_path(options)
374
+ host = HOST
375
+ host = (options[:bucket] + "." + host ) if options[:subdomain]
376
+ "#{scheme}://" + host + path
377
+ end
378
+
379
+ # Generate a get-able URL for an S3 resource key which passes authentication in querystring. Note
380
+ # that this will correctly generate authenticated URLs for logging and ACL resources.
381
+ #
382
+ # +options+ are passed through to s3_path and s3_url; an :expires option is also available:
383
+ # * <tt>:expires => <date time></tt>: when the URL expires (seconds since the epoch); S33r.parse_expiry
384
+ # is used to generate a suitable value from a date/time string, or you can use an int. Use :far_flung_future
385
+ # to get some time in the distant future. Defaults to current time + S33r::DEFAULT_EXPIRY_SECS.
386
+ def s3_authenticated_url(aws_access_key, aws_secret_access_key, options={})
387
+ raise KeysIncomplete, "You must supply both an AWS access key and secret access key to create \
388
+ an authenticated URL" if aws_access_key.nil? or aws_secret_access_key.nil?
389
+
390
+ path = s3_path(options)
391
+ expires = S33r.parse_expiry(options[:expires])
392
+
393
+ canonical_string = generate_canonical_string('GET', path, {}, expires)
394
+ signature = generate_signature(aws_secret_access_key, canonical_string)
395
+
396
+ querystring = generate_querystring({'Signature' => signature, 'Expires' => expires,
397
+ 'AWSAccessKeyId' => aws_access_key })
398
+
399
+ options[:path] = path
400
+ base_url = s3_public_url(options)
401
+ /\?/ =~ base_url ? base_url += '&' : base_url += '?'
402
+ base_url += querystring
403
+ base_url
404
+ end
405
+
406
+ # Return the hash +hsh+ with keys converted to symbols.
407
+ def self.keys_to_symbols(hsh)
408
+ symbolised = {}
409
+ hsh.each_pair do |key, value|
410
+ symbolised[key.to_sym] = value
411
+ end
412
+ symbolised
413
+ end
414
+
415
+ # Parse an expiry date into seconds since the epoch.
416
+ #
417
+ # +expires+ can be set to :far_flung_future to get a time FAR_FUTURE years in the future;
418
+ # or to a specific date (parseable by ParseDate); or to an integer
419
+ # representing seconds since the epoch. If you leave it blank, you'll get
420
+ # the current time + DEFAULT_EXPIRY_SECS.
421
+ #
422
+ # Returns an integer representing seconds since the epoch.
423
+ def self.parse_expiry(expires=nil)
424
+ unless expires.kind_of?(Integer)
425
+ if expires.is_a?(String)
426
+ expires = Time.parse(expires).to_i
427
+ else
428
+ base_expires = Time.now.to_i
429
+ if :far_flung_future == expires
430
+ # 50 years (same as forever in computer terms)
431
+ expires = (base_expires + (60 * 60 * 24 * 365.25 * FAR_FUTURE)).to_i
432
+ else
433
+ # default to DEFAULT_EXPIRY_SECS seconds from now if expires not set
434
+ expires = base_expires + DEFAULT_EXPIRY_SECS
435
+ end
436
+ end
437
+ end
438
+ expires
439
+ end
440
+
441
+ # Remove the namespace declaration from S3 XML response bodies (libxml
442
+ # isn't fond of it).
443
+ def self.remove_namespace(xml_in)
444
+ namespace = S33r::RESPONSE_NAMESPACE_URI.gsub('/', '\/')
445
+ xml_in.gsub(/ xmlns="#{namespace}"/, '')
446
+ end
447
+ end