aliyun-oss-ruby-sdk 0.4.1
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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +95 -0
- data/README.md +423 -0
- data/examples/aliyun/oss/bucket.rb +144 -0
- data/examples/aliyun/oss/callback.rb +61 -0
- data/examples/aliyun/oss/object.rb +182 -0
- data/examples/aliyun/oss/resumable_download.rb +42 -0
- data/examples/aliyun/oss/resumable_upload.rb +49 -0
- data/examples/aliyun/oss/streaming.rb +124 -0
- data/examples/aliyun/oss/using_sts.rb +48 -0
- data/examples/aliyun/sts/assume_role.rb +59 -0
- data/lib/aliyun_sdk/common.rb +6 -0
- data/lib/aliyun_sdk/common/exception.rb +18 -0
- data/lib/aliyun_sdk/common/logging.rb +46 -0
- data/lib/aliyun_sdk/common/struct.rb +56 -0
- data/lib/aliyun_sdk/oss.rb +16 -0
- data/lib/aliyun_sdk/oss/bucket.rb +661 -0
- data/lib/aliyun_sdk/oss/client.rb +106 -0
- data/lib/aliyun_sdk/oss/config.rb +39 -0
- data/lib/aliyun_sdk/oss/download.rb +255 -0
- data/lib/aliyun_sdk/oss/exception.rb +108 -0
- data/lib/aliyun_sdk/oss/http.rb +338 -0
- data/lib/aliyun_sdk/oss/iterator.rb +92 -0
- data/lib/aliyun_sdk/oss/multipart.rb +74 -0
- data/lib/aliyun_sdk/oss/object.rb +15 -0
- data/lib/aliyun_sdk/oss/protocol.rb +1499 -0
- data/lib/aliyun_sdk/oss/struct.rb +208 -0
- data/lib/aliyun_sdk/oss/upload.rb +238 -0
- data/lib/aliyun_sdk/oss/util.rb +89 -0
- data/lib/aliyun_sdk/sts.rb +9 -0
- data/lib/aliyun_sdk/sts/client.rb +38 -0
- data/lib/aliyun_sdk/sts/config.rb +22 -0
- data/lib/aliyun_sdk/sts/exception.rb +53 -0
- data/lib/aliyun_sdk/sts/protocol.rb +130 -0
- data/lib/aliyun_sdk/sts/struct.rb +64 -0
- data/lib/aliyun_sdk/sts/util.rb +48 -0
- data/lib/aliyun_sdk/version.rb +7 -0
- data/spec/aliyun/oss/bucket_spec.rb +597 -0
- data/spec/aliyun/oss/client/bucket_spec.rb +554 -0
- data/spec/aliyun/oss/client/client_spec.rb +297 -0
- data/spec/aliyun/oss/client/resumable_download_spec.rb +220 -0
- data/spec/aliyun/oss/client/resumable_upload_spec.rb +413 -0
- data/spec/aliyun/oss/http_spec.rb +83 -0
- data/spec/aliyun/oss/multipart_spec.rb +686 -0
- data/spec/aliyun/oss/object_spec.rb +785 -0
- data/spec/aliyun/oss/service_spec.rb +142 -0
- data/spec/aliyun/oss/util_spec.rb +50 -0
- data/spec/aliyun/sts/client_spec.rb +150 -0
- data/spec/aliyun/sts/util_spec.rb +39 -0
- data/tests/config.rb +31 -0
- data/tests/test_content_encoding.rb +54 -0
- data/tests/test_content_type.rb +95 -0
- data/tests/test_custom_headers.rb +70 -0
- data/tests/test_encoding.rb +77 -0
- data/tests/test_large_file.rb +66 -0
- data/tests/test_multipart.rb +97 -0
- data/tests/test_object_acl.rb +49 -0
- data/tests/test_object_key.rb +68 -0
- data/tests/test_object_url.rb +69 -0
- data/tests/test_resumable.rb +40 -0
- metadata +240 -0
@@ -0,0 +1,38 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
module AliyunSDK
|
4
|
+
module STS
|
5
|
+
|
6
|
+
# STS服务的客户端,用于向STS申请临时token。
|
7
|
+
# @example 创建Client
|
8
|
+
# client = Client.new(
|
9
|
+
# :access_key_id => 'access_key_id',
|
10
|
+
# :access_key_secret => 'access_key_secret')
|
11
|
+
# token = client.assume_role('role:arn', 'app')
|
12
|
+
#
|
13
|
+
# policy = Policy.new
|
14
|
+
# policy.allow(['oss:Get*'], ['acs:oss:*:*:my-bucket/*'])
|
15
|
+
# token = client.assume_role('role:arn', 'app', policy, 60)
|
16
|
+
# puts token.to_s
|
17
|
+
class Client
|
18
|
+
|
19
|
+
def initialize(opts)
|
20
|
+
@config = Config.new(opts)
|
21
|
+
@protocol = Protocol.new(@config)
|
22
|
+
end
|
23
|
+
|
24
|
+
# Assume a role
|
25
|
+
# @param role [String] the role arn
|
26
|
+
# @param session [String] the session name
|
27
|
+
# @param policy [STS::Policy] the policy
|
28
|
+
# @param duration [Fixnum] the duration seconds for the
|
29
|
+
# requested token
|
30
|
+
# @return [STS::Token] the sts token
|
31
|
+
def assume_role(role, session, policy = nil, duration = 3600)
|
32
|
+
@protocol.assume_role(role, session, policy, duration)
|
33
|
+
end
|
34
|
+
|
35
|
+
end # Client
|
36
|
+
|
37
|
+
end # STS
|
38
|
+
end # Aliyun
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
module AliyunSDK
|
4
|
+
module STS
|
5
|
+
|
6
|
+
# A place to store various configurations: credentials, api
|
7
|
+
# timeout, retry mechanism, etc
|
8
|
+
class Config < Common::Struct::Base
|
9
|
+
|
10
|
+
attrs :access_key_id, :access_key_secret, :endpoint
|
11
|
+
|
12
|
+
def initialize(opts = {})
|
13
|
+
super(opts)
|
14
|
+
|
15
|
+
@access_key_id = @access_key_id.strip if @access_key_id
|
16
|
+
@access_key_secret = @access_key_secret.strip if @access_key_secret
|
17
|
+
@endpoint = @endpoint.strip if @endpoint
|
18
|
+
end
|
19
|
+
end # Config
|
20
|
+
|
21
|
+
end # STS
|
22
|
+
end # Aliyun
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'nokogiri'
|
4
|
+
|
5
|
+
module AliyunSDK
|
6
|
+
module STS
|
7
|
+
|
8
|
+
# ServerError represents exceptions from the STS
|
9
|
+
# service. i.e. Client receives a HTTP response whose status is
|
10
|
+
# NOT OK. #message provides the error message and #to_s gives
|
11
|
+
# detailed information probably including the STS request id.
|
12
|
+
class ServerError < Common::Exception
|
13
|
+
|
14
|
+
attr_reader :http_code, :error_code, :message, :request_id
|
15
|
+
|
16
|
+
def initialize(response)
|
17
|
+
@http_code = response.code
|
18
|
+
@attrs = {}
|
19
|
+
|
20
|
+
doc = Nokogiri::XML(response.body) do |config|
|
21
|
+
config.options |= Nokogiri::XML::ParseOptions::NOBLANKS
|
22
|
+
end rescue nil
|
23
|
+
|
24
|
+
if doc and doc.root
|
25
|
+
doc.root.children.each do |n|
|
26
|
+
@attrs[n.name] = n.text
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
@error_code = @attrs['Code']
|
31
|
+
@message = @attrs['Message']
|
32
|
+
@request_id = @attrs['RequestId']
|
33
|
+
end
|
34
|
+
|
35
|
+
def message
|
36
|
+
msg = @attrs['Message'] || "UnknownError[#{http_code}]."
|
37
|
+
"#{msg} RequestId: #{request_id}"
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_s
|
41
|
+
@attrs.merge({'HTTPCode' => @http_code}).map do |k, v|
|
42
|
+
[k, v].join(": ")
|
43
|
+
end.join(", ")
|
44
|
+
end
|
45
|
+
end # ServerError
|
46
|
+
|
47
|
+
# ClientError represents client exceptions caused mostly by
|
48
|
+
# invalid parameters.
|
49
|
+
class ClientError < Common::Exception
|
50
|
+
end # ClientError
|
51
|
+
|
52
|
+
end # STS
|
53
|
+
end # Aliyun
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'rest-client'
|
4
|
+
require 'nokogiri'
|
5
|
+
require 'time'
|
6
|
+
|
7
|
+
module AliyunSDK
|
8
|
+
module STS
|
9
|
+
|
10
|
+
# Protocol implements the STS Open API which is low-level. User
|
11
|
+
# should refer to {STS::Client} for normal use.
|
12
|
+
class Protocol
|
13
|
+
|
14
|
+
ENDPOINT = 'https://sts.aliyuncs.com'
|
15
|
+
FORMAT = 'XML'
|
16
|
+
API_VERSION = '2015-04-01'
|
17
|
+
SIGNATURE_METHOD = 'HMAC-SHA1'
|
18
|
+
SIGNATURE_VERSION = '1.0'
|
19
|
+
|
20
|
+
include Common::Logging
|
21
|
+
|
22
|
+
def initialize(config)
|
23
|
+
@config = config
|
24
|
+
end
|
25
|
+
|
26
|
+
# Assume a role
|
27
|
+
# @param role [String] the role arn
|
28
|
+
# @param session [String] the session name
|
29
|
+
# @param policy [STS::Policy] the policy
|
30
|
+
# @param duration [Fixnum] the duration seconds for the
|
31
|
+
# requested token
|
32
|
+
# @return [STS::Token] the sts token
|
33
|
+
def assume_role(role, session, policy = nil, duration = 3600)
|
34
|
+
logger.info("Begin assume role, role: #{role}, session: #{session}, "\
|
35
|
+
"policy: #{policy}, duration: #{duration}")
|
36
|
+
|
37
|
+
params = {
|
38
|
+
'Action' => 'AssumeRole',
|
39
|
+
'RoleArn' => role,
|
40
|
+
'RoleSessionName' => session,
|
41
|
+
'DurationSeconds' => duration.to_s
|
42
|
+
}
|
43
|
+
params.merge!({'Policy' => policy.serialize}) if policy
|
44
|
+
|
45
|
+
body = do_request(params)
|
46
|
+
doc = parse_xml(body)
|
47
|
+
|
48
|
+
creds_node = doc.at_css("Credentials")
|
49
|
+
creds = {
|
50
|
+
session_name: session,
|
51
|
+
access_key_id: get_node_text(creds_node, 'AccessKeyId'),
|
52
|
+
access_key_secret: get_node_text(creds_node, 'AccessKeySecret'),
|
53
|
+
security_token: get_node_text(creds_node, 'SecurityToken'),
|
54
|
+
expiration: get_node_text(
|
55
|
+
creds_node, 'Expiration') { |x| Time.parse(x) },
|
56
|
+
}
|
57
|
+
|
58
|
+
logger.info("Done assume role, creds: #{creds}")
|
59
|
+
|
60
|
+
Token.new(creds)
|
61
|
+
end
|
62
|
+
|
63
|
+
private
|
64
|
+
# Generate a random signature nonce
|
65
|
+
# @return [String] a random string
|
66
|
+
def signature_nonce
|
67
|
+
(rand * 1_000_000_000).to_s
|
68
|
+
end
|
69
|
+
|
70
|
+
# Do HTTP POST request with specified params
|
71
|
+
# @param params [Hash] the parameters to STS
|
72
|
+
# @return [String] the response body
|
73
|
+
# @raise [ServerError] raise errors if the server responds with errors
|
74
|
+
def do_request(params)
|
75
|
+
query = params.merge(
|
76
|
+
{'Format' => FORMAT,
|
77
|
+
'Version' => API_VERSION,
|
78
|
+
'AccessKeyId' => @config.access_key_id,
|
79
|
+
'SignatureMethod' => SIGNATURE_METHOD,
|
80
|
+
'SignatureVersion' => SIGNATURE_VERSION,
|
81
|
+
'SignatureNonce' => signature_nonce,
|
82
|
+
'Timestamp' => Time.now.utc.iso8601})
|
83
|
+
|
84
|
+
signature = Util.get_signature('POST', query, @config.access_key_secret)
|
85
|
+
query.merge!({'Signature' => signature})
|
86
|
+
|
87
|
+
r = RestClient::Request.execute(
|
88
|
+
:method => 'POST',
|
89
|
+
:url => @config.endpoint || ENDPOINT,
|
90
|
+
:payload => query
|
91
|
+
) do |response, request, result, &blk|
|
92
|
+
|
93
|
+
if response.code >= 300
|
94
|
+
e = ServerError.new(response)
|
95
|
+
logger.error(e.to_s)
|
96
|
+
raise e
|
97
|
+
else
|
98
|
+
response.return!(request, result, &blk)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
logger.debug("Received HTTP response, code: #{r.code}, headers: "\
|
103
|
+
"#{r.headers}, body: #{r.body}")
|
104
|
+
r.body
|
105
|
+
end
|
106
|
+
|
107
|
+
# Parse body content to xml document
|
108
|
+
# @param content [String] the xml content
|
109
|
+
# @return [Nokogiri::XML::Document] the parsed document
|
110
|
+
def parse_xml(content)
|
111
|
+
doc = Nokogiri::XML(content) do |config|
|
112
|
+
config.options |= Nokogiri::XML::ParseOptions::NOBLANKS
|
113
|
+
end
|
114
|
+
|
115
|
+
doc
|
116
|
+
end
|
117
|
+
|
118
|
+
# Get the text of a xml node
|
119
|
+
# @param node [Nokogiri::XML::Node] the xml node
|
120
|
+
# @param tag [String] the node tag
|
121
|
+
# @yield [String] the node text is given to the block
|
122
|
+
def get_node_text(node, tag, &block)
|
123
|
+
n = node.at_css(tag) if node
|
124
|
+
value = n.text if n
|
125
|
+
block && value ? yield(value) : value
|
126
|
+
end
|
127
|
+
|
128
|
+
end # Protocol
|
129
|
+
end # STS
|
130
|
+
end # Aliyun
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'cgi'
|
5
|
+
|
6
|
+
module AliyunSDK
|
7
|
+
module STS
|
8
|
+
|
9
|
+
# STS Policy. Referer to
|
10
|
+
# https://help.aliyun.com/document_detail/ram/ram-user-guide/policy_reference/struct_def.html for details.
|
11
|
+
class Policy < Common::Struct::Base
|
12
|
+
VERSION = '1'
|
13
|
+
|
14
|
+
attrs :rules
|
15
|
+
|
16
|
+
# Add an 'Allow' rule
|
17
|
+
# @param actions [Array<String>] actions of the rule. e.g.:
|
18
|
+
# oss:GetObject, oss:Get*, oss:*
|
19
|
+
# @param resources [Array<String>] resources of the rule. e.g.:
|
20
|
+
# acs:oss:*:*:my-bucket, acs:oss:*:*:my-bucket/*, acs:oss:*:*:*
|
21
|
+
def allow(actions, resources)
|
22
|
+
add_rule(true, actions, resources)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Add an 'Deny' rule
|
26
|
+
# @param actions [Array<String>] actions of the rule. e.g.:
|
27
|
+
# oss:GetObject, oss:Get*, oss:*
|
28
|
+
# @param resources [Array<String>] resources of the rule. e.g.:
|
29
|
+
# acs:oss:*:*:my-bucket, acs:oss:*:*:my-bucket/*, acs:oss:*:*:*
|
30
|
+
def deny(actions, resources)
|
31
|
+
add_rule(false, actions, resources)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Serialize to rule to string
|
35
|
+
def serialize
|
36
|
+
{'Version' => VERSION, 'Statement' => @rules}.to_json
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def add_rule(allow, actions, resources)
|
41
|
+
@rules ||= []
|
42
|
+
@rules << {
|
43
|
+
'Effect' => allow ? 'Allow' : 'Deny',
|
44
|
+
'Action' => actions,
|
45
|
+
'Resource' => resources
|
46
|
+
}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# STS token. User may use the credentials included to access
|
51
|
+
# Alicloud resources(OSS, OTS, etc).
|
52
|
+
# Attributes:
|
53
|
+
# * access_key_id [String] the AccessKeyId
|
54
|
+
# * access_key_secret [String] the AccessKeySecret
|
55
|
+
# * security_token [String] the SecurityToken
|
56
|
+
# * expiration [Time] the time when the token will be expired
|
57
|
+
# * session_name [String] the session name for this token
|
58
|
+
class Token < Common::Struct::Base
|
59
|
+
attrs :access_key_id, :access_key_secret,
|
60
|
+
:security_token, :expiration, :session_name
|
61
|
+
end
|
62
|
+
|
63
|
+
end # STS
|
64
|
+
end # Aliyun
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
require 'cgi'
|
5
|
+
require 'base64'
|
6
|
+
require 'openssl'
|
7
|
+
require 'digest/md5'
|
8
|
+
|
9
|
+
module AliyunSDK
|
10
|
+
module STS
|
11
|
+
##
|
12
|
+
# Util functions to help generate formatted Date, signatures,
|
13
|
+
# etc.
|
14
|
+
#
|
15
|
+
module Util
|
16
|
+
|
17
|
+
class << self
|
18
|
+
|
19
|
+
include Common::Logging
|
20
|
+
|
21
|
+
# Calculate request signatures
|
22
|
+
def get_signature(verb, params, key)
|
23
|
+
logger.debug("Sign, verb: #{verb}, params: #{params}")
|
24
|
+
|
25
|
+
cano_query = params.sort.map {
|
26
|
+
|k, v| [CGI.escape(k), CGI.escape(v)].join('=') }.join('&')
|
27
|
+
|
28
|
+
string_to_sign =
|
29
|
+
verb + '&' + CGI.escape('/') + '&' + CGI.escape(cano_query)
|
30
|
+
|
31
|
+
logger.debug("String to sign: #{string_to_sign}")
|
32
|
+
|
33
|
+
Util.sign(key + '&', string_to_sign)
|
34
|
+
end
|
35
|
+
|
36
|
+
# Sign a string using HMAC and BASE64
|
37
|
+
# @param [String] key the secret key
|
38
|
+
# @param [String] string_to_sign the string to sign
|
39
|
+
# @return [String] the signature
|
40
|
+
def sign(key, string_to_sign)
|
41
|
+
Base64.strict_encode64(
|
42
|
+
OpenSSL::HMAC.digest('sha1', key, string_to_sign))
|
43
|
+
end
|
44
|
+
|
45
|
+
end # self
|
46
|
+
end # Util
|
47
|
+
end # STS
|
48
|
+
end # Aliyun
|
@@ -0,0 +1,597 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
require 'spec_helper'
|
4
|
+
require 'yaml'
|
5
|
+
require 'nokogiri'
|
6
|
+
|
7
|
+
module AliyunSDK
|
8
|
+
module OSS
|
9
|
+
|
10
|
+
describe "Bucket" do
|
11
|
+
|
12
|
+
before :all do
|
13
|
+
@endpoint = 'oss.aliyuncs.com'
|
14
|
+
@protocol = Protocol.new(
|
15
|
+
Config.new(:endpoint => @endpoint,
|
16
|
+
:access_key_id => 'xxx', :access_key_secret => 'yyy'))
|
17
|
+
@bucket = 'rubysdk-bucket'
|
18
|
+
end
|
19
|
+
|
20
|
+
def request_path
|
21
|
+
@bucket + "." + @endpoint
|
22
|
+
end
|
23
|
+
|
24
|
+
def mock_location(location)
|
25
|
+
Nokogiri::XML::Builder.new do |xml|
|
26
|
+
xml.CreateBucketConfiguration {
|
27
|
+
xml.LocationConstraint location
|
28
|
+
}
|
29
|
+
end.to_xml
|
30
|
+
end
|
31
|
+
|
32
|
+
def mock_objects(objects, more = {})
|
33
|
+
Nokogiri::XML::Builder.new do |xml|
|
34
|
+
xml.ListBucketResult {
|
35
|
+
{
|
36
|
+
:prefix => 'Prefix',
|
37
|
+
:delimiter => 'Delimiter',
|
38
|
+
:limit => 'MaxKeys',
|
39
|
+
:marker => 'Marker',
|
40
|
+
:next_marker => 'NextMarker',
|
41
|
+
:truncated => 'IsTruncated',
|
42
|
+
:encoding => 'EncodingType'
|
43
|
+
}.map do |k, v|
|
44
|
+
xml.send(v, more[k]) if more[k]
|
45
|
+
end
|
46
|
+
|
47
|
+
objects.each do |o|
|
48
|
+
xml.Contents {
|
49
|
+
xml.Key o
|
50
|
+
xml.LastModified Time.now.to_s
|
51
|
+
xml.Type 'Normal'
|
52
|
+
xml.Size 1024
|
53
|
+
xml.StorageClass 'Standard'
|
54
|
+
xml.Etag 'etag'
|
55
|
+
xml.Owner {
|
56
|
+
xml.ID '10086'
|
57
|
+
xml.DisplayName 'CMCC'
|
58
|
+
}
|
59
|
+
}
|
60
|
+
end
|
61
|
+
|
62
|
+
(more[:common_prefixes] || []).each do |p|
|
63
|
+
xml.CommonPrefixes {
|
64
|
+
xml.Prefix p
|
65
|
+
}
|
66
|
+
end
|
67
|
+
}
|
68
|
+
end.to_xml
|
69
|
+
end
|
70
|
+
|
71
|
+
def mock_acl(acl)
|
72
|
+
Nokogiri::XML::Builder.new do |xml|
|
73
|
+
xml.AccessControlPolicy {
|
74
|
+
xml.Owner {
|
75
|
+
xml.ID 'owner_id'
|
76
|
+
xml.DisplayName 'owner_name'
|
77
|
+
}
|
78
|
+
|
79
|
+
xml.AccessControlList {
|
80
|
+
xml.Grant acl
|
81
|
+
}
|
82
|
+
}
|
83
|
+
end.to_xml
|
84
|
+
end
|
85
|
+
|
86
|
+
def mock_logging(opts)
|
87
|
+
Nokogiri::XML::Builder.new do |xml|
|
88
|
+
xml.BucketLoggingStatus {
|
89
|
+
if opts.enabled?
|
90
|
+
xml.LoggingEnabled {
|
91
|
+
xml.TargetBucket opts.target_bucket
|
92
|
+
xml.TargetPrefix opts.target_prefix
|
93
|
+
}
|
94
|
+
end
|
95
|
+
}
|
96
|
+
end.to_xml
|
97
|
+
end
|
98
|
+
|
99
|
+
def mock_website(opts)
|
100
|
+
Nokogiri::XML::Builder.new do |xml|
|
101
|
+
xml.WebsiteConfiguration {
|
102
|
+
xml.IndexDocument {
|
103
|
+
xml.Suffix opts.index
|
104
|
+
}
|
105
|
+
if opts.error
|
106
|
+
xml.ErrorDocument {
|
107
|
+
xml.Key opts.error
|
108
|
+
}
|
109
|
+
end
|
110
|
+
}
|
111
|
+
end.to_xml
|
112
|
+
end
|
113
|
+
|
114
|
+
def mock_referer(opts)
|
115
|
+
Nokogiri::XML::Builder.new do |xml|
|
116
|
+
xml.RefererConfiguration {
|
117
|
+
xml.AllowEmptyReferer opts.allow_empty?
|
118
|
+
xml.RefererList {
|
119
|
+
opts.whitelist.each do |r|
|
120
|
+
xml.Referer r
|
121
|
+
end
|
122
|
+
}
|
123
|
+
}
|
124
|
+
end.to_xml
|
125
|
+
end
|
126
|
+
|
127
|
+
def mock_lifecycle(rules)
|
128
|
+
Nokogiri::XML::Builder.new do |xml|
|
129
|
+
xml.LifecycleConfiguration {
|
130
|
+
rules.each do |r|
|
131
|
+
xml.Rule {
|
132
|
+
xml.ID r.id if r.id
|
133
|
+
xml.Status r.enabled? ? 'Enabled' : 'Disabled'
|
134
|
+
xml.Prefix r.prefix
|
135
|
+
xml.Expiration {
|
136
|
+
if r.expiry.is_a?(Date)
|
137
|
+
xml.Date Time.utc(r.expiry.year, r.expiry.month, r.expiry.day)
|
138
|
+
.iso8601.sub('Z', '.000Z')
|
139
|
+
else
|
140
|
+
xml.Days r.expiry.to_i
|
141
|
+
end
|
142
|
+
}
|
143
|
+
}
|
144
|
+
end
|
145
|
+
}
|
146
|
+
end.to_xml
|
147
|
+
end
|
148
|
+
|
149
|
+
def mock_cors(rules)
|
150
|
+
Nokogiri::XML::Builder.new do |xml|
|
151
|
+
xml.CORSConfiguration {
|
152
|
+
rules.each do |r|
|
153
|
+
xml.CORSRule {
|
154
|
+
r.allowed_origins.each do |x|
|
155
|
+
xml.AllowedOrigin x
|
156
|
+
end
|
157
|
+
r.allowed_methods.each do |x|
|
158
|
+
xml.AllowedMethod x
|
159
|
+
end
|
160
|
+
r.allowed_headers.each do |x|
|
161
|
+
xml.AllowedHeader x
|
162
|
+
end
|
163
|
+
r.expose_headers.each do |x|
|
164
|
+
xml.ExposeHeader x
|
165
|
+
end
|
166
|
+
xml.MaxAgeSeconds r.max_age_seconds if r.max_age_seconds
|
167
|
+
}
|
168
|
+
end
|
169
|
+
}
|
170
|
+
end.to_xml
|
171
|
+
end
|
172
|
+
|
173
|
+
def mock_error(code, message)
|
174
|
+
Nokogiri::XML::Builder.new do |xml|
|
175
|
+
xml.Error {
|
176
|
+
xml.Code code
|
177
|
+
xml.Message message
|
178
|
+
xml.RequestId '0000'
|
179
|
+
}
|
180
|
+
end.to_xml
|
181
|
+
end
|
182
|
+
|
183
|
+
def err(msg, reqid = '0000')
|
184
|
+
"#{msg} RequestId: #{reqid}"
|
185
|
+
end
|
186
|
+
|
187
|
+
context "Create bucket" do
|
188
|
+
|
189
|
+
it "should PUT to create bucket" do
|
190
|
+
stub_request(:put, request_path)
|
191
|
+
|
192
|
+
@protocol.create_bucket(@bucket)
|
193
|
+
|
194
|
+
expect(WebMock).to have_requested(:put, request_path)
|
195
|
+
.with(:body => nil, :query => {})
|
196
|
+
end
|
197
|
+
|
198
|
+
it "should set location when create bucket" do
|
199
|
+
location = 'oss-cn-hangzhou'
|
200
|
+
|
201
|
+
stub_request(:put, request_path).with(:body => mock_location(location))
|
202
|
+
|
203
|
+
@protocol.create_bucket(@bucket, :location => 'oss-cn-hangzhou')
|
204
|
+
|
205
|
+
expect(WebMock).to have_requested(:put, request_path)
|
206
|
+
.with(:body => mock_location(location), :query => {})
|
207
|
+
end
|
208
|
+
end # create bucket
|
209
|
+
|
210
|
+
context "List objects" do
|
211
|
+
|
212
|
+
it "should list all objects" do
|
213
|
+
stub_request(:get, request_path)
|
214
|
+
|
215
|
+
@protocol.list_objects(@bucket)
|
216
|
+
|
217
|
+
expect(WebMock).to have_requested(:get, request_path)
|
218
|
+
.with(:body => nil, :query => {})
|
219
|
+
end
|
220
|
+
|
221
|
+
it "should parse object response" do
|
222
|
+
return_objects = ['hello', 'world', 'foo/bar']
|
223
|
+
stub_request(:get, request_path)
|
224
|
+
.to_return(:body => mock_objects(return_objects))
|
225
|
+
|
226
|
+
objects, more = @protocol.list_objects(@bucket)
|
227
|
+
|
228
|
+
expect(WebMock).to have_requested(:get, request_path)
|
229
|
+
.with(:body => nil, :query => {})
|
230
|
+
|
231
|
+
expect(objects.map {|o| o.key}).to match_array(return_objects)
|
232
|
+
expect(more).to be_empty
|
233
|
+
end
|
234
|
+
|
235
|
+
it "should list objects with prefix & delimiter" do
|
236
|
+
# Webmock cannot capture the request_path encoded query parameters,
|
237
|
+
# so we use 'foo-bar' instead of 'foo/bar' to work around
|
238
|
+
# the problem
|
239
|
+
opts = {
|
240
|
+
:marker => 'foo-bar',
|
241
|
+
:prefix => 'foo-',
|
242
|
+
:delimiter => '-',
|
243
|
+
:limit => 10,
|
244
|
+
:encoding => KeyEncoding::URL}
|
245
|
+
|
246
|
+
query = opts.clone
|
247
|
+
query['max-keys'] = query.delete(:limit)
|
248
|
+
query['encoding-type'] = query.delete(:encoding)
|
249
|
+
|
250
|
+
stub_request(:get, request_path).with(:query => query)
|
251
|
+
|
252
|
+
@protocol.list_objects(@bucket, opts)
|
253
|
+
|
254
|
+
expect(WebMock).to have_requested(:get, request_path)
|
255
|
+
.with(:body => "", :query => query)
|
256
|
+
end
|
257
|
+
|
258
|
+
it "should parse object and common prefixes response" do
|
259
|
+
return_objects = ['hello', 'world', 'foo-bar']
|
260
|
+
return_more = {
|
261
|
+
:marker => 'foo-bar',
|
262
|
+
:prefix => 'foo-',
|
263
|
+
:delimiter => '-',
|
264
|
+
:limit => 10,
|
265
|
+
:encoding => KeyEncoding::URL,
|
266
|
+
:next_marker => 'foo-xxx',
|
267
|
+
:truncated => true,
|
268
|
+
:common_prefixes => ['foo/bar/', 'foo/xxx/']
|
269
|
+
}
|
270
|
+
|
271
|
+
opts = {
|
272
|
+
:marker => 'foo-bar',
|
273
|
+
:prefix => 'foo-',
|
274
|
+
:delimiter => '-',
|
275
|
+
:limit => 10,
|
276
|
+
:encoding => KeyEncoding::URL
|
277
|
+
}
|
278
|
+
|
279
|
+
query = opts.clone
|
280
|
+
query['max-keys'] = query.delete(:limit)
|
281
|
+
query['encoding-type'] = query.delete(:encoding)
|
282
|
+
|
283
|
+
stub_request(:get, request_path).with(:query => query).
|
284
|
+
to_return(:body => mock_objects(return_objects, return_more))
|
285
|
+
|
286
|
+
objects, more = @protocol.list_objects(@bucket, opts)
|
287
|
+
|
288
|
+
expect(WebMock).to have_requested(:get, request_path)
|
289
|
+
.with(:body => nil, :query => query)
|
290
|
+
|
291
|
+
expect(objects.map {|o| o.key}).to match_array(return_objects)
|
292
|
+
expect(more).to eq(return_more)
|
293
|
+
end
|
294
|
+
|
295
|
+
it "should decode object key" do
|
296
|
+
return_objects = ['中国のruby', 'world', 'foo/bar']
|
297
|
+
return_more = {
|
298
|
+
:marker => '杭州のruby',
|
299
|
+
:prefix => 'foo-',
|
300
|
+
:delimiter => '分隔のruby',
|
301
|
+
:limit => 10,
|
302
|
+
:encoding => KeyEncoding::URL,
|
303
|
+
:next_marker => '西湖のruby',
|
304
|
+
:truncated => true,
|
305
|
+
:common_prefixes => ['玉泉のruby', '苏堤のruby']
|
306
|
+
}
|
307
|
+
|
308
|
+
es_objects = [CGI.escape('中国のruby'), 'world', 'foo/bar']
|
309
|
+
es_more = {
|
310
|
+
:marker => CGI.escape('杭州のruby'),
|
311
|
+
:prefix => 'foo-',
|
312
|
+
:delimiter => CGI.escape('分隔のruby'),
|
313
|
+
:limit => 10,
|
314
|
+
:encoding => KeyEncoding::URL,
|
315
|
+
:next_marker => CGI.escape('西湖のruby'),
|
316
|
+
:truncated => true,
|
317
|
+
:common_prefixes => [CGI.escape('玉泉のruby'), CGI.escape('苏堤のruby')]
|
318
|
+
}
|
319
|
+
|
320
|
+
stub_request(:get, request_path)
|
321
|
+
.to_return(:body => mock_objects(es_objects, es_more))
|
322
|
+
|
323
|
+
objects, more = @protocol.list_objects(@bucket)
|
324
|
+
|
325
|
+
expect(WebMock).to have_requested(:get, request_path)
|
326
|
+
.with(:body => nil, :query => {})
|
327
|
+
|
328
|
+
expect(objects.map {|o| o.key}).to match_array(return_objects)
|
329
|
+
expect(more).to eq(return_more)
|
330
|
+
end
|
331
|
+
end # list objects
|
332
|
+
|
333
|
+
context "Delete bucket" do
|
334
|
+
|
335
|
+
it "should send DELETE reqeust" do
|
336
|
+
stub_request(:delete, request_path)
|
337
|
+
|
338
|
+
@protocol.delete_bucket(@bucket)
|
339
|
+
|
340
|
+
expect(WebMock).to have_requested(:delete, request_path)
|
341
|
+
.with(:body => nil, :query => {})
|
342
|
+
end
|
343
|
+
|
344
|
+
it "should raise Exception on error" do
|
345
|
+
code = "NoSuchBucket"
|
346
|
+
message = "The bucket to delete does not exist."
|
347
|
+
|
348
|
+
stub_request(:delete, request_path).to_return(
|
349
|
+
:status => 404, :body => mock_error(code, message))
|
350
|
+
|
351
|
+
expect {
|
352
|
+
@protocol.delete_bucket(@bucket)
|
353
|
+
}.to raise_error(ServerError, err(message))
|
354
|
+
end
|
355
|
+
end # delete bucket
|
356
|
+
|
357
|
+
context "acl, logging, website, referer, lifecycle" do
|
358
|
+
it "should update acl" do
|
359
|
+
query = {'acl' => ''}
|
360
|
+
stub_request(:put, request_path).with(:query => query)
|
361
|
+
|
362
|
+
@protocol.put_bucket_acl(@bucket, ACL::PUBLIC_READ)
|
363
|
+
|
364
|
+
expect(WebMock).to have_requested(:put, request_path)
|
365
|
+
.with(:query => query, :body => nil)
|
366
|
+
end
|
367
|
+
|
368
|
+
it "should get acl" do
|
369
|
+
query = {'acl' => ''}
|
370
|
+
return_acl = ACL::PUBLIC_READ
|
371
|
+
stub_request(:get, request_path)
|
372
|
+
.with(:query => query)
|
373
|
+
.to_return(:body => mock_acl(return_acl))
|
374
|
+
|
375
|
+
acl = @protocol.get_bucket_acl(@bucket)
|
376
|
+
|
377
|
+
expect(WebMock).to have_requested(:get, request_path)
|
378
|
+
.with(:query => query, :body => nil)
|
379
|
+
expect(acl).to eq(return_acl)
|
380
|
+
end
|
381
|
+
|
382
|
+
it "should enable logging" do
|
383
|
+
query = {'logging' => ''}
|
384
|
+
stub_request(:put, request_path).with(:query => query)
|
385
|
+
|
386
|
+
logging_opts = BucketLogging.new(
|
387
|
+
:enable => true,
|
388
|
+
:target_bucket => 'target-bucket', :target_prefix => 'foo')
|
389
|
+
@protocol.put_bucket_logging(@bucket, logging_opts)
|
390
|
+
|
391
|
+
expect(WebMock).to have_requested(:put, request_path)
|
392
|
+
.with(:query => query, :body => mock_logging(logging_opts))
|
393
|
+
end
|
394
|
+
|
395
|
+
it "should disable logging" do
|
396
|
+
query = {'logging' => ''}
|
397
|
+
stub_request(:put, request_path).with(:query => query)
|
398
|
+
|
399
|
+
logging_opts = BucketLogging.new(:enable => false)
|
400
|
+
@protocol.put_bucket_logging(@bucket, logging_opts)
|
401
|
+
|
402
|
+
expect(WebMock).to have_requested(:put, request_path)
|
403
|
+
.with(:query => query, :body => mock_logging(logging_opts))
|
404
|
+
end
|
405
|
+
|
406
|
+
it "should get logging" do
|
407
|
+
query = {'logging' => ''}
|
408
|
+
logging_opts = BucketLogging.new(
|
409
|
+
:enable => true,
|
410
|
+
:target_bucket => 'target-bucket', :target_prefix => 'foo')
|
411
|
+
|
412
|
+
stub_request(:get, request_path)
|
413
|
+
.with(:query => query)
|
414
|
+
.to_return(:body => mock_logging(logging_opts))
|
415
|
+
|
416
|
+
logging = @protocol.get_bucket_logging(@bucket)
|
417
|
+
|
418
|
+
expect(WebMock).to have_requested(:get, request_path)
|
419
|
+
.with(:query => query, :body => nil)
|
420
|
+
expect(logging.to_s).to eq(logging_opts.to_s)
|
421
|
+
end
|
422
|
+
|
423
|
+
it "should delete logging" do
|
424
|
+
query = {'logging' => ''}
|
425
|
+
stub_request(:delete, request_path).with(:query => query)
|
426
|
+
|
427
|
+
@protocol.delete_bucket_logging(@bucket)
|
428
|
+
|
429
|
+
expect(WebMock).to have_requested(:delete, request_path)
|
430
|
+
.with(:query => query, :body => nil)
|
431
|
+
end
|
432
|
+
|
433
|
+
it "should update website" do
|
434
|
+
query = {'website' => ''}
|
435
|
+
stub_request(:put, request_path).with(:query => query)
|
436
|
+
|
437
|
+
website_opts = BucketWebsite.new(
|
438
|
+
:enable => true, :index => 'index.html', :error => 'error.html')
|
439
|
+
@protocol.put_bucket_website(@bucket, website_opts)
|
440
|
+
|
441
|
+
expect(WebMock).to have_requested(:put, request_path)
|
442
|
+
.with(:query => query, :body => mock_website(website_opts))
|
443
|
+
end
|
444
|
+
|
445
|
+
it "should get website" do
|
446
|
+
query = {'website' => ''}
|
447
|
+
website_opts = BucketWebsite.new(
|
448
|
+
:enable => true, :index => 'index.html', :error => 'error.html')
|
449
|
+
|
450
|
+
stub_request(:get, request_path)
|
451
|
+
.with(:query => query)
|
452
|
+
.to_return(:body => mock_website(website_opts))
|
453
|
+
|
454
|
+
opts = @protocol.get_bucket_website(@bucket)
|
455
|
+
|
456
|
+
expect(WebMock).to have_requested(:get, request_path)
|
457
|
+
.with(:query => query, :body => nil)
|
458
|
+
expect(opts.to_s).to eq(website_opts.to_s)
|
459
|
+
end
|
460
|
+
|
461
|
+
it "should delete website" do
|
462
|
+
query = {'website' => ''}
|
463
|
+
stub_request(:delete, request_path).with(:query => query)
|
464
|
+
|
465
|
+
@protocol.delete_bucket_website(@bucket)
|
466
|
+
|
467
|
+
expect(WebMock).to have_requested(:delete, request_path)
|
468
|
+
.with(:query => query, :body => nil)
|
469
|
+
end
|
470
|
+
|
471
|
+
it "should update referer" do
|
472
|
+
query = {'referer' => ''}
|
473
|
+
stub_request(:put, request_path).with(:query => query)
|
474
|
+
|
475
|
+
referer_opts = BucketReferer.new(
|
476
|
+
:allow_empty => true, :whitelist => ['xxx', 'yyy'])
|
477
|
+
@protocol.put_bucket_referer(@bucket, referer_opts)
|
478
|
+
|
479
|
+
expect(WebMock).to have_requested(:put, request_path)
|
480
|
+
.with(:query => query, :body => mock_referer(referer_opts))
|
481
|
+
end
|
482
|
+
|
483
|
+
it "should get referer" do
|
484
|
+
query = {'referer' => ''}
|
485
|
+
referer_opts = BucketReferer.new(
|
486
|
+
:allow_empty => true, :whitelist => ['xxx', 'yyy'])
|
487
|
+
|
488
|
+
stub_request(:get, request_path)
|
489
|
+
.with(:query => query)
|
490
|
+
.to_return(:body => mock_referer(referer_opts))
|
491
|
+
|
492
|
+
opts = @protocol.get_bucket_referer(@bucket)
|
493
|
+
|
494
|
+
expect(WebMock).to have_requested(:get, request_path)
|
495
|
+
.with(:query => query, :body => nil)
|
496
|
+
expect(opts.to_s).to eq(referer_opts.to_s)
|
497
|
+
end
|
498
|
+
|
499
|
+
it "should update lifecycle" do
|
500
|
+
query = {'lifecycle' => ''}
|
501
|
+
stub_request(:put, request_path).with(:query => query)
|
502
|
+
|
503
|
+
rules = (1..5).map do |i|
|
504
|
+
LifeCycleRule.new(
|
505
|
+
:id => i, :enable => i % 2 == 0, :prefix => "foo#{i}",
|
506
|
+
:expiry => (i % 2 == 1 ? Date.today : 10 + i))
|
507
|
+
end
|
508
|
+
|
509
|
+
@protocol.put_bucket_lifecycle(@bucket, rules)
|
510
|
+
|
511
|
+
expect(WebMock).to have_requested(:put, request_path)
|
512
|
+
.with(:query => query, :body => mock_lifecycle(rules))
|
513
|
+
end
|
514
|
+
|
515
|
+
it "should get lifecycle" do
|
516
|
+
query = {'lifecycle' => ''}
|
517
|
+
return_rules = (1..5).map do |i|
|
518
|
+
LifeCycleRule.new(
|
519
|
+
:id => i, :enable => i % 2 == 0, :prefix => "foo#{i}",
|
520
|
+
:expiry => (i % 2 == 1 ? Date.today : 10 + i))
|
521
|
+
end
|
522
|
+
|
523
|
+
stub_request(:get, request_path)
|
524
|
+
.with(:query => query)
|
525
|
+
.to_return(:body => mock_lifecycle(return_rules))
|
526
|
+
|
527
|
+
rules = @protocol.get_bucket_lifecycle(@bucket)
|
528
|
+
|
529
|
+
expect(WebMock).to have_requested(:get, request_path)
|
530
|
+
.with(:query => query, :body => nil)
|
531
|
+
expect(rules.map(&:to_s)).to match_array(return_rules.map(&:to_s))
|
532
|
+
end
|
533
|
+
|
534
|
+
it "should delete lifecycle" do
|
535
|
+
query = {'lifecycle' => ''}
|
536
|
+
stub_request(:delete, request_path).with(:query => query)
|
537
|
+
|
538
|
+
@protocol.delete_bucket_lifecycle(@bucket)
|
539
|
+
|
540
|
+
expect(WebMock).to have_requested(:delete, request_path)
|
541
|
+
.with(:query => query, :body => nil)
|
542
|
+
end
|
543
|
+
|
544
|
+
it "should set cors" do
|
545
|
+
query = {'cors' => ''}
|
546
|
+
stub_request(:put, request_path).with(:query => query)
|
547
|
+
|
548
|
+
rules = (1..5).map do |i|
|
549
|
+
CORSRule.new(
|
550
|
+
:allowed_origins => (1..3).map {|x| "origin-#{x}"},
|
551
|
+
:allowed_methods => ['PUT', 'GET'],
|
552
|
+
:allowed_headers => (1..3).map {|x| "header-#{x}"},
|
553
|
+
:expose_headers => (1..3).map {|x| "header-#{x}"})
|
554
|
+
end
|
555
|
+
@protocol.set_bucket_cors(@bucket, rules)
|
556
|
+
|
557
|
+
expect(WebMock).to have_requested(:put, request_path)
|
558
|
+
.with(:query => query, :body => mock_cors(rules))
|
559
|
+
end
|
560
|
+
|
561
|
+
it "should get cors" do
|
562
|
+
query = {'cors' => ''}
|
563
|
+
return_rules = (1..5).map do |i|
|
564
|
+
CORSRule.new(
|
565
|
+
:allowed_origins => (1..3).map {|x| "origin-#{x}"},
|
566
|
+
:allowed_methods => ['PUT', 'GET'],
|
567
|
+
:allowed_headers => (1..3).map {|x| "header-#{x}"},
|
568
|
+
:expose_headers => (1..3).map {|x| "header-#{x}"})
|
569
|
+
end
|
570
|
+
|
571
|
+
stub_request(:get, request_path)
|
572
|
+
.with(:query => query)
|
573
|
+
.to_return(:body => mock_cors(return_rules))
|
574
|
+
|
575
|
+
rules = @protocol.get_bucket_cors(@bucket)
|
576
|
+
|
577
|
+
expect(WebMock).to have_requested(:get, request_path)
|
578
|
+
.with(:query => query, :body => nil)
|
579
|
+
expect(rules.map(&:to_s)).to match_array(return_rules.map(&:to_s))
|
580
|
+
end
|
581
|
+
|
582
|
+
it "should delete cors" do
|
583
|
+
query = {'cors' => ''}
|
584
|
+
|
585
|
+
stub_request(:delete, request_path).with(:query => query)
|
586
|
+
|
587
|
+
@protocol.delete_bucket_cors(@bucket)
|
588
|
+
expect(WebMock).to have_requested(:delete, request_path)
|
589
|
+
.with(:query => query, :body => nil)
|
590
|
+
end
|
591
|
+
|
592
|
+
end # acl, logging, cors, etc
|
593
|
+
|
594
|
+
end # Bucket
|
595
|
+
|
596
|
+
end # OSS
|
597
|
+
end # Aliyun
|