aliyun-sdk 0.2.0 → 0.3.0

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