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