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.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +95 -0
  3. data/README.md +423 -0
  4. data/examples/aliyun/oss/bucket.rb +144 -0
  5. data/examples/aliyun/oss/callback.rb +61 -0
  6. data/examples/aliyun/oss/object.rb +182 -0
  7. data/examples/aliyun/oss/resumable_download.rb +42 -0
  8. data/examples/aliyun/oss/resumable_upload.rb +49 -0
  9. data/examples/aliyun/oss/streaming.rb +124 -0
  10. data/examples/aliyun/oss/using_sts.rb +48 -0
  11. data/examples/aliyun/sts/assume_role.rb +59 -0
  12. data/lib/aliyun_sdk/common.rb +6 -0
  13. data/lib/aliyun_sdk/common/exception.rb +18 -0
  14. data/lib/aliyun_sdk/common/logging.rb +46 -0
  15. data/lib/aliyun_sdk/common/struct.rb +56 -0
  16. data/lib/aliyun_sdk/oss.rb +16 -0
  17. data/lib/aliyun_sdk/oss/bucket.rb +661 -0
  18. data/lib/aliyun_sdk/oss/client.rb +106 -0
  19. data/lib/aliyun_sdk/oss/config.rb +39 -0
  20. data/lib/aliyun_sdk/oss/download.rb +255 -0
  21. data/lib/aliyun_sdk/oss/exception.rb +108 -0
  22. data/lib/aliyun_sdk/oss/http.rb +338 -0
  23. data/lib/aliyun_sdk/oss/iterator.rb +92 -0
  24. data/lib/aliyun_sdk/oss/multipart.rb +74 -0
  25. data/lib/aliyun_sdk/oss/object.rb +15 -0
  26. data/lib/aliyun_sdk/oss/protocol.rb +1499 -0
  27. data/lib/aliyun_sdk/oss/struct.rb +208 -0
  28. data/lib/aliyun_sdk/oss/upload.rb +238 -0
  29. data/lib/aliyun_sdk/oss/util.rb +89 -0
  30. data/lib/aliyun_sdk/sts.rb +9 -0
  31. data/lib/aliyun_sdk/sts/client.rb +38 -0
  32. data/lib/aliyun_sdk/sts/config.rb +22 -0
  33. data/lib/aliyun_sdk/sts/exception.rb +53 -0
  34. data/lib/aliyun_sdk/sts/protocol.rb +130 -0
  35. data/lib/aliyun_sdk/sts/struct.rb +64 -0
  36. data/lib/aliyun_sdk/sts/util.rb +48 -0
  37. data/lib/aliyun_sdk/version.rb +7 -0
  38. data/spec/aliyun/oss/bucket_spec.rb +597 -0
  39. data/spec/aliyun/oss/client/bucket_spec.rb +554 -0
  40. data/spec/aliyun/oss/client/client_spec.rb +297 -0
  41. data/spec/aliyun/oss/client/resumable_download_spec.rb +220 -0
  42. data/spec/aliyun/oss/client/resumable_upload_spec.rb +413 -0
  43. data/spec/aliyun/oss/http_spec.rb +83 -0
  44. data/spec/aliyun/oss/multipart_spec.rb +686 -0
  45. data/spec/aliyun/oss/object_spec.rb +785 -0
  46. data/spec/aliyun/oss/service_spec.rb +142 -0
  47. data/spec/aliyun/oss/util_spec.rb +50 -0
  48. data/spec/aliyun/sts/client_spec.rb +150 -0
  49. data/spec/aliyun/sts/util_spec.rb +39 -0
  50. data/tests/config.rb +31 -0
  51. data/tests/test_content_encoding.rb +54 -0
  52. data/tests/test_content_type.rb +95 -0
  53. data/tests/test_custom_headers.rb +70 -0
  54. data/tests/test_encoding.rb +77 -0
  55. data/tests/test_large_file.rb +66 -0
  56. data/tests/test_multipart.rb +97 -0
  57. data/tests/test_object_acl.rb +49 -0
  58. data/tests/test_object_key.rb +68 -0
  59. data/tests/test_object_url.rb +69 -0
  60. data/tests/test_resumable.rb +40 -0
  61. metadata +240 -0
@@ -0,0 +1,9 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require_relative 'common'
4
+ require_relative 'sts/util'
5
+ require_relative 'sts/exception'
6
+ require_relative 'sts/struct'
7
+ require_relative 'sts/config'
8
+ require_relative 'sts/protocol'
9
+ require_relative 'sts/client'
@@ -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,7 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module AliyunSDK
4
+
5
+ VERSION = "0.4.1"
6
+
7
+ 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