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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 1f79884482b39489cdabb7123b28bb1dae2bb547
4
- data.tar.gz: e7a09b6bc276c0f8651f6ead88be61e8fa7d70cd
3
+ metadata.gz: 30d7a2978cd6ef7cdc1ed369febb51a24ca84012
4
+ data.tar.gz: a8e6b8125b4f64ed182b2a190b01eed1db5dac8e
5
5
  SHA512:
6
- metadata.gz: 3021afacd2b718ec6cc01e898fed4b0d3c909f1576d0744c84ad45c608b325f7ec047650b23c75176e1e7a27277afec31064f58d742998e359ffb9bcd31efa31
7
- data.tar.gz: 91fca099332c548efc4d6fc0ae111aa4e7ffda04afdda041807d9da156e5ef5eecf002c2258dc5f086104ba4878cb956bac38b77ff0740ba73746bc02f486e1b
6
+ metadata.gz: e9db93f799259a7066bd948cbab927bc38c831f6dea8a1bd155665f85132df0e0520af9a2766d2d33b4dff41f240dfdfe0296b135fb80393f026a0634f41e358
7
+ data.tar.gz: e4d5e3a83e3950bfb73601a4fcc7e3c0ce6910c6cf1141d7a46277e381f38d34d0254f880e615c5297343340132c3da88a14024dc7c41a62c16324a772bdb356
@@ -1,5 +1,9 @@
1
1
  ## Change Log
2
2
 
3
+ ### v0.3.0
4
+
5
+ - Add support for OSS Callback
6
+
3
7
  ### v0.2.0
4
8
 
5
9
  - Add aliyun/sts
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
@@ -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
- # @note 如果opts中指定了:file,则block会被忽略
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
 
@@ -54,6 +54,9 @@ module Aliyun
54
54
 
55
55
  end # ServerError
56
56
 
57
+ class CallbackError < ServerError
58
+ end # CallbackError
59
+
57
60
  ##
58
61
  # ClientError represents client exceptions caused mostly by
59
62
  # invalid parameters.
@@ -277,7 +277,7 @@ module Aliyun
277
277
  logger.debug("Received HTTP response, code: #{r.code}, headers: " \
278
278
  "#{r.headers}, body: #{r.body}")
279
279
 
280
- [r.headers, r.body]
280
+ r
281
281
  end
282
282
 
283
283
  def get_user_agent
@@ -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
- _, body = @http.get( {}, {:query => params})
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
- _, body = @http.get({:bucket => name, :sub_res => sub_res})
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
- _, body = @http.get({:bucket => name, :sub_res => sub_res})
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
- _, body = @http.get({:bucket => name, :sub_res => sub_res})
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
- _, body = @http.get({:bucket => name, :sub_res => sub_res})
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
- _, body = @http.get({:bucket => name, :sub_res => sub_res})
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
- _, body = @http.get({:bucket => name, :sub_res => sub_res})
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
- h, _ = @http.post(
561
- {:bucket => bucket_name, :object => object_name, :sub_res => sub_res},
562
- {:headers => headers, :body => HTTP::StreamPayload.new(&block)})
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(h[:x_oss_next_append_position], &:to_i) || -1
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
- _, body = @http.get({:bucket => bucket_name}, {:query => params})
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
- h, _ = @http.get(
732
- {:bucket => bucket_name, :object => object_name,
733
- :sub_res => sub_res},
734
- {:headers => headers}
735
- ) { |chunk| yield chunk if block_given? }
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
- h, _ = @http.head(
776
- {:bucket => bucket_name, :object => object_name},
777
- {:headers => headers})
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
- _, body = @http.put(
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
- _, body = @http.post(
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
- _, body = @http.get(
946
- {bucket: bucket_name, object: object_name, sub_res: sub_res})
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
- return_headers, _ = @http.options(
978
- {:bucket => bucket_name, :object => object_name},
979
- {:headers => h})
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 => return_headers[:access_control_allow_origin],
985
- :allowed_methods => return_headers[:access_control_allow_methods],
986
- :allowed_headers => return_headers[:access_control_allow_headers],
987
- :expose_headers => return_headers[:access_control_expose_headers],
988
- :max_age_seconds => return_headers[:access_control_max_age]
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
- _, body = @http.post(
1018
- {:bucket => bucket_name, :object => object_name,
1019
- :sub_res => sub_res},
1020
- {:headers => headers})
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
- headers, _ = @http.put(
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
- headers, _ = @http.put(
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
- # parts in this transaction
1102
- def complete_multipart_upload(bucket_name, object_name, txn_id, parts)
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
- _, body = @http.get(
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
- _, body = @http.get(
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),
@@ -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
@@ -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(bucket, object, id, parts)
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
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Aliyun
4
4
 
5
- VERSION = "0.2.0"
5
+ VERSION = "0.3.0"
6
6
 
7
7
  end # Aliyun
@@ -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| not req.headers.has_key?('x-oss-security-token') }
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
@@ -5,7 +5,7 @@ require 'aliyun/oss'
5
5
 
6
6
  class TestContentType < Minitest::Test
7
7
  def setup
8
- Aliyun::OSS::Logging.set_log_level(Logger::DEBUG)
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(
@@ -6,7 +6,7 @@ require 'aliyun/oss'
6
6
 
7
7
  class TestEncoding < Minitest::Test
8
8
  def setup
9
- Aliyun::OSS::Logging.set_log_level(Logger::DEBUG)
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(
@@ -6,7 +6,7 @@ require 'aliyun/oss'
6
6
 
7
7
  class TestObjectKey < Minitest::Test
8
8
  def setup
9
- Aliyun::OSS::Logging.set_log_level(Logger::DEBUG)
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(
@@ -5,7 +5,7 @@ require 'aliyun/oss'
5
5
 
6
6
  class TestResumable < Minitest::Test
7
7
  def setup
8
- Aliyun::OSS::Logging.set_log_level(Logger::DEBUG)
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.2.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-07 00:00:00.000000000 Z
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