s3 0.2.0
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/.document +5 -0
- data/.gitignore +6 -0
- data/LICENSE +20 -0
- data/README.rdoc +48 -0
- data/Rakefile +58 -0
- data/VERSION +1 -0
- data/bin/stree +187 -0
- data/extra/stree_backend.rb +159 -0
- data/lib/stree/bucket.rb +186 -0
- data/lib/stree/connection.rb +199 -0
- data/lib/stree/exceptions.rb +108 -0
- data/lib/stree/object.rb +210 -0
- data/lib/stree/parser.rb +48 -0
- data/lib/stree/roxy/moxie.rb +58 -0
- data/lib/stree/roxy/proxy.rb +72 -0
- data/lib/stree/service.rb +110 -0
- data/lib/stree/signature.rb +157 -0
- data/lib/stree.rb +24 -0
- data/test/bucket_test.rb +231 -0
- data/test/connection_test.rb +164 -0
- data/test/object_test.rb +164 -0
- data/test/service_test.rb +128 -0
- data/test/signature_test.rb +143 -0
- data/test/test_helper.rb +11 -0
- metadata +94 -0
@@ -0,0 +1,110 @@
|
|
1
|
+
module Stree
|
2
|
+
class Service
|
3
|
+
include Parser
|
4
|
+
extend Roxy::Moxie
|
5
|
+
|
6
|
+
attr_reader :access_key_id, :secret_access_key, :use_ssl
|
7
|
+
|
8
|
+
# Compares service to other, by access_key_id and secret_access_key
|
9
|
+
def ==(other)
|
10
|
+
self.access_key_id == other.access_key_id and self.secret_access_key == other.secret_access_key
|
11
|
+
end
|
12
|
+
|
13
|
+
# ==== Parameters:
|
14
|
+
# +options+:: a hash of options described below
|
15
|
+
#
|
16
|
+
# ==== Options:
|
17
|
+
# +access_key_id+:: Amazon access key id, required
|
18
|
+
# +secret_access_key+:: Amazon secret access key, required
|
19
|
+
# +use_ssl+:: true if use ssl in connection, otherwise false
|
20
|
+
# +timeout+:: parameter for Net::HTTP module
|
21
|
+
# +debug+:: prints the raw requests to STDOUT
|
22
|
+
def initialize(options)
|
23
|
+
@access_key_id = options[:access_key_id] or raise ArgumentError, "No access key id given"
|
24
|
+
@secret_access_key = options[:secret_access_key] or raise ArgumentError, "No secret access key given"
|
25
|
+
@use_ssl = options[:use_ssl]
|
26
|
+
@timeout = options[:timeout]
|
27
|
+
@debug = options[:debug]
|
28
|
+
end
|
29
|
+
|
30
|
+
# Returns all buckets in the service and caches the result (see reload)
|
31
|
+
def buckets(reload = false)
|
32
|
+
if reload or @buckets.nil?
|
33
|
+
@buckets = list_all_my_buckets
|
34
|
+
else
|
35
|
+
@buckets
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Returns "http://" or "https://", depends on use_ssl value from initializer
|
40
|
+
def protocol
|
41
|
+
use_ssl ? "https://" : "http://"
|
42
|
+
end
|
43
|
+
|
44
|
+
# Return 443 or 80, depends on use_ssl value from initializer
|
45
|
+
def port
|
46
|
+
use_ssl ? 443 : 80
|
47
|
+
end
|
48
|
+
|
49
|
+
proxy :buckets do
|
50
|
+
# Builds new bucket with given name
|
51
|
+
def build(name)
|
52
|
+
Bucket.send(:new, proxy_owner, name)
|
53
|
+
end
|
54
|
+
|
55
|
+
# Finds the bucket with given name
|
56
|
+
def find_first(name)
|
57
|
+
bucket = build(name)
|
58
|
+
bucket.retrieve
|
59
|
+
end
|
60
|
+
alias :find :find_first
|
61
|
+
|
62
|
+
# Find all buckets in the service
|
63
|
+
def find_all
|
64
|
+
proxy_target
|
65
|
+
end
|
66
|
+
|
67
|
+
# Reloads the bucket list (clears the cache)
|
68
|
+
def reload
|
69
|
+
proxy_owner.buckets(true)
|
70
|
+
end
|
71
|
+
|
72
|
+
# Destroy all buckets in the service. Doesn't destroy non-empty
|
73
|
+
# buckets by default, pass true to force destroy (USE WITH
|
74
|
+
# CARE!).
|
75
|
+
def destroy_all(force = false)
|
76
|
+
proxy_target.each do |bucket|
|
77
|
+
bucket.destroy(force)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
def inspect #:nodoc:
|
83
|
+
"#<#{self.class}:#@access_key_id>"
|
84
|
+
end
|
85
|
+
|
86
|
+
private
|
87
|
+
|
88
|
+
def list_all_my_buckets
|
89
|
+
response = service_request(:get)
|
90
|
+
names = parse_list_all_my_buckets_result(response.body)
|
91
|
+
names.map { |name| Bucket.send(:new, self, name) }
|
92
|
+
end
|
93
|
+
|
94
|
+
def service_request(method, options = {})
|
95
|
+
connection.request(method, options.merge(:path => "/#{options[:path]}"))
|
96
|
+
end
|
97
|
+
|
98
|
+
def connection
|
99
|
+
if @connection.nil?
|
100
|
+
@connection = Connection.new
|
101
|
+
@connection.access_key_id = @access_key_id
|
102
|
+
@connection.secret_access_key = @secret_access_key
|
103
|
+
@connection.use_ssl = @use_ssl
|
104
|
+
@connection.timeout = @timeout
|
105
|
+
@connection.debug = @debug
|
106
|
+
end
|
107
|
+
@connection
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
@@ -0,0 +1,157 @@
|
|
1
|
+
module Stree
|
2
|
+
|
3
|
+
# Class responsible for generating signatures to requests.
|
4
|
+
#
|
5
|
+
# Implements algorithm defined by Amazon Web Services to sign
|
6
|
+
# request with secret private credentials
|
7
|
+
#
|
8
|
+
# === See:
|
9
|
+
# http://docs.amazonwebservices.com/AmazonS3/latest/index.html?RESTAuthentication.html
|
10
|
+
|
11
|
+
class Signature
|
12
|
+
|
13
|
+
# Generates signature for given parameters
|
14
|
+
#
|
15
|
+
# ==== Parameters:
|
16
|
+
# +options+: a hash that contains options listed below
|
17
|
+
#
|
18
|
+
# ==== Options:
|
19
|
+
# +host+: hostname
|
20
|
+
# +request+: Net::HTTPRequest object with correct headers
|
21
|
+
# +access_key_id+: access key id
|
22
|
+
# +secret_access_key+: secret access key
|
23
|
+
#
|
24
|
+
# ==== Returns:
|
25
|
+
# Generated signature for given hostname and request
|
26
|
+
def self.generate(options)
|
27
|
+
request = options[:request]
|
28
|
+
host = options[:host]
|
29
|
+
access_key_id = options[:access_key_id]
|
30
|
+
secret_access_key = options[:secret_access_key]
|
31
|
+
|
32
|
+
http_verb = request.method
|
33
|
+
content_md5 = request["content-md5"] || ""
|
34
|
+
content_type = request["content-type"] || ""
|
35
|
+
date = request["x-amz-date"].nil? ? request["date"] : ""
|
36
|
+
canonicalized_resource = canonicalized_resource(host, request)
|
37
|
+
canonicalized_amz_headers = canonicalized_amz_headers(request)
|
38
|
+
|
39
|
+
string_to_sign = ""
|
40
|
+
string_to_sign << http_verb
|
41
|
+
string_to_sign << "\n"
|
42
|
+
string_to_sign << content_md5
|
43
|
+
string_to_sign << "\n"
|
44
|
+
string_to_sign << content_type
|
45
|
+
string_to_sign << "\n"
|
46
|
+
string_to_sign << date
|
47
|
+
string_to_sign << "\n"
|
48
|
+
string_to_sign << canonicalized_amz_headers
|
49
|
+
string_to_sign << canonicalized_resource
|
50
|
+
|
51
|
+
digest = OpenSSL::Digest::Digest.new('sha1')
|
52
|
+
hmac = OpenSSL::HMAC.digest(digest, secret_access_key, string_to_sign)
|
53
|
+
base64 = Base64.encode64(hmac)
|
54
|
+
signature = base64.chomp
|
55
|
+
|
56
|
+
"AWS #{access_key_id}:#{signature}"
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
# Helper method for extracting header fields from Net::HTTPRequest and
|
62
|
+
# preparing them for singing in #generate method
|
63
|
+
#
|
64
|
+
# ==== Parameters:
|
65
|
+
# +request+: Net::HTTPRequest object with header fields filled in
|
66
|
+
#
|
67
|
+
# ==== Returns:
|
68
|
+
# String containing interesting header fields in suitable order and form
|
69
|
+
def self.canonicalized_amz_headers(request)
|
70
|
+
headers = []
|
71
|
+
|
72
|
+
# 1. Convert each HTTP header name to lower-case. For example,
|
73
|
+
# 'X-Amz-Date' becomes 'x-amz-date'.
|
74
|
+
request.each { |key, value| headers << [key.downcase, value] if key =~ /\Ax-amz-/io }
|
75
|
+
#=> [["c", 0], ["a", 1], ["a", 2], ["b", 3]]
|
76
|
+
|
77
|
+
# 2. Sort the collection of headers lexicographically by header
|
78
|
+
# name.
|
79
|
+
headers.sort!
|
80
|
+
#=> [["a", 1], ["a", 2], ["b", 3], ["c", 0]]
|
81
|
+
|
82
|
+
# 3. Combine header fields with the same name into one
|
83
|
+
# "header-name:comma-separated-value-list" pair as prescribed by
|
84
|
+
# RFC 2616, section 4.2, without any white-space between
|
85
|
+
# values. For example, the two metadata headers
|
86
|
+
# 'x-amz-meta-username: fred' and 'x-amz-meta-username: barney'
|
87
|
+
# would be combined into the single header 'x-amz-meta-username:
|
88
|
+
# fred,barney'.
|
89
|
+
groupped_headers = headers.group_by { |i| i.first }
|
90
|
+
#=> {"a"=>[["a", 1], ["a", 2]], "b"=>[["b", 3]], "c"=>[["c", 0]]}
|
91
|
+
combined_headers = groupped_headers.map do |key, value|
|
92
|
+
values = value.map { |e| e.last }
|
93
|
+
[key, values.join(",")]
|
94
|
+
end
|
95
|
+
#=> [["a", "1,2"], ["b", "3"], ["c", "0"]]
|
96
|
+
|
97
|
+
# 4. "Un-fold" long headers that span multiple lines (as allowed
|
98
|
+
# by RFC 2616, section 4.2) by replacing the folding white-space
|
99
|
+
# (including new-line) by a single space.
|
100
|
+
unfolded_headers = combined_headers.map do |header|
|
101
|
+
key = header.first
|
102
|
+
value = header.last
|
103
|
+
value.gsub!(/\s+/, " ")
|
104
|
+
[key, value]
|
105
|
+
end
|
106
|
+
|
107
|
+
# 5. Trim any white-space around the colon in the header. For
|
108
|
+
# example, the header 'x-amz-meta-username: fred,barney' would
|
109
|
+
# become 'x-amz-meta-username:fred,barney'
|
110
|
+
joined_headers = unfolded_headers.map do |header|
|
111
|
+
key = header.first.strip
|
112
|
+
value = header.last.strip
|
113
|
+
"#{key}:#{value}"
|
114
|
+
end
|
115
|
+
|
116
|
+
# 6. Finally, append a new-line (U+000A) to each canonicalized
|
117
|
+
# header in the resulting list. Construct the
|
118
|
+
# CanonicalizedResource element by concatenating all headers in
|
119
|
+
# this list into a single string.
|
120
|
+
joined_headers << "" unless joined_headers.empty?
|
121
|
+
joined_headers.join("\n")
|
122
|
+
end
|
123
|
+
|
124
|
+
# Helper methods for extracting caninocalized resource address
|
125
|
+
#
|
126
|
+
# ==== Parameters:
|
127
|
+
# +host+: hostname
|
128
|
+
# +request+: Net::HTTPRequest object with headers filealds filled in
|
129
|
+
#
|
130
|
+
# ==== Returns:
|
131
|
+
# String containing extracted canonicalized resource
|
132
|
+
def self.canonicalized_resource(host, request)
|
133
|
+
# 1. Start with the empty string ("").
|
134
|
+
string = ""
|
135
|
+
|
136
|
+
# 2. If the request specifies a bucket using the HTTP Host
|
137
|
+
# header (virtual hosted-style), append the bucket name preceded
|
138
|
+
# by a "/" (e.g., "/bucketname"). For path-style requests and
|
139
|
+
# requests that don't address a bucket, do nothing. For more
|
140
|
+
# information on virtual hosted-style requests, see Virtual
|
141
|
+
# Hosting of Buckets.
|
142
|
+
bucket_name = host.sub(/\.?s3\.amazonaws\.com\Z/, "")
|
143
|
+
string << "/#{bucket_name}" unless bucket_name.empty?
|
144
|
+
|
145
|
+
# 3. Append the path part of the un-decoded HTTP Request-URI,
|
146
|
+
# up-to but not including the query string.
|
147
|
+
uri = URI.parse(request.path)
|
148
|
+
string << uri.path
|
149
|
+
|
150
|
+
# 4. If the request addresses a sub-resource, like ?location,
|
151
|
+
# ?acl, or ?torrent, append the sub-resource including question
|
152
|
+
# mark.
|
153
|
+
string << "?#{$1}" if uri.query =~ /&?(acl|torrent|logging|location)(?:&|=|\Z)/
|
154
|
+
string
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
data/lib/stree.rb
ADDED
@@ -0,0 +1,24 @@
|
|
1
|
+
require "base64"
|
2
|
+
require "digest/md5"
|
3
|
+
require "forwardable"
|
4
|
+
require "net/http"
|
5
|
+
require "net/https"
|
6
|
+
require "openssl"
|
7
|
+
require "rexml/document"
|
8
|
+
require "time"
|
9
|
+
|
10
|
+
require "stree/roxy/moxie"
|
11
|
+
require "stree/roxy/proxy"
|
12
|
+
|
13
|
+
require "stree/parser"
|
14
|
+
require "stree/bucket"
|
15
|
+
require "stree/connection"
|
16
|
+
require "stree/exceptions"
|
17
|
+
require "stree/object"
|
18
|
+
require "stree/service"
|
19
|
+
require "stree/signature"
|
20
|
+
|
21
|
+
module Stree
|
22
|
+
# Default (and only) host serving S3 stuff
|
23
|
+
HOST = "s3.amazonaws.com"
|
24
|
+
end
|
data/test/bucket_test.rb
ADDED
@@ -0,0 +1,231 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class BucketTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@bucket_vhost = Stree::Bucket.new(nil, "data-bucket")
|
6
|
+
@bucket_path = Stree::Bucket.new(nil, "data_bucket")
|
7
|
+
@bucket = @bucket_vhost
|
8
|
+
|
9
|
+
@response_location = Net::HTTPOK.new("1.1", "200", "OK")
|
10
|
+
stub(@response_location).body { @bucket_location_body }
|
11
|
+
@bucket_location = "EU"
|
12
|
+
@bucket_location_body = <<-EOLocation
|
13
|
+
<?xml version="1.0" encoding="UTF-8"?>\n<LocationConstraint xmlns="http://s3.amazonaws.com/doc/2006-03-01/">EU</LocationConstraint>
|
14
|
+
EOLocation
|
15
|
+
|
16
|
+
@reponse_owned_by_you = Net::HTTPConflict.new("1.1", "409", "Conflict")
|
17
|
+
stub(@reponse_owned_by_you).body { @bucket_owned_by_you_body }
|
18
|
+
@bucket_owned_by_you_body = <<-EOOwnedByYou
|
19
|
+
<?xml version="1.0" encoding="UTF-8"?>\n<Error> <Code>BucketAlreadyOwnedByYou</Code> <Message>Your previous request to create the named bucket succeeded and you already own it.</Message> <BucketName>bucket</BucketName> <RequestId>117D08EA0EC6E860</RequestId> <HostId>4VpMSvmJ+G5+DLtVox6O5cZNgdPlYcjCu3l0n4HjDe01vPxxuk5eTAtcAkUynRyV</HostId> </Error>
|
20
|
+
EOOwnedByYou
|
21
|
+
|
22
|
+
@reponse_already_exists = Net::HTTPConflict.new("1.1", "409", "Conflict")
|
23
|
+
stub(@response_already_exists).body { @bucket_already_exists_body }
|
24
|
+
@bucket_already_exists_body = <<-EOAlreadyExists
|
25
|
+
<?xml version="1.0" encoding="UTF-8"?>\n<Error> <Code>BucketAlreadyExists</Code> <Message>The requested bucket name is not available. The bucket namespace is shared by all users of the system. Please select a different name and try again.</Message> <BucketName>bucket</BucketName> <RequestId>4C154D32807C92BD</RequestId> <HostId>/xyHQgXcUXTZQhoO+NUBzbaxbFrIhKlyuaRHFnmcId0bMePvY9Zwg+dyk2LYE4g5</HostId> </Error>
|
26
|
+
EOAlreadyExists
|
27
|
+
|
28
|
+
@objects_list_empty = []
|
29
|
+
@objects_list = [
|
30
|
+
Stree::Object.new(@bucket, "obj1"),
|
31
|
+
Stree::Object.new(@bucket, "obj2")
|
32
|
+
]
|
33
|
+
|
34
|
+
@response_objects_list_empty = Net::HTTPOK.new("1.1", "200", "OK")
|
35
|
+
stub(@response_objects_list_empty).body { @response_objects_list_empty_body }
|
36
|
+
@response_objects_list_empty_body = <<-EOEmpty
|
37
|
+
<?xml version="1.0" encoding="UTF-8"?>\n<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> <Name>bucket</Name> <Prefix></Prefix> <Marker></Marker> <MaxKeys>1000</MaxKeys> <IsTruncated>false</IsTruncated> </ListBucketResult>
|
38
|
+
EOEmpty
|
39
|
+
|
40
|
+
@response_objects_list = Net::HTTPOK.new("1.1", "200", "OK")
|
41
|
+
stub(@response_objects_list).body { @response_objects_list_body }
|
42
|
+
@response_objects_list_body = <<-EOObjects
|
43
|
+
<?xml version="1.0" encoding="UTF-8"?>\n<ListBucketResult xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> <Name>bucket</Name> <Prefix></Prefix> <Marker></Marker> <MaxKeys>1000</MaxKeys> <IsTruncated>false</IsTruncated> <Contents> <Key>obj1</Key> <LastModified>2009-07-03T10:17:33.000Z</LastModified> <ETag>"99519cdf14c255e580e1b7bca85a458c"</ETag> <Size>1729</Size> <Owner> <ID>df864aeb6f42be43f1d9e60aaabe3f15e245b035a4b79d1cfe36c4deaec67205</ID> <DisplayName>owner</DisplayName> </Owner> <StorageClass>STANDARD</StorageClass> </Contents> <Contents> <Key>obj2</Key> <LastModified>2009-07-03T11:17:33.000Z</LastModified> <ETag>"99519cdf14c255e586e1b12bca85a458c"</ETag> <Size>179</Size> <Owner> <ID>df864aeb6f42be43f1d9e60aaabe3f17e247b037a4b79d1cfe36c4deaec67205</ID> <DisplayName>owner</DisplayName> </Owner> <StorageClass>STANDARD</StorageClass> </Contents> </ListBucketResult>
|
44
|
+
EOObjects
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_name_valid
|
48
|
+
assert_raise ArgumentError do Stree::Bucket.new(nil, "") end # should not be valid with empty name
|
49
|
+
assert_raise ArgumentError do Stree::Bucket.new(nil, "10.0.0.1") end # should not be valid with IP as name
|
50
|
+
assert_raise ArgumentError do Stree::Bucket.new(nil, "as") end # should not be valid with name shorter than 3 characters
|
51
|
+
assert_raise ArgumentError do Stree::Bucket.new(nil, "a"*256) end # should not be valid with name longer than 255 characters
|
52
|
+
assert_raise ArgumentError do Stree::Bucket.new(nil, ".asdf") end # should not allow special characters as first character
|
53
|
+
assert_raise ArgumentError do Stree::Bucket.new(nil, "-asdf") end # should not allow special characters as first character
|
54
|
+
assert_raise ArgumentError do Stree::Bucket.new(nil, "_asdf") end # should not allow special characters as first character
|
55
|
+
|
56
|
+
assert_nothing_raised do
|
57
|
+
Stree::Bucket.new(nil, "a-a-")
|
58
|
+
Stree::Bucket.new(nil, "a.a.")
|
59
|
+
Stree::Bucket.new(nil, "a_a_")
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def test_path_prefix
|
64
|
+
expected = ""
|
65
|
+
actual = @bucket_vhost.path_prefix
|
66
|
+
assert_equal expected, actual
|
67
|
+
|
68
|
+
expected = "data_bucket/"
|
69
|
+
actual = @bucket_path.path_prefix
|
70
|
+
assert_equal expected, actual
|
71
|
+
end
|
72
|
+
|
73
|
+
def test_host
|
74
|
+
expected = "data-bucket.s3.amazonaws.com"
|
75
|
+
actual = @bucket_vhost.host
|
76
|
+
assert_equal expected, actual
|
77
|
+
|
78
|
+
expected = "s3.amazonaws.com"
|
79
|
+
actual = @bucket_path.host
|
80
|
+
assert_equal expected, actual
|
81
|
+
end
|
82
|
+
|
83
|
+
def test_vhost
|
84
|
+
assert @bucket_vhost.vhost?
|
85
|
+
assert ! @bucket_path.vhost?
|
86
|
+
end
|
87
|
+
|
88
|
+
def test_exists
|
89
|
+
mock(@bucket).retrieve { @bucket_vhost }
|
90
|
+
assert @bucket.exists?
|
91
|
+
|
92
|
+
mock(@bucket).retrieve { raise Stree::Error::NoSuchBucket.new(nil, nil) }
|
93
|
+
assert ! @bucket.exists?
|
94
|
+
end
|
95
|
+
|
96
|
+
def test_location_and_parse_location
|
97
|
+
mock(@bucket).bucket_request(:get, {:params=>{:location=>nil}}) { @response_location }
|
98
|
+
|
99
|
+
expected = @bucket_location
|
100
|
+
actual = @bucket.location
|
101
|
+
assert_equal expected, actual
|
102
|
+
|
103
|
+
stub(@bucket).bucket_request(:get, {:params=>{:location=>nil}}) { flunk "should deliver from cached result" }
|
104
|
+
actual = @bucket.location
|
105
|
+
assert_equal expected, actual
|
106
|
+
end
|
107
|
+
|
108
|
+
def test_save
|
109
|
+
mock(@bucket).bucket_request(:put, {:headers=>{}}) { }
|
110
|
+
assert @bucket.save
|
111
|
+
# mock ensures that bucket_request was called
|
112
|
+
end
|
113
|
+
|
114
|
+
def test_save_failure_owned_by_you
|
115
|
+
mock(@bucket).bucket_request(:put, {:headers=>{}}) { raise Stree::Error::BucketAlreadyOwnedByYou.new(409, @response_owned_by_you) }
|
116
|
+
assert_raise Stree::Error::BucketAlreadyOwnedByYou do
|
117
|
+
@bucket.save
|
118
|
+
end
|
119
|
+
|
120
|
+
mock(@bucket).bucket_request(:put, {:headers=>{}}) { raise Stree::Error::BucketAlreadyExists.new(409, @response_already_exists) }
|
121
|
+
assert_raise Stree::Error::BucketAlreadyExists do
|
122
|
+
@bucket.save
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
def test_objects
|
127
|
+
mock(@bucket).fetch_objects { @objects_list_empty }
|
128
|
+
expected = @objects_list_empty
|
129
|
+
actual = @bucket.objects
|
130
|
+
assert_equal expected, actual
|
131
|
+
|
132
|
+
stub(@bucket).fetch_objects { flunk "should load objects from cache" }
|
133
|
+
actual = @bucket.objects
|
134
|
+
assert_equal expected, actual
|
135
|
+
|
136
|
+
stub(@bucket).fetch_objects { @objects_list }
|
137
|
+
|
138
|
+
expected = @objects_list
|
139
|
+
actual = @bucket.objects(true)
|
140
|
+
assert_equal expected, actual
|
141
|
+
end
|
142
|
+
|
143
|
+
def test_fetch_objects_and_parse_objects
|
144
|
+
mock(@bucket).bucket_request(:get, :test=>true) { @response_objects_list_empty }
|
145
|
+
expected = @objects_list_empty
|
146
|
+
actual = @bucket.objects.find_all(:test => true)
|
147
|
+
assert_equal expected, actual
|
148
|
+
|
149
|
+
mock(@bucket).bucket_request(:get, :test=>true) { @response_objects_list }
|
150
|
+
expected = @objects_list
|
151
|
+
actual = @bucket.objects.find_all(:test => true)
|
152
|
+
assert_equal expected, actual
|
153
|
+
end
|
154
|
+
|
155
|
+
def test_destroy
|
156
|
+
mock(@bucket).bucket_request(:delete) { }
|
157
|
+
assert @bucket.destroy
|
158
|
+
end
|
159
|
+
|
160
|
+
def test_objects_build
|
161
|
+
stub(@bucket).bucket_request { flunk "should not connect to server" }
|
162
|
+
|
163
|
+
expected = "object_name"
|
164
|
+
actual = @bucket.objects.build("object_name")
|
165
|
+
assert_kind_of Stree::Object, actual
|
166
|
+
assert_equal expected, actual.key
|
167
|
+
end
|
168
|
+
|
169
|
+
def test_objects_find_first
|
170
|
+
assert_nothing_raised do
|
171
|
+
stub.instance_of(Stree::Object).retrieve { Stree::Object.new(nil, "obj2") }
|
172
|
+
expected = "obj2"
|
173
|
+
actual = @bucket.objects.find_first("obj2")
|
174
|
+
assert_equal "obj2", actual.key
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
def test_objects_find_first_fail
|
179
|
+
assert_raise Stree::Error::NoSuchKey do
|
180
|
+
stub.instance_of(Stree::Object).retrieve { raise Stree::Error::NoSuchKey.new(404, nil) }
|
181
|
+
@bucket.objects.find_first("obj3")
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def test_objects_find_all_on_empty_list
|
186
|
+
stub(@bucket).fetch_objects { @objects_list_empty }
|
187
|
+
assert_nothing_raised do
|
188
|
+
expected = @objects_list_empty
|
189
|
+
actual = @bucket.objects.find_all
|
190
|
+
assert_equal expected, actual
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
def test_objects_find_all
|
195
|
+
stub(@bucket).fetch_objects { @objects_list }
|
196
|
+
assert_nothing_raised do
|
197
|
+
expected = @objects_list
|
198
|
+
actual = @bucket.objects.find_all
|
199
|
+
assert_equal expected, actual
|
200
|
+
end
|
201
|
+
end
|
202
|
+
|
203
|
+
def test_objects_reload
|
204
|
+
stub(@bucket).fetch_objects { @objects_list_empty }
|
205
|
+
expected = @objects_list_empty
|
206
|
+
actual = @bucket.objects
|
207
|
+
assert_equal expected, actual
|
208
|
+
|
209
|
+
stub(@bucket).fetch_objects { @objects_list }
|
210
|
+
expected = @objects_list_empty
|
211
|
+
actual = @bucket.objects
|
212
|
+
assert_equal expected, actual
|
213
|
+
|
214
|
+
assert @bucket.objects.reload
|
215
|
+
|
216
|
+
expected = @objects_list
|
217
|
+
actual = @bucket.objects
|
218
|
+
assert_equal expected, actual
|
219
|
+
end
|
220
|
+
|
221
|
+
def test_objects_destroy_all
|
222
|
+
@counter = 0
|
223
|
+
stub(@bucket).fetch_objects { @objects_list }
|
224
|
+
@bucket.objects.each do |obj|
|
225
|
+
mock(obj).destroy { @counter += 1 }
|
226
|
+
end
|
227
|
+
|
228
|
+
@bucket.objects.destroy_all
|
229
|
+
assert_equal @objects_list.length, @counter
|
230
|
+
end
|
231
|
+
end
|
@@ -0,0 +1,164 @@
|
|
1
|
+
require 'test_helper'
|
2
|
+
|
3
|
+
class ConnectionTest < Test::Unit::TestCase
|
4
|
+
def setup
|
5
|
+
@connection = Stree::Connection.new(
|
6
|
+
:access_key_id => "12345678901234567890",
|
7
|
+
:secret_access_key => "qwertyuiopasdfghjklzxcvbnmQWERTYUIOPASDF"
|
8
|
+
)
|
9
|
+
@http_request = Net::HTTP.new("")
|
10
|
+
@response_ok = Net::HTTPOK.new("1.1", "200", "OK")
|
11
|
+
@response_not_found = Net::HTTPNotFound.new("1.1", "404", "Not Found")
|
12
|
+
stub(@connection).http { @http_request }
|
13
|
+
stub(@http_request).start { @response_ok }
|
14
|
+
end
|
15
|
+
|
16
|
+
def test_handle_response_not_modify_response_when_ok
|
17
|
+
assert_nothing_raised do
|
18
|
+
response = @connection.request(
|
19
|
+
:get,
|
20
|
+
:host => "s3.amazonaws.com",
|
21
|
+
:path => "/"
|
22
|
+
)
|
23
|
+
assert_equal @response_ok, response
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def test_handle_response_throws_exception_when_not_ok
|
28
|
+
response_body = <<-EOFakeBody
|
29
|
+
<?xml version=\"1.0\" encoding=\"UTF-8\"?>
|
30
|
+
<SomeResult xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">
|
31
|
+
<Code>NoSuchBucket</Code>
|
32
|
+
<Message>The specified bucket does not exist</Message>
|
33
|
+
</SomeResult>
|
34
|
+
EOFakeBody
|
35
|
+
stub(@http_request).start { @response_not_found }
|
36
|
+
stub(@response_not_found).body { response_body }
|
37
|
+
|
38
|
+
assert_raise Stree::Error::NoSuchBucket do
|
39
|
+
response = @connection.request(
|
40
|
+
:get,
|
41
|
+
:host => "data.example.com.s3.amazonaws.com",
|
42
|
+
:path => "/"
|
43
|
+
)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def test_handle_response_throws_standard_exception_when_not_ok
|
48
|
+
stub(@http_request).start { @response_not_found }
|
49
|
+
stub(@response_not_found).body { nil }
|
50
|
+
assert_raise Stree::Error::ResponseError do
|
51
|
+
response = @connection.request(
|
52
|
+
:get,
|
53
|
+
:host => "data.example.com.s3.amazonaws.com",
|
54
|
+
:path => "/"
|
55
|
+
)
|
56
|
+
end
|
57
|
+
|
58
|
+
stub(@response_not_found).body { "" }
|
59
|
+
assert_raise Stree::Error::ResponseError do
|
60
|
+
response = @connection.request(
|
61
|
+
:get,
|
62
|
+
:host => "data.example.com.s3.amazonaws.com",
|
63
|
+
:path => "/"
|
64
|
+
)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def test_parse_params_empty
|
69
|
+
expected = ""
|
70
|
+
actual = Stree::Connection.parse_params({})
|
71
|
+
assert_equal expected, actual
|
72
|
+
end
|
73
|
+
|
74
|
+
def test_parse_params_only_interesting_params
|
75
|
+
expected = ""
|
76
|
+
actual = Stree::Connection.parse_params(:param1 => "1", :maxkeys => "2")
|
77
|
+
assert_equal expected, actual
|
78
|
+
end
|
79
|
+
|
80
|
+
def test_parse_params_remove_underscore
|
81
|
+
expected = "max-keys=100"
|
82
|
+
actual = Stree::Connection.parse_params(:max_keys => 100)
|
83
|
+
assert_equal expected, actual
|
84
|
+
end
|
85
|
+
|
86
|
+
def test_parse_params_with_and_without_values
|
87
|
+
expected = "max-keys=100&prefix"
|
88
|
+
actual = Stree::Connection.parse_params(:max_keys => 100, :prefix => nil)
|
89
|
+
assert_equal expected, actual
|
90
|
+
end
|
91
|
+
|
92
|
+
def test_headers_headers_empty
|
93
|
+
expected = {}
|
94
|
+
actual = Stree::Connection.parse_headers({})
|
95
|
+
assert_equal expected, actual
|
96
|
+
end
|
97
|
+
|
98
|
+
def test_parse_headers_only_interesting_headers
|
99
|
+
expected = {}
|
100
|
+
actual = Stree::Connection.parse_headers(
|
101
|
+
:accept => "text/*, text/html, text/html;level=1, */*",
|
102
|
+
:accept_charset => "iso-8859-2, unicode-1-1;q=0.8"
|
103
|
+
)
|
104
|
+
assert_equal expected, actual
|
105
|
+
end
|
106
|
+
|
107
|
+
def test_parse_headers_remove_underscore
|
108
|
+
expected = {
|
109
|
+
"content-type" => nil,
|
110
|
+
"x-amz-acl" => nil,
|
111
|
+
"if-modified-since" => nil,
|
112
|
+
"if-unmodified-since" => nil,
|
113
|
+
"if-match" => nil,
|
114
|
+
"if-none-match" => nil,
|
115
|
+
"content-disposition" => nil,
|
116
|
+
"content-encoding" => nil
|
117
|
+
}
|
118
|
+
actual = Stree::Connection.parse_headers(
|
119
|
+
:content_type => nil,
|
120
|
+
:x_amz_acl => nil,
|
121
|
+
:if_modified_since => nil,
|
122
|
+
:if_unmodified_since => nil,
|
123
|
+
:if_match => nil,
|
124
|
+
:if_none_match => nil,
|
125
|
+
:content_disposition => nil,
|
126
|
+
:content_encoding => nil
|
127
|
+
)
|
128
|
+
assert_equal expected, actual
|
129
|
+
end
|
130
|
+
|
131
|
+
def test_parse_headers_with_values
|
132
|
+
expected = {
|
133
|
+
"content-type" => "text/html",
|
134
|
+
"x-amz-acl" => "public-read",
|
135
|
+
"if-modified-since" => "today",
|
136
|
+
"if-unmodified-since" => "tomorrow",
|
137
|
+
"if-match" => "1234",
|
138
|
+
"if-none-match" => "1243",
|
139
|
+
"content-disposition" => "inline",
|
140
|
+
"content-encoding" => "gzip"
|
141
|
+
}
|
142
|
+
actual = Stree::Connection.parse_headers(
|
143
|
+
:content_type => "text/html",
|
144
|
+
:x_amz_acl => "public-read",
|
145
|
+
:if_modified_since => "today",
|
146
|
+
:if_unmodified_since => "tomorrow",
|
147
|
+
:if_match => "1234",
|
148
|
+
:if_none_match => "1243",
|
149
|
+
:content_disposition => "inline",
|
150
|
+
:content_encoding => "gzip"
|
151
|
+
)
|
152
|
+
assert_equal expected, actual
|
153
|
+
end
|
154
|
+
|
155
|
+
def test_parse_headers_with_range
|
156
|
+
expected = {
|
157
|
+
"range" => "bytes=0-100"
|
158
|
+
}
|
159
|
+
actual = Stree::Connection.parse_headers(
|
160
|
+
:range => 0..100
|
161
|
+
)
|
162
|
+
assert_equal expected, actual
|
163
|
+
end
|
164
|
+
end
|