aliyun-sdk 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/README.md +30 -0
- data/examples/aliyun/oss/callback.rb +61 -0
- data/lib/aliyun/oss/bucket.rb +33 -1
- data/lib/aliyun/oss/exception.rb +3 -0
- data/lib/aliyun/oss/http.rb +1 -1
- data/lib/aliyun/oss/protocol.rb +88 -66
- data/lib/aliyun/oss/struct.rb +58 -0
- data/lib/aliyun/oss/upload.rb +2 -1
- data/lib/aliyun/version.rb +1 -1
- data/spec/aliyun/oss/client/bucket_spec.rb +47 -0
- data/spec/aliyun/oss/client/client_spec.rb +1 -1
- data/spec/aliyun/oss/client/resumable_upload_spec.rb +96 -0
- data/spec/aliyun/oss/object_spec.rb +20 -0
- data/tests/test_content_type.rb +1 -1
- data/tests/test_encoding.rb +1 -1
- data/tests/test_object_key.rb +1 -1
- data/tests/test_resumable.rb +1 -1
- metadata +17 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 30d7a2978cd6ef7cdc1ed369febb51a24ca84012
|
4
|
+
data.tar.gz: a8e6b8125b4f64ed182b2a190b01eed1db5dac8e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e9db93f799259a7066bd948cbab927bc38c831f6dea8a1bd155665f85132df0e0520af9a2766d2d33b4dff41f240dfdfe0296b135fb80393f026a0634f41e358
|
7
|
+
data.tar.gz: e4d5e3a83e3950bfb73601a4fcc7e3c0ce6910c6cf1141d7a46277e381f38d34d0254f880e615c5297343340132c3da88a14024dc7c41a62c16324a772bdb356
|
data/CHANGELOG.md
CHANGED
data/README.md
CHANGED
@@ -185,6 +185,35 @@ Object的common prefix,包含在`list_objects`的结果中。
|
|
185
185
|
Common prefix让用户不需要遍历所有的object(可能数量巨大)而找出前缀,
|
186
186
|
在模拟目录结构时非常有用。
|
187
187
|
|
188
|
+
## 上传回调
|
189
|
+
|
190
|
+
在`put_object`和`resumable_upload`时可以指定一个`Callback`,这样在文件
|
191
|
+
成功上传到OSS之后,OSS会向用户提供的服务器地址发起一个HTTP POST请求,
|
192
|
+
以通知用户相应的事件发生了。用户可以在收到这个通知之后进行相应的动作,
|
193
|
+
例如更新数据库、统计行为等。更多有关上传回调的内容请参考[OSS上传回调][oss-callback]。
|
194
|
+
|
195
|
+
下面的例子将演示如何使用上传回调:
|
196
|
+
|
197
|
+
callback = Aliyun::OSS::Callback.new(
|
198
|
+
url: 'http://10.101.168.94:1234/callback',
|
199
|
+
query: {user: 'put_object'},
|
200
|
+
body: 'bucket=${bucket}&object=${object}'
|
201
|
+
)
|
202
|
+
|
203
|
+
begin
|
204
|
+
bucket.put_object('files/hello', callback: callback)
|
205
|
+
rescue Aliyun::OSS::CallbackError => e
|
206
|
+
puts "Callback failed: #{e.message}"
|
207
|
+
end
|
208
|
+
|
209
|
+
**注意**
|
210
|
+
|
211
|
+
1. callback的url**不能**包含query string,而应该在`:query`参数中指定
|
212
|
+
2. 可能出现文件上传成功,但是执行回调失败的情况,此时client会抛出
|
213
|
+
`CallbackError`,用户如果要忽略此错误,需要显式接住这个异常。
|
214
|
+
3. 详细的例子可以参考[callback.rb](examples/aliyun/oss/callback.rb)
|
215
|
+
4. 接受回调的server可以参考[callback_server.rb](rails/aliyun_oss_callback_server.rb)
|
216
|
+
|
188
217
|
## 断点上传/下载
|
189
218
|
|
190
219
|
OSS支持大文件的存储,用户如果上传/下载大文件(Object)的时候中断了(网络
|
@@ -386,3 +415,4 @@ SDK采用rspec进行测试,如果要对SDK进行修改,请确保没有break
|
|
386
415
|
[custom-domain]: https://help.aliyun.com/document_detail/oss/user_guide/oss_concept/oss_cname.html
|
387
416
|
[aliyun-sts]: https://help.aliyun.com/document_detail/ram/intro/concepts.html
|
388
417
|
[sdk-api]: http://www.rubydoc.info/gems/aliyun-sdk/
|
418
|
+
[oss-callback]: https://help.aliyun.com/document_detail/oss/user_guide/upload_object/upload_callback.html
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift(File.expand_path("../../../../lib", __FILE__))
|
4
|
+
require 'yaml'
|
5
|
+
require 'json'
|
6
|
+
require 'aliyun/oss'
|
7
|
+
|
8
|
+
##
|
9
|
+
# 用户在上传文件时可以指定“上传回调”,这样在文件上传成功后OSS会向用户
|
10
|
+
# 提供的服务器地址发起一个HTTP POST请求,相当于一个通知机制。用户可以
|
11
|
+
# 在收到回调的时候做相应的动作。
|
12
|
+
# 1. 如何接受OSS的回调可以参考代码目录下的
|
13
|
+
# rails/aliyun_oss_callback_server.rb
|
14
|
+
# 2. 只有put_object和resumable_upload支持上传回调
|
15
|
+
|
16
|
+
# 初始化OSS client
|
17
|
+
Aliyun::Common::Logging.set_log_level(Logger::DEBUG)
|
18
|
+
conf_file = '~/.oss.yml'
|
19
|
+
conf = YAML.load(File.read(File.expand_path(conf_file)))
|
20
|
+
bucket = Aliyun::OSS::Client.new(
|
21
|
+
:endpoint => conf['endpoint'],
|
22
|
+
:cname => conf['cname'],
|
23
|
+
:access_key_id => conf['access_key_id'],
|
24
|
+
:access_key_secret => conf['access_key_secret']).get_bucket(conf['bucket'])
|
25
|
+
|
26
|
+
# 辅助打印函数
|
27
|
+
def demo(msg)
|
28
|
+
puts "######### #{msg} ########"
|
29
|
+
puts
|
30
|
+
yield
|
31
|
+
puts "-------------------------"
|
32
|
+
puts
|
33
|
+
end
|
34
|
+
|
35
|
+
demo "put object with callback" do
|
36
|
+
callback = Aliyun::OSS::Callback.new(
|
37
|
+
url: 'http://10.101.168.94:1234/callback',
|
38
|
+
query: {user: 'put_object'},
|
39
|
+
body: 'bucket=${bucket}&object=${object}'
|
40
|
+
)
|
41
|
+
|
42
|
+
begin
|
43
|
+
bucket.put_object('files/hello', callback: callback)
|
44
|
+
rescue Aliyun::OSS::CallbackError => e
|
45
|
+
puts "Callback failed: #{e.message}"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
demo "resumable upload with callback" do
|
50
|
+
callback = Aliyun::OSS::Callback.new(
|
51
|
+
url: 'http://10.101.168.94:1234/callback',
|
52
|
+
query: {user: 'resumable_upload'},
|
53
|
+
body: 'bucket=${bucket}&object=${object}'
|
54
|
+
)
|
55
|
+
|
56
|
+
begin
|
57
|
+
bucket.resumable_upload('files/world', '/tmp/x', callback: callback)
|
58
|
+
rescue Aliyun::OSS::CallbackError => e
|
59
|
+
puts "Callback failed: #{e.message}"
|
60
|
+
end
|
61
|
+
end
|
data/lib/aliyun/oss/bucket.rb
CHANGED
@@ -178,6 +178,9 @@ module Aliyun
|
|
178
178
|
# @option opts [Hash] :metas 设置object的meta,这是一些用户自定
|
179
179
|
# 义的属性,它们会和object一起存储,在{#get_object}的时候会
|
180
180
|
# 返回这些meta。属性的key不区分大小写。例如:{ 'year' => '2015' }
|
181
|
+
# @option opts [Callback] :callback 指定操作成功后OSS的
|
182
|
+
# 上传回调,上传成功后OSS会向用户的应用服务器发一个HTTP POST请
|
183
|
+
# 求,`:callback`参数指定这个请求的相关参数
|
181
184
|
# @yield [HTTP::StreamWriter] 如果调用的时候传递了block,则写入
|
182
185
|
# 到object的数据由block指定
|
183
186
|
# @example 流式上传数据
|
@@ -188,7 +191,20 @@ module Aliyun
|
|
188
191
|
# @example 指定Content-Type和metas
|
189
192
|
# put_object('x', :file => '/tmp/x', :content_type => 'text/html',
|
190
193
|
# :metas => {'year' => '2015', 'people' => 'mary'})
|
191
|
-
# @
|
194
|
+
# @example 指定Callback
|
195
|
+
# callback = Aliyun::OSS::Callback.new(
|
196
|
+
# url: 'http://10.101.168.94:1234/callback',
|
197
|
+
# query: {user: 'put_object'},
|
198
|
+
# body: 'bucket=${bucket}&object=${object}'
|
199
|
+
# )
|
200
|
+
#
|
201
|
+
# bucket.put_object('files/hello', callback: callback)
|
202
|
+
# @raise [CallbackError] 如果文件上传成功而Callback调用失败,抛
|
203
|
+
# 出此错误
|
204
|
+
# @note 如果opts中指定了`:file`,则block会被忽略
|
205
|
+
# @note 如果指定了`:callback`,则可能文件上传成功,但是callback
|
206
|
+
# 执行失败,此时会抛出{OSS::CallbackError},用户可以选择接住这
|
207
|
+
# 个异常,以忽略Callback调用错误
|
192
208
|
def put_object(key, opts = {}, &block)
|
193
209
|
args = opts.dup
|
194
210
|
|
@@ -420,15 +436,31 @@ module Aliyun
|
|
420
436
|
# 果设置为true,则在上传的过程中不会写checkpoint文件,这意味着
|
421
437
|
# 上传失败后不能断点续传,而只能重新上传整个文件。如果这个值为
|
422
438
|
# true,则:cpt_file会被忽略。
|
439
|
+
# @option opts [Callback] :callback 指定文件上传成功后OSS的
|
440
|
+
# 上传回调,上传成功后OSS会向用户的应用服务器发一个HTTP POST请
|
441
|
+
# 求,`:callback`参数指定这个请求的相关参数
|
423
442
|
# @yield [Float] 如果调用的时候传递了block,则会将上传进度交由
|
424
443
|
# block处理,进度值是一个0-1之间的小数
|
425
444
|
# @raise [CheckpointBrokenError] 如果cpt文件被损坏,则抛出此错误
|
426
445
|
# @raise [FileInconsistentError] 如果指定的文件与cpt中记录的不一
|
427
446
|
# 致,则抛出此错误
|
447
|
+
# @raise [CallbackError] 如果文件上传成功而Callback调用失败,抛
|
448
|
+
# 出此错误
|
428
449
|
# @example
|
429
450
|
# bucket.resumable_upload('my-object', '/tmp/x') do |p|
|
430
451
|
# puts "Progress: #{(p * 100).round(2)} %"
|
431
452
|
# end
|
453
|
+
# @example 指定Callback
|
454
|
+
# callback = Aliyun::OSS::Callback.new(
|
455
|
+
# url: 'http://10.101.168.94:1234/callback',
|
456
|
+
# query: {user: 'put_object'},
|
457
|
+
# body: 'bucket=${bucket}&object=${object}'
|
458
|
+
# )
|
459
|
+
#
|
460
|
+
# bucket.resumable_upload('files/hello', '/tmp/x', callback: callback)
|
461
|
+
# @note 如果指定了`:callback`,则可能文件上传成功,但是callback
|
462
|
+
# 执行失败,此时会抛出{OSS::CallbackError},用户可以选择接住这
|
463
|
+
# 个异常,以忽略Callback调用错误
|
432
464
|
def resumable_upload(key, file, opts = {}, &block)
|
433
465
|
args = opts.dup
|
434
466
|
|
data/lib/aliyun/oss/exception.rb
CHANGED
data/lib/aliyun/oss/http.rb
CHANGED
data/lib/aliyun/oss/protocol.rb
CHANGED
@@ -14,6 +14,7 @@ module Aliyun
|
|
14
14
|
class Protocol
|
15
15
|
|
16
16
|
STREAM_CHUNK_SIZE = 16 * 1024
|
17
|
+
CALLBACK_HEADER = 'x-oss-callback'
|
17
18
|
|
18
19
|
include Common::Logging
|
19
20
|
|
@@ -49,8 +50,8 @@ module Aliyun
|
|
49
50
|
'max-keys' => opts[:limit]
|
50
51
|
}.reject { |_, v| v.nil? }
|
51
52
|
|
52
|
-
|
53
|
-
doc = parse_xml(body)
|
53
|
+
r = @http.get( {}, {:query => params})
|
54
|
+
doc = parse_xml(r.body)
|
54
55
|
|
55
56
|
buckets = doc.css("Buckets Bucket").map do |node|
|
56
57
|
Bucket.new(
|
@@ -135,9 +136,9 @@ module Aliyun
|
|
135
136
|
logger.info("Begin get bucket acl, name: #{name}")
|
136
137
|
|
137
138
|
sub_res = {'acl' => nil}
|
138
|
-
|
139
|
+
r = @http.get({:bucket => name, :sub_res => sub_res})
|
139
140
|
|
140
|
-
doc = parse_xml(body)
|
141
|
+
doc = parse_xml(r.body)
|
141
142
|
acl = get_node_text(doc.at_css("AccessControlList"), 'Grant')
|
142
143
|
logger.info("Done get bucket acl")
|
143
144
|
|
@@ -182,9 +183,9 @@ module Aliyun
|
|
182
183
|
logger.info("Begin get bucket logging, name: #{name}")
|
183
184
|
|
184
185
|
sub_res = {'logging' => nil}
|
185
|
-
|
186
|
+
r = @http.get({:bucket => name, :sub_res => sub_res})
|
186
187
|
|
187
|
-
doc = parse_xml(body)
|
188
|
+
doc = parse_xml(r.body)
|
188
189
|
opts = {:enable => false}
|
189
190
|
|
190
191
|
logging_node = doc.at_css("LoggingEnabled")
|
@@ -249,10 +250,10 @@ module Aliyun
|
|
249
250
|
logger.info("Begin get bucket website, name: #{name}")
|
250
251
|
|
251
252
|
sub_res = {'website' => nil}
|
252
|
-
|
253
|
+
r = @http.get({:bucket => name, :sub_res => sub_res})
|
253
254
|
|
254
255
|
opts = {:enable => true}
|
255
|
-
doc = parse_xml(body)
|
256
|
+
doc = parse_xml(r.body)
|
256
257
|
opts.update(
|
257
258
|
:index => get_node_text(doc.at_css('IndexDocument'), 'Suffix'),
|
258
259
|
:error => get_node_text(doc.at_css('ErrorDocument'), 'Key')
|
@@ -307,9 +308,9 @@ module Aliyun
|
|
307
308
|
logger.info("Begin get bucket referer, name: #{name}")
|
308
309
|
|
309
310
|
sub_res = {'referer' => nil}
|
310
|
-
|
311
|
+
r = @http.get({:bucket => name, :sub_res => sub_res})
|
311
312
|
|
312
|
-
doc = parse_xml(body)
|
313
|
+
doc = parse_xml(r.body)
|
313
314
|
opts = {
|
314
315
|
:allow_empty =>
|
315
316
|
get_node_text(doc.root, 'AllowEmptyReferer', &:to_bool),
|
@@ -370,9 +371,9 @@ module Aliyun
|
|
370
371
|
logger.info("Begin get bucket lifecycle, name: #{name}")
|
371
372
|
|
372
373
|
sub_res = {'lifecycle' => nil}
|
373
|
-
|
374
|
+
r = @http.get({:bucket => name, :sub_res => sub_res})
|
374
375
|
|
375
|
-
doc = parse_xml(body)
|
376
|
+
doc = parse_xml(r.body)
|
376
377
|
rules = doc.css("Rule").map do |n|
|
377
378
|
days = n.at_css("Expiration Days")
|
378
379
|
date = n.at_css("Expiration Date")
|
@@ -443,9 +444,9 @@ module Aliyun
|
|
443
444
|
logger.info("Begin get bucket cors, bucket: #{name}")
|
444
445
|
|
445
446
|
sub_res = {'cors' => nil}
|
446
|
-
|
447
|
+
r = @http.get({:bucket => name, :sub_res => sub_res})
|
447
448
|
|
448
|
-
doc = parse_xml(body)
|
449
|
+
doc = parse_xml(r.body)
|
449
450
|
rules = []
|
450
451
|
|
451
452
|
doc.css("CORSRule").map do |n|
|
@@ -505,6 +506,8 @@ module Aliyun
|
|
505
506
|
# @option opts [Hash<Symbol, String>] :metas key-value pairs
|
506
507
|
# that serve as the object meta which will be stored together
|
507
508
|
# with the object
|
509
|
+
# @option opts [Callback] :callback the HTTP callback performed
|
510
|
+
# by OSS after `put_object` succeeds
|
508
511
|
# @yield [HTTP::StreamWriter] a stream writer is
|
509
512
|
# yielded to the caller to which it can write chunks of data
|
510
513
|
# streamingly
|
@@ -516,13 +519,23 @@ module Aliyun
|
|
516
519
|
"#{object_name}, options: #{opts}")
|
517
520
|
|
518
521
|
headers = {'Content-Type' => opts[:content_type]}
|
522
|
+
if opts.key?(:callback)
|
523
|
+
headers[CALLBACK_HEADER] = opts[:callback].serialize
|
524
|
+
end
|
525
|
+
|
519
526
|
(opts[:metas] || {})
|
520
527
|
.each { |k, v| headers["x-oss-meta-#{k.to_s}"] = v.to_s }
|
521
528
|
|
522
|
-
@http.put(
|
529
|
+
r = @http.put(
|
523
530
|
{:bucket => bucket_name, :object => object_name},
|
524
531
|
{:headers => headers, :body => HTTP::StreamPayload.new(&block)})
|
525
532
|
|
533
|
+
if r.code == 203
|
534
|
+
e = CallbackError.new(r)
|
535
|
+
logger.error(e.to_s)
|
536
|
+
raise e
|
537
|
+
end
|
538
|
+
|
526
539
|
logger.debug('Done put object')
|
527
540
|
end
|
528
541
|
|
@@ -557,13 +570,13 @@ module Aliyun
|
|
557
570
|
(opts[:metas] || {})
|
558
571
|
.each { |k, v| headers["x-oss-meta-#{k.to_s}"] = v.to_s }
|
559
572
|
|
560
|
-
|
561
|
-
|
562
|
-
|
573
|
+
r = @http.post(
|
574
|
+
{:bucket => bucket_name, :object => object_name, :sub_res => sub_res},
|
575
|
+
{:headers => headers, :body => HTTP::StreamPayload.new(&block)})
|
563
576
|
|
564
577
|
logger.debug('Done append object')
|
565
578
|
|
566
|
-
wrap(
|
579
|
+
wrap(r.headers[:x_oss_next_append_position], &:to_i) || -1
|
567
580
|
end
|
568
581
|
|
569
582
|
# List objects in a bucket.
|
@@ -614,12 +627,10 @@ module Aliyun
|
|
614
627
|
'encoding-type' => opts[:encoding]
|
615
628
|
}.reject { |_, v| v.nil? }
|
616
629
|
|
617
|
-
|
618
|
-
|
619
|
-
doc = parse_xml(body)
|
630
|
+
r = @http.get({:bucket => bucket_name}, {:query => params})
|
620
631
|
|
632
|
+
doc = parse_xml(r.body)
|
621
633
|
encoding = get_node_text(doc.root, 'EncodingType')
|
622
|
-
|
623
634
|
objects = doc.css("Contents").map do |node|
|
624
635
|
Object.new(
|
625
636
|
:key => get_node_text(node, "Key") { |x| decode_key(x, encoding) },
|
@@ -728,12 +739,13 @@ module Aliyun
|
|
728
739
|
rewrites[:expires].httpdate if rewrites.key?(:expires)
|
729
740
|
end
|
730
741
|
|
731
|
-
|
732
|
-
|
733
|
-
|
734
|
-
|
735
|
-
|
742
|
+
r = @http.get(
|
743
|
+
{:bucket => bucket_name, :object => object_name,
|
744
|
+
:sub_res => sub_res},
|
745
|
+
{:headers => headers}
|
746
|
+
) { |chunk| yield chunk if block_given? }
|
736
747
|
|
748
|
+
h = r.headers
|
737
749
|
metas = {}
|
738
750
|
meta_prefix = 'x_oss_meta_'
|
739
751
|
h.select { |k, _| k.to_s.start_with?(meta_prefix) }
|
@@ -772,10 +784,11 @@ module Aliyun
|
|
772
784
|
headers = {}
|
773
785
|
headers.merge!(get_conditions(opts[:condition])) if opts[:condition]
|
774
786
|
|
775
|
-
|
776
|
-
|
777
|
-
|
787
|
+
r = @http.head(
|
788
|
+
{:bucket => bucket_name, :object => object_name},
|
789
|
+
{:headers => headers})
|
778
790
|
|
791
|
+
h = r.headers
|
779
792
|
metas = {}
|
780
793
|
meta_prefix = 'x_oss_meta_'
|
781
794
|
h.select { |k, _| k.to_s.start_with?(meta_prefix) }
|
@@ -839,11 +852,11 @@ module Aliyun
|
|
839
852
|
|
840
853
|
headers.merge!(get_copy_conditions(opts[:condition])) if opts[:condition]
|
841
854
|
|
842
|
-
|
855
|
+
r = @http.put(
|
843
856
|
{:bucket => bucket_name, :object => dst_object_name},
|
844
857
|
{:headers => headers})
|
845
858
|
|
846
|
-
doc = parse_xml(body)
|
859
|
+
doc = parse_xml(r.body)
|
847
860
|
copy_result = {
|
848
861
|
:last_modified => get_node_text(
|
849
862
|
doc.root, 'LastModified') { |x| Time.parse(x) },
|
@@ -897,13 +910,13 @@ module Aliyun
|
|
897
910
|
query = {}
|
898
911
|
query['encoding-type'] = opts[:encoding] if opts[:encoding]
|
899
912
|
|
900
|
-
|
913
|
+
r = @http.post(
|
901
914
|
{:bucket => bucket_name, :sub_res => sub_res},
|
902
915
|
{:query => query, :body => body})
|
903
916
|
|
904
917
|
deleted = []
|
905
918
|
unless opts[:quiet]
|
906
|
-
doc = parse_xml(body)
|
919
|
+
doc = parse_xml(r.body)
|
907
920
|
encoding = get_node_text(doc.root, 'EncodingType')
|
908
921
|
doc.css("Deleted").map do |n|
|
909
922
|
deleted << get_node_text(n, 'Key') { |x| decode_key(x, encoding) }
|
@@ -942,10 +955,10 @@ module Aliyun
|
|
942
955
|
"object: #{object_name}")
|
943
956
|
|
944
957
|
sub_res = {'acl' => nil}
|
945
|
-
|
946
|
-
|
958
|
+
r = @http.get(
|
959
|
+
{bucket: bucket_name, object: object_name, sub_res: sub_res})
|
947
960
|
|
948
|
-
doc = parse_xml(body)
|
961
|
+
doc = parse_xml(r.body)
|
949
962
|
acl = get_node_text(doc.at_css("AccessControlList"), 'Grant')
|
950
963
|
|
951
964
|
logger.debug("Done get object acl")
|
@@ -974,18 +987,18 @@ module Aliyun
|
|
974
987
|
'Access-Control-Request-Headers' => headers.join(',')
|
975
988
|
}
|
976
989
|
|
977
|
-
|
978
|
-
|
979
|
-
|
990
|
+
r = @http.options(
|
991
|
+
{:bucket => bucket_name, :object => object_name},
|
992
|
+
{:headers => h})
|
980
993
|
|
981
994
|
logger.debug("Done get object cors")
|
982
995
|
|
983
996
|
CORSRule.new(
|
984
|
-
:allowed_origins =>
|
985
|
-
:allowed_methods =>
|
986
|
-
:allowed_headers =>
|
987
|
-
:expose_headers =>
|
988
|
-
:max_age_seconds =>
|
997
|
+
:allowed_origins => r.headers[:access_control_allow_origin],
|
998
|
+
:allowed_methods => r.headers[:access_control_allow_methods],
|
999
|
+
:allowed_headers => r.headers[:access_control_allow_headers],
|
1000
|
+
:expose_headers => r.headers[:access_control_expose_headers],
|
1001
|
+
:max_age_seconds => r.headers[:access_control_max_age]
|
989
1002
|
)
|
990
1003
|
end
|
991
1004
|
|
@@ -1014,12 +1027,12 @@ module Aliyun
|
|
1014
1027
|
(opts[:metas] || {})
|
1015
1028
|
.each { |k, v| headers["x-oss-meta-#{k.to_s}"] = v.to_s }
|
1016
1029
|
|
1017
|
-
|
1018
|
-
|
1019
|
-
|
1020
|
-
|
1030
|
+
r = @http.post(
|
1031
|
+
{:bucket => bucket_name, :object => object_name,
|
1032
|
+
:sub_res => sub_res},
|
1033
|
+
{:headers => headers})
|
1021
1034
|
|
1022
|
-
doc = parse_xml(body)
|
1035
|
+
doc = parse_xml(r.body)
|
1023
1036
|
txn_id = get_node_text(doc.root, 'UploadId')
|
1024
1037
|
|
1025
1038
|
logger.info("Done initiate multipart upload: #{txn_id}.")
|
@@ -1040,13 +1053,13 @@ module Aliyun
|
|
1040
1053
|
"#{object_name}, txn id: #{txn_id}, part No: #{part_no}")
|
1041
1054
|
|
1042
1055
|
sub_res = {'partNumber' => part_no, 'uploadId' => txn_id}
|
1043
|
-
|
1056
|
+
r = @http.put(
|
1044
1057
|
{:bucket => bucket_name, :object => object_name, :sub_res => sub_res},
|
1045
1058
|
{:body => HTTP::StreamPayload.new(&block)})
|
1046
1059
|
|
1047
1060
|
logger.debug("Done upload part")
|
1048
1061
|
|
1049
|
-
Multipart::Part.new(:number => part_no, :etag => headers[:etag])
|
1062
|
+
Multipart::Part.new(:number => part_no, :etag => r.headers[:etag])
|
1050
1063
|
end
|
1051
1064
|
|
1052
1065
|
# Upload a part in a multipart uploading transaction by copying
|
@@ -1084,26 +1097,31 @@ module Aliyun
|
|
1084
1097
|
|
1085
1098
|
sub_res = {'partNumber' => part_no, 'uploadId' => txn_id}
|
1086
1099
|
|
1087
|
-
|
1100
|
+
r = @http.put(
|
1088
1101
|
{:bucket => bucket_name, :object => object_name, :sub_res => sub_res},
|
1089
1102
|
{:headers => headers})
|
1090
1103
|
|
1091
1104
|
logger.debug("Done upload part by copy: #{source_object}.")
|
1092
1105
|
|
1093
|
-
Multipart::Part.new(:number => part_no, :etag => headers[:etag])
|
1106
|
+
Multipart::Part.new(:number => part_no, :etag => r.headers[:etag])
|
1094
1107
|
end
|
1095
1108
|
|
1096
1109
|
# Complete a multipart uploading transaction
|
1097
1110
|
# @param bucket_name [String] the bucket name
|
1098
1111
|
# @param object_name [String] the object name
|
1099
1112
|
# @param txn_id [String] the upload id
|
1100
|
-
# @param parts [Array<Multipart::Part>] all the
|
1101
|
-
#
|
1102
|
-
|
1113
|
+
# @param parts [Array<Multipart::Part>] all the parts in this
|
1114
|
+
# transaction
|
1115
|
+
# @param callback [Callback] the HTTP callback performed by OSS
|
1116
|
+
# after this operation succeeds
|
1117
|
+
def complete_multipart_upload(
|
1118
|
+
bucket_name, object_name, txn_id, parts, callback = nil)
|
1103
1119
|
logger.debug("Begin complete multipart upload, "\
|
1104
1120
|
"txn id: #{txn_id}, parts: #{parts.map(&:to_s)}")
|
1105
1121
|
|
1106
1122
|
sub_res = {'uploadId' => txn_id}
|
1123
|
+
headers = {}
|
1124
|
+
headers[CALLBACK_HEADER] = callback.serialize if callback
|
1107
1125
|
|
1108
1126
|
body = Nokogiri::XML::Builder.new do |xml|
|
1109
1127
|
xml.CompleteMultipartUpload {
|
@@ -1116,9 +1134,15 @@ module Aliyun
|
|
1116
1134
|
}
|
1117
1135
|
end.to_xml
|
1118
1136
|
|
1119
|
-
@http.post(
|
1137
|
+
r = @http.post(
|
1120
1138
|
{:bucket => bucket_name, :object => object_name, :sub_res => sub_res},
|
1121
|
-
{:body => body})
|
1139
|
+
{:headers => headers, :body => body})
|
1140
|
+
|
1141
|
+
if r.code == 203
|
1142
|
+
e = CallbackError.new(r)
|
1143
|
+
logger.error(e.to_s)
|
1144
|
+
raise e
|
1145
|
+
end
|
1122
1146
|
|
1123
1147
|
logger.debug("Done complete multipart upload: #{txn_id}.")
|
1124
1148
|
end
|
@@ -1188,14 +1212,12 @@ module Aliyun
|
|
1188
1212
|
'encoding-type' => opts[:encoding]
|
1189
1213
|
}.reject { |_, v| v.nil? }
|
1190
1214
|
|
1191
|
-
|
1215
|
+
r = @http.get(
|
1192
1216
|
{:bucket => bucket_name, :sub_res => sub_res},
|
1193
1217
|
{:query => params})
|
1194
1218
|
|
1195
|
-
doc = parse_xml(body)
|
1196
|
-
|
1219
|
+
doc = parse_xml(r.body)
|
1197
1220
|
encoding = get_node_text(doc.root, 'EncodingType')
|
1198
|
-
|
1199
1221
|
txns = doc.css("Upload").map do |node|
|
1200
1222
|
Multipart::Transaction.new(
|
1201
1223
|
:id => get_node_text(node, "UploadId"),
|
@@ -1259,11 +1281,11 @@ module Aliyun
|
|
1259
1281
|
'encoding-type' => opts[:encoding]
|
1260
1282
|
}.reject { |_, v| v.nil? }
|
1261
1283
|
|
1262
|
-
|
1284
|
+
r = @http.get(
|
1263
1285
|
{:bucket => bucket_name, :object => object_name, :sub_res => sub_res},
|
1264
1286
|
{:query => params})
|
1265
1287
|
|
1266
|
-
doc = parse_xml(body)
|
1288
|
+
doc = parse_xml(r.body)
|
1267
1289
|
parts = doc.css("Part").map do |node|
|
1268
1290
|
Multipart::Part.new(
|
1269
1291
|
:number => get_node_text(node, 'PartNumber', &:to_i),
|
data/lib/aliyun/oss/struct.rb
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
# -*- encoding: utf-8 -*-
|
2
2
|
|
3
|
+
require 'base64'
|
4
|
+
require 'json'
|
5
|
+
require 'uri'
|
6
|
+
|
3
7
|
module Aliyun
|
4
8
|
module OSS
|
5
9
|
|
@@ -146,5 +150,59 @@ module Aliyun
|
|
146
150
|
|
147
151
|
end # CORSRule
|
148
152
|
|
153
|
+
##
|
154
|
+
# Callback represents a HTTP call made by OSS to user's
|
155
|
+
# application server after an event happens, such as an object is
|
156
|
+
# successfully uploaded to OSS. See: {https://help.aliyun.com/document_detail/oss/api-reference/object/Callback.html}
|
157
|
+
# Attributes:
|
158
|
+
# * url [String] the URL *WITHOUT* the query string
|
159
|
+
# * query [Hash] the query to generate query string
|
160
|
+
# * body [String] the body of the request
|
161
|
+
# * content_type [String] the Content-Type of the request
|
162
|
+
# * host [String] the Host in HTTP header for this request
|
163
|
+
class Callback < Common::Struct::Base
|
164
|
+
|
165
|
+
attrs :url, :query, :body, :content_type, :host
|
166
|
+
|
167
|
+
include Common::Logging
|
168
|
+
|
169
|
+
def serialize
|
170
|
+
query_string = (query || {}).map { |k, v|
|
171
|
+
[CGI.escape(k.to_s), CGI.escape(v.to_s)].join('=') }.join('&')
|
172
|
+
|
173
|
+
cb = {
|
174
|
+
'callbackUrl' => "#{normalize_url(url)}?#{query_string}",
|
175
|
+
'callbackBody' => body,
|
176
|
+
'callbackBodyType' => content_type || default_content_type
|
177
|
+
}
|
178
|
+
cb['callbackHost'] = host if host
|
179
|
+
|
180
|
+
logger.debug("Callback json: #{cb}")
|
181
|
+
|
182
|
+
Base64.strict_encode64(cb.to_json)
|
183
|
+
end
|
184
|
+
|
185
|
+
private
|
186
|
+
def normalize_url(url)
|
187
|
+
uri = URI.parse(url)
|
188
|
+
uri = URI.parse("http://#{url}") unless uri.scheme
|
189
|
+
|
190
|
+
if uri.scheme != 'http' and uri.scheme != 'https'
|
191
|
+
fail ClientError, "Only HTTP and HTTPS endpoint are accepted."
|
192
|
+
end
|
193
|
+
|
194
|
+
unless uri.query.nil?
|
195
|
+
fail ClientError, "Query parameters should not appear in URL."
|
196
|
+
end
|
197
|
+
|
198
|
+
uri.to_s
|
199
|
+
end
|
200
|
+
|
201
|
+
def default_content_type
|
202
|
+
"application/x-www-form-urlencoded"
|
203
|
+
end
|
204
|
+
|
205
|
+
end # Callback
|
206
|
+
|
149
207
|
end # OSS
|
150
208
|
end # Aliyun
|
data/lib/aliyun/oss/upload.rb
CHANGED
@@ -116,7 +116,8 @@ module Aliyun
|
|
116
116
|
parts = sync_get_all_parts.map{ |p|
|
117
117
|
Part.new(:number => p[:number], :etag => p[:etag])
|
118
118
|
}
|
119
|
-
@protocol.complete_multipart_upload(
|
119
|
+
@protocol.complete_multipart_upload(
|
120
|
+
bucket, object, id, parts, @options[:callback])
|
120
121
|
|
121
122
|
File.delete(@cpt_file) unless options[:disable_cpt]
|
122
123
|
|
data/lib/aliyun/version.rb
CHANGED
@@ -104,6 +104,16 @@ module Aliyun
|
|
104
104
|
end.to_xml
|
105
105
|
end
|
106
106
|
|
107
|
+
def mock_error(code, message)
|
108
|
+
Nokogiri::XML::Builder.new do |xml|
|
109
|
+
xml.Error {
|
110
|
+
xml.Code code
|
111
|
+
xml.Message message
|
112
|
+
xml.RequestId '0000'
|
113
|
+
}
|
114
|
+
end.to_xml
|
115
|
+
end
|
116
|
+
|
107
117
|
def err(msg, reqid = '0000')
|
108
118
|
"#{msg} RequestId: #{reqid}"
|
109
119
|
end
|
@@ -229,6 +239,43 @@ module Aliyun
|
|
229
239
|
.with(:body => content, :query => {})
|
230
240
|
end
|
231
241
|
|
242
|
+
it "should put object with callback" do
|
243
|
+
key = 'ruby'
|
244
|
+
stub_request(:put, object_url(key))
|
245
|
+
|
246
|
+
callback = Callback.new(
|
247
|
+
url: 'http://app.server.com/callback',
|
248
|
+
query: {'id' => 1, 'name' => '杭州'},
|
249
|
+
body: 'hello world',
|
250
|
+
host: 'server.com'
|
251
|
+
)
|
252
|
+
@bucket.put_object(key, callback: callback)
|
253
|
+
|
254
|
+
expect(WebMock).to have_requested(:put, object_url(key))
|
255
|
+
.with { |req| req.headers.key?('X-Oss-Callback') }
|
256
|
+
end
|
257
|
+
|
258
|
+
it "should raise CallbackError when callback failed" do
|
259
|
+
key = 'ruby'
|
260
|
+
code = 'CallbackFailed'
|
261
|
+
message = 'Error status: 502.'
|
262
|
+
stub_request(:put, object_url(key))
|
263
|
+
.to_return(:status => 203, :body => mock_error(code, message))
|
264
|
+
|
265
|
+
callback = Callback.new(
|
266
|
+
url: 'http://app.server.com/callback',
|
267
|
+
query: {'id' => 1, 'name' => '杭州'},
|
268
|
+
body: 'hello world',
|
269
|
+
host: 'server.com'
|
270
|
+
)
|
271
|
+
expect {
|
272
|
+
@bucket.put_object(key, callback: callback)
|
273
|
+
}.to raise_error(CallbackError, err(message))
|
274
|
+
|
275
|
+
expect(WebMock).to have_requested(:put, object_url(key))
|
276
|
+
.with { |req| req.headers.key?('X-Oss-Callback') }
|
277
|
+
end
|
278
|
+
|
232
279
|
it "should get object to file" do
|
233
280
|
key = 'ruby'
|
234
281
|
# 100 KB
|
@@ -54,7 +54,7 @@ module Aliyun
|
|
54
54
|
|
55
55
|
expect(WebMock)
|
56
56
|
.to have_requested(:get, "#{bucket}.#{endpoint}/#{object}")
|
57
|
-
.with{ |req|
|
57
|
+
.with{ |req| req.headers.key?('X-Oss-Security-Token') }
|
58
58
|
end
|
59
59
|
|
60
60
|
it "should construct different client" do
|
@@ -66,6 +66,10 @@ module Aliyun
|
|
66
66
|
end.to_xml
|
67
67
|
end
|
68
68
|
|
69
|
+
def err(msg, reqid = '0000')
|
70
|
+
"#{msg} RequestId: #{reqid}"
|
71
|
+
end
|
72
|
+
|
69
73
|
it "should upload file when all goes well" do
|
70
74
|
stub_request(:post, /#{object_url}\?uploads.*/)
|
71
75
|
.to_return(:body => mock_txn_id('upload_id'))
|
@@ -99,6 +103,98 @@ module Aliyun
|
|
99
103
|
expect(prg.size).to eq(10)
|
100
104
|
end
|
101
105
|
|
106
|
+
it "should upload file with callback" do
|
107
|
+
stub_request(:post, /#{object_url}\?uploads.*/)
|
108
|
+
.to_return(:body => mock_txn_id('upload_id'))
|
109
|
+
stub_request(:put, /#{object_url}\?partNumber.*/)
|
110
|
+
stub_request(:post, /#{object_url}\?uploadId.*/)
|
111
|
+
|
112
|
+
callback = Callback.new(
|
113
|
+
url: 'http://app.server.com/callback',
|
114
|
+
query: {'id' => 1, 'name' => '杭州'},
|
115
|
+
body: 'hello world',
|
116
|
+
host: 'server.com'
|
117
|
+
)
|
118
|
+
prg = []
|
119
|
+
@bucket.resumable_upload(
|
120
|
+
@object_key, @file,
|
121
|
+
:part_size => 10, :callback => callback) { |p| prg << p }
|
122
|
+
|
123
|
+
expect(WebMock).to have_requested(
|
124
|
+
:post, /#{object_url}\?uploads.*/).times(1)
|
125
|
+
|
126
|
+
part_numbers = Set.new([])
|
127
|
+
upload_ids = Set.new([])
|
128
|
+
|
129
|
+
expect(WebMock).to have_requested(
|
130
|
+
:put, /#{object_url}\?partNumber.*/).with{ |req|
|
131
|
+
query = parse_query_from_uri(req.uri)
|
132
|
+
part_numbers << query['partNumber']
|
133
|
+
upload_ids << query['uploadId']
|
134
|
+
}.times(10)
|
135
|
+
|
136
|
+
expect(part_numbers.to_a).to match_array((1..10).map{ |x| x.to_s })
|
137
|
+
expect(upload_ids.to_a).to match_array(['upload_id'])
|
138
|
+
|
139
|
+
expect(WebMock)
|
140
|
+
.to have_requested(
|
141
|
+
:post, /#{object_url}\?uploadId.*/)
|
142
|
+
.with { |req| req.headers.key?('X-Oss-Callback') }
|
143
|
+
.times(1)
|
144
|
+
|
145
|
+
expect(File.exist?("#{@file}.cpt")).to be false
|
146
|
+
expect(prg.size).to eq(10)
|
147
|
+
end
|
148
|
+
|
149
|
+
it "should raise CallbackError when callback failed" do
|
150
|
+
stub_request(:post, /#{object_url}\?uploads.*/)
|
151
|
+
.to_return(:body => mock_txn_id('upload_id'))
|
152
|
+
stub_request(:put, /#{object_url}\?partNumber.*/)
|
153
|
+
|
154
|
+
code = 'CallbackFailed'
|
155
|
+
message = 'Error status: 502.'
|
156
|
+
stub_request(:post, /#{object_url}\?uploadId.*/)
|
157
|
+
.to_return(:status => 203, :body => mock_error(code, message))
|
158
|
+
|
159
|
+
callback = Callback.new(
|
160
|
+
url: 'http://app.server.com/callback',
|
161
|
+
query: {'id' => 1, 'name' => '杭州'},
|
162
|
+
body: 'hello world',
|
163
|
+
host: 'server.com'
|
164
|
+
)
|
165
|
+
prg = []
|
166
|
+
expect {
|
167
|
+
@bucket.resumable_upload(
|
168
|
+
@object_key, @file,
|
169
|
+
:part_size => 10, :callback => callback) { |p| prg << p }
|
170
|
+
}.to raise_error(CallbackError, err(message))
|
171
|
+
|
172
|
+
expect(WebMock).to have_requested(
|
173
|
+
:post, /#{object_url}\?uploads.*/).times(1)
|
174
|
+
|
175
|
+
part_numbers = Set.new([])
|
176
|
+
upload_ids = Set.new([])
|
177
|
+
|
178
|
+
expect(WebMock).to have_requested(
|
179
|
+
:put, /#{object_url}\?partNumber.*/).with{ |req|
|
180
|
+
query = parse_query_from_uri(req.uri)
|
181
|
+
part_numbers << query['partNumber']
|
182
|
+
upload_ids << query['uploadId']
|
183
|
+
}.times(10)
|
184
|
+
|
185
|
+
expect(part_numbers.to_a).to match_array((1..10).map{ |x| x.to_s })
|
186
|
+
expect(upload_ids.to_a).to match_array(['upload_id'])
|
187
|
+
|
188
|
+
expect(WebMock)
|
189
|
+
.to have_requested(
|
190
|
+
:post, /#{object_url}\?uploadId.*/)
|
191
|
+
.with { |req| req.headers.key?('X-Oss-Callback') }
|
192
|
+
.times(1)
|
193
|
+
|
194
|
+
expect(File.exist?("#{@file}.cpt")).to be true
|
195
|
+
expect(prg.size).to eq(10)
|
196
|
+
end
|
197
|
+
|
102
198
|
it "should restart when begin txn fails" do
|
103
199
|
code = 'Timeout'
|
104
200
|
message = 'Request timeout.'
|
@@ -737,6 +737,26 @@ module Aliyun
|
|
737
737
|
end
|
738
738
|
end # cors
|
739
739
|
|
740
|
+
context "callback" do
|
741
|
+
it "should encode callback" do
|
742
|
+
callback = Callback.new(
|
743
|
+
url: 'http://app.server.com/callback',
|
744
|
+
query: {'id' => 1, 'name' => '杭州'},
|
745
|
+
body: 'hello world',
|
746
|
+
host: 'server.com'
|
747
|
+
)
|
748
|
+
|
749
|
+
encoded = "eyJjYWxsYmFja1VybCI6Imh0dHA6Ly9hcHAuc2VydmVyLmNvbS9jYWxsYmFjaz9pZD0xJm5hbWU9JUU2JTlEJUFEJUU1JUI3JTlFIiwiY2FsbGJhY2tCb2R5IjoiaGVsbG8gd29ybGQiLCJjYWxsYmFja0JvZHlUeXBlIjoiYXBwbGljYXRpb24veC13d3ctZm9ybS11cmxlbmNvZGVkIiwiY2FsbGJhY2tIb3N0Ijoic2VydmVyLmNvbSJ9"
|
750
|
+
expect(callback.serialize).to eq(encoded)
|
751
|
+
end
|
752
|
+
|
753
|
+
it "should not accept url with query string" do
|
754
|
+
expect {
|
755
|
+
Callback.new(url: 'http://app.server.com/callback?id=1').serialize
|
756
|
+
}.to raise_error(ClientError, "Query parameters should not appear in URL.")
|
757
|
+
end
|
758
|
+
|
759
|
+
end
|
740
760
|
end # Object
|
741
761
|
|
742
762
|
end # OSS
|
data/tests/test_content_type.rb
CHANGED
@@ -5,7 +5,7 @@ require 'aliyun/oss'
|
|
5
5
|
|
6
6
|
class TestContentType < Minitest::Test
|
7
7
|
def setup
|
8
|
-
Aliyun::
|
8
|
+
Aliyun::Common::Logging.set_log_level(Logger::DEBUG)
|
9
9
|
conf_file = '~/.oss.yml'
|
10
10
|
conf = YAML.load(File.read(File.expand_path(conf_file)))
|
11
11
|
client = Aliyun::OSS::Client.new(
|
data/tests/test_encoding.rb
CHANGED
@@ -6,7 +6,7 @@ require 'aliyun/oss'
|
|
6
6
|
|
7
7
|
class TestEncoding < Minitest::Test
|
8
8
|
def setup
|
9
|
-
Aliyun::
|
9
|
+
Aliyun::Common::Logging.set_log_level(Logger::DEBUG)
|
10
10
|
conf_file = '~/.oss.yml'
|
11
11
|
conf = YAML.load(File.read(File.expand_path(conf_file)))
|
12
12
|
client = Aliyun::OSS::Client.new(
|
data/tests/test_object_key.rb
CHANGED
@@ -6,7 +6,7 @@ require 'aliyun/oss'
|
|
6
6
|
|
7
7
|
class TestObjectKey < Minitest::Test
|
8
8
|
def setup
|
9
|
-
Aliyun::
|
9
|
+
Aliyun::Common::Logging.set_log_level(Logger::DEBUG)
|
10
10
|
conf_file = '~/.oss.yml'
|
11
11
|
conf = YAML.load(File.read(File.expand_path(conf_file)))
|
12
12
|
client = Aliyun::OSS::Client.new(
|
data/tests/test_resumable.rb
CHANGED
@@ -5,7 +5,7 @@ require 'aliyun/oss'
|
|
5
5
|
|
6
6
|
class TestResumable < Minitest::Test
|
7
7
|
def setup
|
8
|
-
Aliyun::
|
8
|
+
Aliyun::Common::Logging.set_log_level(Logger::DEBUG)
|
9
9
|
conf_file = '~/.oss.yml'
|
10
10
|
conf = YAML.load(File.read(File.expand_path(conf_file)))
|
11
11
|
client = Aliyun::OSS::Client.new(
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: aliyun-sdk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.3.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tianlong Wu
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2015-12-
|
11
|
+
date: 2015-12-09 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: nokogiri
|
@@ -108,6 +108,20 @@ dependencies:
|
|
108
108
|
- - "~>"
|
109
109
|
- !ruby/object:Gem::Version
|
110
110
|
version: '0.10'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: minitest
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - "~>"
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '5.8'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - "~>"
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '5.8'
|
111
125
|
description: A Ruby program to facilitate accessing Aliyun Object Storage Service
|
112
126
|
email:
|
113
127
|
- rockuw.@gmail.com
|
@@ -120,6 +134,7 @@ files:
|
|
120
134
|
- CHANGELOG.md
|
121
135
|
- README.md
|
122
136
|
- examples/aliyun/oss/bucket.rb
|
137
|
+
- examples/aliyun/oss/callback.rb
|
123
138
|
- examples/aliyun/oss/object.rb
|
124
139
|
- examples/aliyun/oss/resumable_download.rb
|
125
140
|
- examples/aliyun/oss/resumable_upload.rb
|