s33r 0.1 → 0.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.
- data/LICENSE.txt +15 -2
- data/README.txt +4 -1
- data/bin/config.yml +2 -2
- data/bin/s3cli.rb +46 -27
- data/lib/s33r.rb +2 -7
- data/lib/s33r/bucket_listing.rb +141 -0
- data/lib/s33r/client.rb +96 -65
- data/lib/s33r/core.rb +80 -2
- data/lib/s33r/libxml_extensions.rb +29 -0
- data/lib/s33r/{external/mimetypes.rb → mimetypes.rb} +0 -0
- data/lib/s33r/named_bucket.rb +34 -8
- data/lib/s33r/net_http_overrides.rb +23 -0
- data/lib/s33r/s3_exception.rb +14 -5
- data/lib/s33r/sync.rb +11 -0
- data/test/cases/spec_bucket_listing.rb +125 -0
- data/test/cases/spec_core.rb +130 -0
- data/test/cases/spec_sync.rb +29 -0
- data/test/cases/spec_xml.rb +22 -0
- data/test/cases/unit_client.rb +40 -0
- data/test/cases/unit_named_bucket.rb +12 -0
- data/test/files/bucket_listing.xml +1 -0
- data/test/files/bucket_listing2.xml +1 -0
- data/test/files/bucket_listing3.xml +8 -0
- data/test/files/bucket_listing_broken.xml +1 -0
- data/test/files/s3_object.xml +12 -0
- data/test/files/textfile.txt +10 -0
- data/test/files/wave.jpg +0 -0
- data/test/s3_test_constants.rb +25 -0
- data/test/test_bucket_setup.rb +41 -0
- metadata +27 -10
- data/lib/s33r/list_bucket_result.rb +0 -1
- data/test/spec/spec_core.rb +0 -87
data/lib/s33r/core.rb
CHANGED
@@ -1,18 +1,39 @@
|
|
1
|
+
# Parts of this code are heavily based on Amazon's code. Here's their license:
|
2
|
+
#
|
3
|
+
# This software code is made available "AS IS" without warranties of any
|
4
|
+
# kind. You may copy, display, modify and redistribute the software
|
5
|
+
# code either by itself or as incorporated into your code; provided that
|
6
|
+
# you do not remove any proprietary notices. Your use of this software
|
7
|
+
# code is at your own risk and you waive any claim against Amazon
|
8
|
+
# Digital Services, Inc. or its affiliates with respect to your use of
|
9
|
+
# this software code. (c) 2006 Amazon Digital Services, Inc. or its
|
10
|
+
# affiliates.
|
11
|
+
|
1
12
|
require 'base64'
|
2
13
|
require 'time'
|
14
|
+
require 'net/http'
|
3
15
|
require 'net/https'
|
4
16
|
require 'openssl'
|
17
|
+
require 'xml/libxml'
|
5
18
|
|
19
|
+
# this module handles operations which don't require an internet connection,
|
20
|
+
# i.e. data validation and request building operations;
|
21
|
+
# it also holds all the constants relating to S3
|
6
22
|
module S3
|
7
23
|
HOST = 's3.amazonaws.com'
|
8
24
|
PORT = 443
|
25
|
+
NON_SSL_PORT = 80
|
9
26
|
METADATA_PREFIX = 'x-amz-meta-'
|
10
27
|
AWS_HEADER_PREFIX = 'x-amz-'
|
11
28
|
AWS_AUTH_HEADER_VALUE = "AWS %s:%s"
|
12
29
|
INTERESTING_HEADERS = ['content-md5', 'content-type', 'date']
|
13
30
|
REQUIRED_HEADERS = ['Content-Type', 'Date']
|
14
31
|
CANNED_ACLS = ['private', 'public-read', 'public-read-write', 'authenticated-read']
|
15
|
-
METHOD_VERBS = ['GET', 'PUT', 'HEAD', 'POST']
|
32
|
+
METHOD_VERBS = ['GET', 'PUT', 'HEAD', 'POST', 'DELETE']
|
33
|
+
# maximum number which can be passed in max-keys parameter when GETting bucket list
|
34
|
+
BUCKET_LIST_MAX_MAX_KEYS = 1000
|
35
|
+
# default number of seconds an authenticated URL will last for (15 minutes)
|
36
|
+
DEFAULT_EXPIRY_SECS = 60 * 15
|
16
37
|
|
17
38
|
# builds the canonical string for signing;
|
18
39
|
# modified (slightly) from the Amazon sample code
|
@@ -75,7 +96,7 @@ module S3
|
|
75
96
|
|
76
97
|
# encode the given string with the aws_secret_access_key, by taking the
|
77
98
|
# hmac sha1 sum, and then base64 encoding it
|
78
|
-
def generate_signature(aws_secret_access_key, str
|
99
|
+
def generate_signature(aws_secret_access_key, str)
|
79
100
|
digest = OpenSSL::HMAC::digest(OpenSSL::Digest::Digest.new("SHA1"), aws_secret_access_key, str)
|
80
101
|
Base64.encode64(digest).strip
|
81
102
|
end
|
@@ -128,4 +149,61 @@ module S3
|
|
128
149
|
mime_type = MIME::Types['text/plain'][0] unless mime_type
|
129
150
|
mime_type
|
130
151
|
end
|
152
|
+
|
153
|
+
# ensure that a bucket_name is well-formed (no leading or trailing slash)
|
154
|
+
def bucket_name_valid?(bucket_name)
|
155
|
+
if '/' == bucket_name[0,1]
|
156
|
+
raise S3Exception::MalformedBucketName, "Bucket name cannot have a leading slash"
|
157
|
+
elsif '/' == bucket_name[-1,1]
|
158
|
+
raise S3Exception::MalformedBucketName, "Bucket name cannot have a trailing slash"
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
# convert a hash of name/value pairs to querystring variables
|
163
|
+
# names can be strings or symbols
|
164
|
+
def generate_querystring(pairs={})
|
165
|
+
str = ''
|
166
|
+
if pairs.size > 0
|
167
|
+
str += "?" + pairs.map { |key, value| "#{key}=#{CGI::escape(value.to_s)}" }.join('&')
|
168
|
+
end
|
169
|
+
str
|
170
|
+
end
|
171
|
+
|
172
|
+
# put / between args, but only if it's not already there
|
173
|
+
def url_join(*args)
|
174
|
+
url_start = ''
|
175
|
+
url_end = args.join('/')
|
176
|
+
|
177
|
+
# string index where the scheme of the URL (xxxx://) ends
|
178
|
+
scheme_ends_at = (url_end =~ /:\/\//)
|
179
|
+
unless scheme_ends_at.nil?
|
180
|
+
scheme_ends_at = scheme_ends_at + 1
|
181
|
+
url_start = url_end[0..scheme_ends_at]
|
182
|
+
url_end = url_end[(scheme_ends_at + 1)..-1]
|
183
|
+
end
|
184
|
+
|
185
|
+
# replace any multiple forward slashes (except those in the scheme)
|
186
|
+
url_end = url_end.gsub(/\/{2,}/, '/')
|
187
|
+
|
188
|
+
return url_start + url_end
|
189
|
+
end
|
190
|
+
|
191
|
+
def s3_absolute_url(bucket_name, resource_key)
|
192
|
+
"http://" + HOST + '/' + bucket_name + '/' + resource_key
|
193
|
+
end
|
194
|
+
|
195
|
+
# generate a gettable URL for an S3 resource key which passes authentication in querystring
|
196
|
+
# int expires: when the URL expires (seconds since the epoch)
|
197
|
+
def s3_authenticated_url(aws_access_key, aws_secret_access_key, bucket_name, resource_key,
|
198
|
+
expires)
|
199
|
+
path = '/' + bucket_name + '/' + resource_key
|
200
|
+
|
201
|
+
canonical_string = generate_canonical_string('GET', path, {}, expires)
|
202
|
+
signature = generate_signature(aws_secret_access_key, canonical_string)
|
203
|
+
|
204
|
+
querystring = generate_querystring({ 'Signature' => signature, 'Expires' => expires,
|
205
|
+
'AWSAccessKeyId' => aws_access_key })
|
206
|
+
|
207
|
+
return s3_absolute_url(bucket_name, resource_key) + querystring
|
208
|
+
end
|
131
209
|
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# convenience methods for libxml classes
|
2
|
+
module XML
|
3
|
+
# find first matching element and return its content
|
4
|
+
# xpath: XPath query
|
5
|
+
# returns nil if no element matches xpath
|
6
|
+
def xget(xpath)
|
7
|
+
nodes = self.find(xpath).to_a
|
8
|
+
if nodes.empty?
|
9
|
+
return nil
|
10
|
+
else
|
11
|
+
return nodes.first.content
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# parse an XML string into an XML::Document instance
|
16
|
+
def XML.get_xml_doc(xml_str)
|
17
|
+
parser = XML::Parser.new
|
18
|
+
parser.string = xml_str
|
19
|
+
parser.parse
|
20
|
+
end
|
21
|
+
|
22
|
+
class Document
|
23
|
+
include XML
|
24
|
+
end
|
25
|
+
|
26
|
+
class Node
|
27
|
+
include XML
|
28
|
+
end
|
29
|
+
end
|
File without changes
|
data/lib/s33r/named_bucket.rb
CHANGED
@@ -1,19 +1,40 @@
|
|
1
|
+
# TODO: provide access to metadata on the enclosed bucket_listing
|
2
|
+
|
1
3
|
module S3
|
2
4
|
# a client for dealing with a single bucket
|
3
5
|
class NamedBucket < Client
|
4
6
|
attr_accessor :bucket_name
|
5
|
-
|
6
|
-
# TODO: check bucket exists before setting it
|
7
|
+
|
7
8
|
# options available:
|
8
|
-
# :public_contents => true: all items put into bucket are made public
|
9
|
+
# :public_contents => true: all items put into bucket are made public (can be overridden per request)
|
10
|
+
# :strict => true: check whether the bucket exists before attempting to initialize
|
9
11
|
def initialize(aws_access_key, aws_secret_access_key, bucket_name, options={}, &block)
|
10
|
-
super(aws_access_key, aws_secret_access_key)
|
11
12
|
@bucket_name = bucket_name
|
13
|
+
|
14
|
+
# holds a BucketListing instance
|
15
|
+
@bucket_listing = nil
|
16
|
+
|
17
|
+
# all content should be created as public-read
|
12
18
|
@client_headers.merge!(canned_acl_header('public-read')) if options[:public_contents]
|
13
|
-
|
19
|
+
|
20
|
+
super(aws_access_key, aws_secret_access_key, options)
|
21
|
+
|
22
|
+
if true == options[:strict]
|
23
|
+
raise S3Exception::MissingResource unless bucket_exists?(bucket_name)
|
24
|
+
end
|
14
25
|
end
|
15
26
|
|
16
|
-
def
|
27
|
+
def metadata
|
28
|
+
# TODO: get bucket metadata from the bucket_listing
|
29
|
+
end
|
30
|
+
|
31
|
+
def contents
|
32
|
+
# TODO: S3Object instances inside the bucket_listing
|
33
|
+
end
|
34
|
+
|
35
|
+
def listing
|
36
|
+
# TODO: build a bucket_listing whose objects are associated with the bucket
|
37
|
+
# and set the @bucket_listing instance variable
|
17
38
|
list_bucket(@bucket_name)
|
18
39
|
end
|
19
40
|
|
@@ -21,8 +42,13 @@ module S3
|
|
21
42
|
super(string, @bucket_name, resource_key, headers)
|
22
43
|
end
|
23
44
|
|
24
|
-
def put_file(filename, resource_key=nil, headers={})
|
25
|
-
super(filename, @bucket_name, resource_key, headers)
|
45
|
+
def put_file(filename, resource_key=nil, headers={}, options={})
|
46
|
+
super(filename, @bucket_name, resource_key, headers, options)
|
47
|
+
end
|
48
|
+
|
49
|
+
# expires: time in secs since the epoch when
|
50
|
+
def s3_authenticated_url(resource_key, expires=(Time.now.to_i + DEFAULT_EXPIRY_SECS))
|
51
|
+
super(@aws_access_key, @aws_secret_access_key, @bucket_name, resource_key, expires)
|
26
52
|
end
|
27
53
|
end
|
28
54
|
end
|
@@ -1,5 +1,8 @@
|
|
1
1
|
require 'net/http'
|
2
2
|
|
3
|
+
# overrides for the default Net::HTTP classes
|
4
|
+
|
5
|
+
# add some convenience functions for checking response status
|
3
6
|
class Net::HTTPResponse
|
4
7
|
attr_accessor :success
|
5
8
|
|
@@ -11,11 +14,17 @@ class Net::HTTPResponse
|
|
11
14
|
response.is_a?(Net::HTTPOK)
|
12
15
|
end
|
13
16
|
|
17
|
+
def not_found
|
18
|
+
response.is_a?(Net::HTTPNotFound)
|
19
|
+
end
|
20
|
+
|
14
21
|
def to_s
|
15
22
|
body
|
16
23
|
end
|
17
24
|
end
|
18
25
|
|
26
|
+
# modified to enable larger chunk sizes to be used
|
27
|
+
# when streaming data from large files
|
19
28
|
class Net::HTTPGenericRequest
|
20
29
|
attr_accessor :chunk_size
|
21
30
|
|
@@ -39,4 +48,18 @@ class Net::HTTPGenericRequest
|
|
39
48
|
end
|
40
49
|
end
|
41
50
|
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class Net::HTTPRequest
|
54
|
+
def to_s
|
55
|
+
str = "*******\n" +
|
56
|
+
"#{self.class::METHOD} #{@path} HTTP/1.1\n" +
|
57
|
+
"Host: #{S3::HOST}\n"
|
58
|
+
|
59
|
+
self.each_capitalized do |key, value|
|
60
|
+
str += "#{key}: #{value}\n"
|
61
|
+
end
|
62
|
+
str += "*******\n\n"
|
63
|
+
str
|
64
|
+
end
|
42
65
|
end
|
data/lib/s33r/s3_exception.rb
CHANGED
@@ -1,20 +1,29 @@
|
|
1
1
|
module S3
|
2
2
|
module S3Exception
|
3
|
-
|
3
|
+
|
4
4
|
class MethodNotAvailable < Exception
|
5
5
|
end
|
6
|
-
|
6
|
+
|
7
7
|
class MissingRequiredHeaders < Exception
|
8
8
|
end
|
9
|
-
|
9
|
+
|
10
10
|
class UnsupportedCannedACL < Exception
|
11
11
|
end
|
12
|
-
|
12
|
+
|
13
13
|
class UnsupportedHTTPMethod < Exception
|
14
14
|
end
|
15
|
-
|
15
|
+
|
16
16
|
class MalformedBucketName < Exception
|
17
17
|
end
|
18
18
|
|
19
|
+
class MissingResource < Exception
|
20
|
+
end
|
21
|
+
|
22
|
+
class BucketListingMaxKeysError < Exception
|
23
|
+
end
|
24
|
+
|
25
|
+
class InvalidBucketListing < Exception
|
26
|
+
end
|
27
|
+
|
19
28
|
end
|
20
29
|
end
|
data/lib/s33r/sync.rb
ADDED
@@ -0,0 +1,125 @@
|
|
1
|
+
base = File.dirname(__FILE__)
|
2
|
+
require base + '/../s3_test_constants'
|
3
|
+
require 'set'
|
4
|
+
|
5
|
+
context 'S3 bucket listing' do
|
6
|
+
setup do
|
7
|
+
xml_file = File.join(base, '../files/bucket_listing.xml')
|
8
|
+
@with_bucket_listing_xml = File.open(xml_file) { |f| f.read }
|
9
|
+
xml_file2 = File.join(base, '../files/bucket_listing2.xml')
|
10
|
+
@with_bucket_listing_xml2 = File.open(xml_file2) { |f| f.read }
|
11
|
+
xml_file3 = File.join(base, '../files/bucket_listing3.xml')
|
12
|
+
@with_empty_bucket_listing_xml = File.open(xml_file3) { |f| f.read }
|
13
|
+
xml_file4 = File.join(base, '../files/bucket_listing_broken.xml')
|
14
|
+
@with_broken_bucket_listing_xml = File.open(xml_file4) { |f| f.read }
|
15
|
+
@bucket_listing = BucketListing.new(@with_bucket_listing_xml)
|
16
|
+
@bucket_properties = %w(name prefix marker max_keys is_truncated)
|
17
|
+
@bucket_property_setters = @bucket_properties.map { |prop| prop + "=" }
|
18
|
+
end
|
19
|
+
|
20
|
+
specify 'can only be created if bucket listing XML supplied' do
|
21
|
+
lambda { BucketListing.new }.should.raise ArgumentError
|
22
|
+
end
|
23
|
+
|
24
|
+
specify 'cannot be created from invalid XML' do
|
25
|
+
lambda { BucketListing.new(nil) }.should.raise S3Exception::InvalidBucketListing
|
26
|
+
end
|
27
|
+
|
28
|
+
specify 'should recover gracefully from broken bucket listing XML' do
|
29
|
+
lambda { BucketListing.new(@with_broken_bucket_listing_xml) }.should.raise S3Exception::InvalidBucketListing
|
30
|
+
end
|
31
|
+
|
32
|
+
specify 'should cope if bucket is empty (i.e. no <Contents> elements)' do
|
33
|
+
@bucket_listing.set_listing_xml(@with_empty_bucket_listing_xml)
|
34
|
+
end
|
35
|
+
|
36
|
+
specify 'can return the raw XML used to initialise it' do
|
37
|
+
@bucket_listing.listing_xml.should.equal(@with_bucket_listing_xml)
|
38
|
+
end
|
39
|
+
|
40
|
+
specify 'can have the bucket listing XML reset' do
|
41
|
+
@bucket_listing.should.respond_to :set_listing_xml
|
42
|
+
end
|
43
|
+
|
44
|
+
specify 'should present bucket metadata as typed properties' do
|
45
|
+
@bucket_listing.name.should.equal('testingtesting')
|
46
|
+
@bucket_listing.prefix.should.equal('')
|
47
|
+
@bucket_listing.marker.should.equal('')
|
48
|
+
@bucket_listing.max_keys.should.equal(1000)
|
49
|
+
@bucket_listing.is_truncated.should.equal(false)
|
50
|
+
@bucket_listing.delimiter.should.be nil
|
51
|
+
end
|
52
|
+
|
53
|
+
specify 'when listing XML is reset, should update all properties correctly' do
|
54
|
+
@bucket_listing.set_listing_xml(@with_bucket_listing_xml2)
|
55
|
+
@bucket_listing.name.should.equal('testing2')
|
56
|
+
@bucket_listing.prefix.should.equal('/home/ell/')
|
57
|
+
@bucket_listing.marker.should.equal('')
|
58
|
+
@bucket_listing.max_keys.should.equal(100)
|
59
|
+
@bucket_listing.delimiter.should.equal('/')
|
60
|
+
@bucket_listing.is_truncated.should.equal(false)
|
61
|
+
end
|
62
|
+
|
63
|
+
specify 'should provide private setters for metadata' do
|
64
|
+
# all private methods
|
65
|
+
methods = @bucket_listing.private_methods.to_set
|
66
|
+
# names of the setters
|
67
|
+
setters = @bucket_property_setters
|
68
|
+
# all methods should be a superset of the private setters
|
69
|
+
methods.superset?(setters.to_set).should.equal true
|
70
|
+
end
|
71
|
+
|
72
|
+
specify 'should store resources (<Contents> elements) in a hash' do
|
73
|
+
@bucket_listing.contents.size.should_be 10
|
74
|
+
first_obj = @bucket_listing.contents['/home/ell/dir1/four.txt']
|
75
|
+
first_obj.should_be_instance_of S3Object
|
76
|
+
end
|
77
|
+
|
78
|
+
specify 'should enable access to metadata for a resource by its key' do
|
79
|
+
obj = @bucket_listing['/home/ell/dir1/four.txt']
|
80
|
+
obj.should_be_instance_of S3Object
|
81
|
+
obj.etag.should_equal '24ce59274b89287b3960c184153ac24b'
|
82
|
+
end
|
83
|
+
|
84
|
+
specify 'should be able to build a full representation given full object XML from GET on resource key' do
|
85
|
+
fix
|
86
|
+
end
|
87
|
+
|
88
|
+
specify 'should provide easy access to <CommonPrefixes> elements as a hash' do
|
89
|
+
fix
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
context 'S3 object' do
|
94
|
+
setup do
|
95
|
+
@s3_object_xml = File.open(File.join(base, '../files/s3_object.xml')).read
|
96
|
+
@s3obj = S3Object.new
|
97
|
+
@s3obj.set_from_xml_string(@s3_object_xml)
|
98
|
+
end
|
99
|
+
|
100
|
+
specify 'can be initialised from XML fragment with correct data types' do
|
101
|
+
@s3obj.key.should.equal '/home/ell/dir1/four.txt'
|
102
|
+
d = @s3obj.last_modified
|
103
|
+
[d.year, d.month, d.day, d.hour, d.min, d.sec].should.equal [2006, 8, 19, 22, 53, 29]
|
104
|
+
@s3obj.etag.should.equal '24ce59274b89287b3960c184153ac24b'
|
105
|
+
@s3obj.size.should.equal 14
|
106
|
+
end
|
107
|
+
|
108
|
+
specify 'should treat the owner as an object in his/her own right' do
|
109
|
+
[@s3obj.owner.id, @s3obj.owner.display_name].should.equal \
|
110
|
+
['56efddfead5aa65da942f156fb2b294f44d78fd932d701331edc5fba19620fd4', 'elliotsmith3']
|
111
|
+
@s3obj.owner.should_be_instance_of S3User
|
112
|
+
end
|
113
|
+
|
114
|
+
specify 'can be associated with a NamedBucket' do
|
115
|
+
fix
|
116
|
+
end
|
117
|
+
|
118
|
+
specify 'can be saved by proxing through the NamedBucket it is associated with' do
|
119
|
+
fix
|
120
|
+
end
|
121
|
+
|
122
|
+
specify 'cannot be saved unless associated with a NamedBucket' do
|
123
|
+
fix
|
124
|
+
end
|
125
|
+
end
|
@@ -0,0 +1,130 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/../s3_test_constants'
|
2
|
+
|
3
|
+
context 'S3 core' do
|
4
|
+
|
5
|
+
setup do
|
6
|
+
@for_request_method = 'PUT'
|
7
|
+
@for_request_path = "/quotes/nelson"
|
8
|
+
@for_request_headers = {
|
9
|
+
"Content-Md5" => "c8fdb181845a4ca6b8fec737b3581d76",
|
10
|
+
"Content-Type" => "text/html",
|
11
|
+
"Date" => "Thu, 17 Nov 2005 18:49:58 GMT",
|
12
|
+
"X-Amz-Meta-Author" => "foo@bar.com",
|
13
|
+
"X-Amz-Magic" => "abracadabra"
|
14
|
+
}
|
15
|
+
|
16
|
+
# create broken request header hash
|
17
|
+
@for_incomplete_headers = @for_request_headers.clone.delete_if do |key,value|
|
18
|
+
'Content-Type' == key or 'Date' == key
|
19
|
+
end
|
20
|
+
|
21
|
+
@correct_canonical_string = "PUT\nc8fdb181845a4ca6b8fec737b3581d76\n" +
|
22
|
+
"text/html\nThu, 17 Nov 2005 18:49:58 GMT\nx-amz-magic:abracadabra\n" +
|
23
|
+
"x-amz-meta-author:foo@bar.com\n/quotes/nelson"
|
24
|
+
@correct_signature = "jZNOcbfWmD/A/f3hSvVzXZjM2HU="
|
25
|
+
@correct_auth_header = "AWS #{S3Testing::ACCESS_KEY}:#{@correct_signature}"
|
26
|
+
|
27
|
+
@with_invalid_bucket_name = '/badbucket'
|
28
|
+
@with_invalid_bucket_name2 = 'badbucket/'
|
29
|
+
|
30
|
+
@correct_authenticated_url = "http://s3.amazonaws.com/quotes/nelson?Signature=vjbyPxybdZaNmGa%2ByT272YEAiv4%3D&"+
|
31
|
+
"AWSAccessKeyId=44CF9590006BF252F707&Expires=1141889120"
|
32
|
+
end
|
33
|
+
|
34
|
+
specify 'should generate correct canonical strings' do
|
35
|
+
generate_canonical_string(@for_request_method, @for_request_path,
|
36
|
+
@for_request_headers).should.equal @correct_canonical_string
|
37
|
+
end
|
38
|
+
|
39
|
+
specify 'should generate correct signatures' do
|
40
|
+
generate_signature(S3Testing::SECRET_ACCESS_KEY,
|
41
|
+
@correct_canonical_string).should.equal @correct_signature
|
42
|
+
end
|
43
|
+
|
44
|
+
specify 'should generate correct auth headers' do
|
45
|
+
generate_auth_header_value(@for_request_method, @for_request_path, @for_request_headers,
|
46
|
+
S3Testing::ACCESS_KEY, S3Testing::SECRET_ACCESS_KEY).should.equal @correct_auth_header
|
47
|
+
end
|
48
|
+
|
49
|
+
specify 'should not generate auth header if bad HTTP method passed' do
|
50
|
+
lambda { generate_auth_header_value('duff', nil, nil, nil, nil) }.should.raise \
|
51
|
+
S3Exception::MethodNotAvailable
|
52
|
+
end
|
53
|
+
|
54
|
+
specify 'should not generate auth header if required headers missing' do
|
55
|
+
lambda { generate_auth_header_value('PUT', '/', @for_incomplete_headers,
|
56
|
+
nil, nil) }.should.raise S3Exception::MissingRequiredHeaders
|
57
|
+
end
|
58
|
+
|
59
|
+
specify 'when generating auth header, should allow addition of Date and Content-Type headers' do
|
60
|
+
now = Time.now
|
61
|
+
|
62
|
+
fixed_headers = add_default_headers(@for_incomplete_headers, :date => now,
|
63
|
+
:content_type => 'text/html')
|
64
|
+
|
65
|
+
fixed_headers['Date'].should.equal now.httpdate
|
66
|
+
fixed_headers['Content-Type'].should.equal 'text/html'
|
67
|
+
end
|
68
|
+
|
69
|
+
specify 'should not generate canned ACL header if invalid canned ACL supplied' do
|
70
|
+
lambda { canned_acl_header('duff') }.should.raise \
|
71
|
+
S3Exception::UnsupportedCannedACL
|
72
|
+
end
|
73
|
+
|
74
|
+
specify 'should correctly add canned ACL headers' do
|
75
|
+
new_headers = canned_acl_header('private', { 'Content-Type' => 'text/html' })
|
76
|
+
new_headers.should.have(2).keys
|
77
|
+
new_headers.keys.should.include 'Content-Type'
|
78
|
+
new_headers.keys.should.include 'x-amz-acl'
|
79
|
+
new_headers['x-amz-acl'].should.equal 'private'
|
80
|
+
end
|
81
|
+
|
82
|
+
specify 'should set sensible defaults for missing Content-Type and Date headers' do
|
83
|
+
fixed_headers = add_default_headers(@for_incomplete_headers)
|
84
|
+
fixed_headers['Content-Type'].should.equal ''
|
85
|
+
fixed_headers.include?('Date').should.not.be nil
|
86
|
+
end
|
87
|
+
|
88
|
+
specify 'should default to text/plain mimetype for unknown file types' do
|
89
|
+
guess_mime_type('hello.madeup').should.equal('text/plain')
|
90
|
+
end
|
91
|
+
|
92
|
+
specify 'should recognise invalid bucket names' do
|
93
|
+
lambda { bucket_name_valid?(@with_invalid_bucket_name) }.should.raise \
|
94
|
+
S3Exception::MalformedBucketName
|
95
|
+
lambda { bucket_name_valid?(@with_invalid_bucket_name2) }.should.raise \
|
96
|
+
S3Exception::MalformedBucketName
|
97
|
+
end
|
98
|
+
|
99
|
+
specify 'should return empty string if generating querystring with no key/value pairs' do
|
100
|
+
generate_querystring({}).should_equal ''
|
101
|
+
end
|
102
|
+
|
103
|
+
specify 'should correctly format querystring key/value pairs' do
|
104
|
+
generate_querystring({'message' => 'Hello world', 'id' => 1, 'page' => '[2,4]'}).should_equal \
|
105
|
+
'?message=Hello+world&id=1&page=%5B2%2C4%5D'
|
106
|
+
end
|
107
|
+
|
108
|
+
specify 'should allow symbols as names for querystring variables when generating querystrings' do
|
109
|
+
generate_querystring({ :prefix => '/home/ell' }).should.equal('?prefix=%2Fhome%2Fell')
|
110
|
+
end
|
111
|
+
|
112
|
+
specify 'should convert integers to strings when generating querystrings' do
|
113
|
+
generate_querystring({ 'max-keys' => 400 }).should.equal('?max-keys=400')
|
114
|
+
end
|
115
|
+
|
116
|
+
specify 'should be able to construct correct URL paths from path fragments' do
|
117
|
+
url_join('http://', 'localhost/', 'test').should.equal('http://localhost/test')
|
118
|
+
url_join('ftp://test.com', '/public/', '/files').should.equal('ftp://test.com/public/files')
|
119
|
+
url_join('http://localhost/', '/index.html').should.equal('http://localhost/index.html')
|
120
|
+
url_join('http://', 'localhost', 'test').should.equal('http://localhost/test')
|
121
|
+
url_join('http://localhost:8080/test/', '/path/', 'element.html').should.equal('http://localhost:8080/test/path/element.html')
|
122
|
+
url_join('/test/', 'index.html').should.equal '/test/index.html'
|
123
|
+
end
|
124
|
+
|
125
|
+
specify 'should generate URLs with authentication parameters' do
|
126
|
+
s3_authenticated_url(S3Testing::ACCESS_KEY, S3Testing::SECRET_ACCESS_KEY, 'quotes', 'nelson', \
|
127
|
+
1141889120).should.equal @correct_authenticated_url
|
128
|
+
end
|
129
|
+
|
130
|
+
end
|