aliyun-oss-ruby-sdk 0.4.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (61) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +95 -0
  3. data/README.md +423 -0
  4. data/examples/aliyun/oss/bucket.rb +144 -0
  5. data/examples/aliyun/oss/callback.rb +61 -0
  6. data/examples/aliyun/oss/object.rb +182 -0
  7. data/examples/aliyun/oss/resumable_download.rb +42 -0
  8. data/examples/aliyun/oss/resumable_upload.rb +49 -0
  9. data/examples/aliyun/oss/streaming.rb +124 -0
  10. data/examples/aliyun/oss/using_sts.rb +48 -0
  11. data/examples/aliyun/sts/assume_role.rb +59 -0
  12. data/lib/aliyun_sdk/common.rb +6 -0
  13. data/lib/aliyun_sdk/common/exception.rb +18 -0
  14. data/lib/aliyun_sdk/common/logging.rb +46 -0
  15. data/lib/aliyun_sdk/common/struct.rb +56 -0
  16. data/lib/aliyun_sdk/oss.rb +16 -0
  17. data/lib/aliyun_sdk/oss/bucket.rb +661 -0
  18. data/lib/aliyun_sdk/oss/client.rb +106 -0
  19. data/lib/aliyun_sdk/oss/config.rb +39 -0
  20. data/lib/aliyun_sdk/oss/download.rb +255 -0
  21. data/lib/aliyun_sdk/oss/exception.rb +108 -0
  22. data/lib/aliyun_sdk/oss/http.rb +338 -0
  23. data/lib/aliyun_sdk/oss/iterator.rb +92 -0
  24. data/lib/aliyun_sdk/oss/multipart.rb +74 -0
  25. data/lib/aliyun_sdk/oss/object.rb +15 -0
  26. data/lib/aliyun_sdk/oss/protocol.rb +1499 -0
  27. data/lib/aliyun_sdk/oss/struct.rb +208 -0
  28. data/lib/aliyun_sdk/oss/upload.rb +238 -0
  29. data/lib/aliyun_sdk/oss/util.rb +89 -0
  30. data/lib/aliyun_sdk/sts.rb +9 -0
  31. data/lib/aliyun_sdk/sts/client.rb +38 -0
  32. data/lib/aliyun_sdk/sts/config.rb +22 -0
  33. data/lib/aliyun_sdk/sts/exception.rb +53 -0
  34. data/lib/aliyun_sdk/sts/protocol.rb +130 -0
  35. data/lib/aliyun_sdk/sts/struct.rb +64 -0
  36. data/lib/aliyun_sdk/sts/util.rb +48 -0
  37. data/lib/aliyun_sdk/version.rb +7 -0
  38. data/spec/aliyun/oss/bucket_spec.rb +597 -0
  39. data/spec/aliyun/oss/client/bucket_spec.rb +554 -0
  40. data/spec/aliyun/oss/client/client_spec.rb +297 -0
  41. data/spec/aliyun/oss/client/resumable_download_spec.rb +220 -0
  42. data/spec/aliyun/oss/client/resumable_upload_spec.rb +413 -0
  43. data/spec/aliyun/oss/http_spec.rb +83 -0
  44. data/spec/aliyun/oss/multipart_spec.rb +686 -0
  45. data/spec/aliyun/oss/object_spec.rb +785 -0
  46. data/spec/aliyun/oss/service_spec.rb +142 -0
  47. data/spec/aliyun/oss/util_spec.rb +50 -0
  48. data/spec/aliyun/sts/client_spec.rb +150 -0
  49. data/spec/aliyun/sts/util_spec.rb +39 -0
  50. data/tests/config.rb +31 -0
  51. data/tests/test_content_encoding.rb +54 -0
  52. data/tests/test_content_type.rb +95 -0
  53. data/tests/test_custom_headers.rb +70 -0
  54. data/tests/test_encoding.rb +77 -0
  55. data/tests/test_large_file.rb +66 -0
  56. data/tests/test_multipart.rb +97 -0
  57. data/tests/test_object_acl.rb +49 -0
  58. data/tests/test_object_key.rb +68 -0
  59. data/tests/test_object_url.rb +69 -0
  60. data/tests/test_resumable.rb +40 -0
  61. metadata +240 -0
@@ -0,0 +1,124 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ $LOAD_PATH.unshift(File.expand_path("../../../../lib", __FILE__))
4
+ require 'yaml'
5
+ require 'aliyun/oss'
6
+
7
+ ##
8
+ # 一般来说用户在上传object和下载object时只需要指定文件名就可以满足需要:
9
+ # - 在上传的时候client会从指定的文件中读取数据上传到OSS
10
+ # - 在下载的时候client会把从OSS下载的数据写入到指定的文件中
11
+ #
12
+ # 在某些情况下用户可能会需要流式地上传和下载:
13
+ # - 用户要写入到object中的数据不能立即得到全部,而是从网络中流式获取,
14
+ # 然后再一段一段地写入到OSS中
15
+ # - 用户要写入到object的数据是经过运算得出,每次得到一部分,用户不希望
16
+ # 保留所有的数据然后一次性写入到OSS
17
+ # - 用户下载的object很大,用户不希望一次性把它们下载到内存中,而是希望
18
+ # 获取一部分就处理一部分;用户也不希望把它先下载到文件中,然后再从文
19
+ # 件中读出来处理,这样会让数据经历不必要的拷贝
20
+ #
21
+ # 当然,对于流式上传的需求,我们可以使用OSS的appendable object来满足。
22
+ # 但是即使是normal object,利用sdk的streaming功能,也可以实现流式上传
23
+ # 和下载。
24
+
25
+ # 初始化OSS client
26
+ AliyunSDK::Common::Logging.set_log_level(Logger::DEBUG)
27
+ conf_file = '~/.oss.yml'
28
+ conf = YAML.load(File.read(File.expand_path(conf_file)))
29
+ bucket = AliyunSDK::OSS::Client.new(
30
+ :endpoint => conf['endpoint'],
31
+ :cname => conf['cname'],
32
+ :access_key_id => conf['access_key_id'],
33
+ :access_key_secret => conf['access_key_secret']).get_bucket(conf['bucket'])
34
+
35
+ # 辅助打印函数
36
+ def demo(msg)
37
+ puts "######### #{msg} ########"
38
+ puts
39
+ yield
40
+ puts "-------------------------"
41
+ puts
42
+ end
43
+
44
+ # 例子1: 归并排序
45
+ # 有两个文件sort.1, sort.2,它们分别存了一些从小到大排列的整数,每个整
46
+ # 数1行,现在要将它们做归并排序的结果上传到OSS中,命名为sort.all
47
+
48
+ local_1, local_2 = 'sort.1', 'sort.2'
49
+ result_object = 'sort.all'
50
+
51
+ File.open(File.expand_path(local_1), 'w') do |f|
52
+ [1001, 2005, 2007, 2011, 2013, 2015].each do |i|
53
+ f.puts(i.to_s)
54
+ end
55
+ end
56
+
57
+ File.open(File.expand_path(local_2), 'w') do |f|
58
+ [2009, 2010, 2012, 2017, 2020, 9999].each do |i|
59
+ f.puts(i.to_s)
60
+ end
61
+ end
62
+
63
+ demo "Streaming upload" do
64
+ bucket.put_object(result_object) do |content|
65
+ f1 = File.open(File.expand_path(local_1))
66
+ f2 = File.open(File.expand_path(local_2))
67
+ v1, v2 = f1.readline, f2.readline
68
+
69
+ until f1.eof? or f2.eof?
70
+ if v1.to_i < v2.to_i
71
+ content << v1
72
+ v1 = f1.readline
73
+ else
74
+ content << v2
75
+ v2 = f2.readline
76
+ end
77
+ end
78
+
79
+ [v1, v2].sort.each{|i| content << i}
80
+ content << f1.readline until f1.eof?
81
+ content << f2.readline until f2.eof?
82
+ end
83
+
84
+ puts "Put object: #{result_object}"
85
+
86
+ # 将文件下载下来查看
87
+ bucket.get_object(result_object, :file => result_object)
88
+ puts "Get object: #{result_object}"
89
+ puts "Content: #{File.read(result_object)}"
90
+ end
91
+
92
+ # 例子2: 下载进度条
93
+ # 下载一个大文件(10M),在下载的过程中打印下载进度
94
+
95
+ large_file = 'large_file'
96
+
97
+ demo "Streaming download" do
98
+ puts "Begin put object: #{large_file}"
99
+ # 利用streaming上传
100
+ bucket.put_object(large_file) do |stream|
101
+ 10.times { stream << "x" * (1024 * 1024) }
102
+ end
103
+
104
+ # 查看object大小
105
+ object_size = bucket.get_object(large_file).size
106
+ puts "Put object: #{large_file}, size: #{object_size}"
107
+
108
+ # 流式下载文件,仅打印进度,不保存文件
109
+ def to_percentile(v)
110
+ "#{(v * 100.0).round(2)} %"
111
+ end
112
+
113
+ puts "Begin download: #{large_file}"
114
+ last_got, got = 0, 0
115
+ bucket.get_object(large_file) do |chunk|
116
+ got += chunk.size
117
+ # 仅在下载进度大于10%的时候打印
118
+ if (got - last_got).to_f / object_size > 0.1
119
+ puts "Progress: #{to_percentile(got.to_f / object_size)}"
120
+ last_got = got
121
+ end
122
+ end
123
+ puts "Get object: #{large_file}, size: #{object_size}"
124
+ end
@@ -0,0 +1,48 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ $LOAD_PATH.unshift(File.expand_path("../../../../lib", __FILE__))
4
+ require 'yaml'
5
+ require 'aliyun/sts'
6
+ require 'aliyun/oss'
7
+
8
+ # 初始化OSS client
9
+ AliyunSDK::Common::Logging.set_log_level(Logger::DEBUG)
10
+ conf_file = '~/.sts.yml'
11
+ conf = YAML.load(File.read(File.expand_path(conf_file)))
12
+
13
+ # 辅助打印函数
14
+ def demo(msg)
15
+ puts "######### #{msg} ########"
16
+ puts
17
+ yield
18
+ puts "-------------------------"
19
+ puts
20
+ end
21
+
22
+ demo "Using STS" do
23
+ sts = AliyunSDK::STS::Client.new(
24
+ :access_key_id => conf['access_key_id'],
25
+ :access_key_secret => conf['access_key_secret'])
26
+
27
+ token = sts.assume_role(
28
+ 'acs:ram::52352:role/aliyunosstokengeneratorrole', 'app-1')
29
+
30
+ client = AliyunSDK::OSS::Client.new(
31
+ :endpoint => 'http://oss-cn-hangzhou.aliyuncs.com',
32
+ :sts_token => token.security_token,
33
+ :access_key_id => token.access_key_id,
34
+ :access_key_secret => token.access_key_secret)
35
+
36
+ unless client.bucket_exists?('bucket-for-sts-test')
37
+ client.create_bucket('bucket-for-sts-test')
38
+ end
39
+
40
+ bucket = client.get_bucket('bucket-for-sts-test')
41
+
42
+ bucket.put_object('hello') { |s| s << 'hello' }
43
+ bucket.put_object('world') { |s| s << 'world' }
44
+
45
+ bucket.list_objects.take(10).each do |obj|
46
+ puts "Object: #{obj.key}, size: #{obj.size}"
47
+ end
48
+ end
@@ -0,0 +1,59 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ $LOAD_PATH.unshift(File.expand_path("../../../../lib", __FILE__))
4
+ require 'yaml'
5
+ require 'aliyun/sts'
6
+
7
+ AliyunSDK::Common::Logging.set_log_level(Logger::DEBUG)
8
+ conf_file = '~/.sts.yml'
9
+ conf = YAML.load(File.read(File.expand_path(conf_file)))
10
+ client = AliyunSDK::STS::Client.new(
11
+ :access_key_id => conf['access_key_id'],
12
+ :access_key_secret => conf['access_key_secret'])
13
+
14
+ # 辅助打印函数
15
+ def demo(msg)
16
+ puts "######### #{msg} ########"
17
+ puts
18
+ yield
19
+ puts "-------------------------"
20
+ puts
21
+ end
22
+
23
+ token = client.assume_role(
24
+ 'acs:ram::52352:role/aliyunosstokengeneratorrole', 'app-1')
25
+
26
+ demo "Assume role" do
27
+ begin
28
+ token = client.assume_role(
29
+ 'acs:ram::52352:role/aliyunosstokengeneratorrole', 'app-1')
30
+
31
+ puts "Credentials for session: #{token.session_name}"
32
+ puts "access key id: #{token.access_key_id}"
33
+ puts "access key secret: #{token.access_key_secret}"
34
+ puts "security token: #{token.security_token}"
35
+ puts "expiration at: #{token.expiration}"
36
+ rescue => e
37
+ puts "AssumeRole failed: #{e.message}"
38
+ end
39
+ end
40
+
41
+ demo "Assume role with policy" do
42
+ begin
43
+ policy = AliyunSDK::STS::Policy.new
44
+ policy.allow(
45
+ ['oss:Get*', 'oss:PutObject'],
46
+ ['acs:oss:*:*:my-bucket', 'acs:oss:*:*:my-bucket/*'])
47
+
48
+ token = client.assume_role(
49
+ 'acs:ram::52352:role/aliyunosstokengeneratorrole', 'app-2', policy, 900)
50
+
51
+ puts "Credentials for session: #{token.session_name}"
52
+ puts "access key id: #{token.access_key_id}"
53
+ puts "access key secret: #{token.access_key_secret}"
54
+ puts "security token: #{token.security_token}"
55
+ puts "expiration at: #{token.expiration}"
56
+ rescue => e
57
+ puts "AssumeRole failed: #{e.message}"
58
+ end
59
+ end
@@ -0,0 +1,6 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require_relative 'version'
4
+ require_relative 'common/struct'
5
+ require_relative 'common/logging'
6
+ require_relative 'common/exception'
@@ -0,0 +1,18 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module AliyunSDK
4
+ module Common
5
+
6
+ ##
7
+ # Base exception class
8
+ #
9
+ class Exception < RuntimeError
10
+ attr_reader :message
11
+
12
+ def initialize(message)
13
+ @message = message
14
+ end
15
+ end
16
+
17
+ end # Common
18
+ end # Aliyun
@@ -0,0 +1,46 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'logger'
4
+
5
+ module AliyunSDK
6
+ module Common
7
+ ##
8
+ # Logging support
9
+ # @example
10
+ # include Logging
11
+ # logger.info(xxx)
12
+ module Logging
13
+
14
+ DEFAULT_LOG_FILE = "./aliyun_sdk.log"
15
+ MAX_NUM_LOG = 100
16
+ ROTATE_SIZE = 10 * 1024 * 1024
17
+
18
+ # level = Logger::DEBUG | Logger::INFO | Logger::ERROR | Logger::FATAL
19
+ def self.set_log_level(level)
20
+ Logging.logger.level = level
21
+ end
22
+
23
+ # 设置日志输出的文件
24
+ def self.set_log_file(file)
25
+ @log_file = file
26
+ end
27
+
28
+ # 获取logger
29
+ def logger
30
+ Logging.logger
31
+ end
32
+
33
+ private
34
+
35
+ def self.logger
36
+ unless @logger
37
+ @logger = Logger.new(
38
+ @log_file ||= DEFAULT_LOG_FILE, MAX_NUM_LOG, ROTATE_SIZE)
39
+ @logger.level = Logger::INFO
40
+ end
41
+ @logger
42
+ end
43
+
44
+ end # logging
45
+ end # Common
46
+ end # Aliyun
@@ -0,0 +1,56 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module AliyunSDK
4
+ module Common
5
+
6
+ # Common structs used. It provides a 'attrs' helper method for
7
+ # subclass to define its attributes. 'attrs' is based on
8
+ # attr_reader and provide additional functionalities for classes
9
+ # that inherits Struct::Base :
10
+ # * the constuctor is provided to accept options and set the
11
+ # corresponding attibute automatically
12
+ # * the #to_s method is rewrite to concatenate the defined
13
+ # attributes keys and values
14
+ # @example
15
+ # class X < Struct::Base
16
+ # attrs :foo, :bar
17
+ # end
18
+ #
19
+ # x.new(:foo => 'hello', :bar => 'world')
20
+ # x.foo # == "hello"
21
+ # x.bar # == "world"
22
+ # x.to_s # == "foo: hello, bar: world"
23
+ module Struct
24
+ class Base
25
+ module AttrHelper
26
+ def attrs(*s)
27
+ define_method(:attrs) {s}
28
+ attr_reader(*s)
29
+ end
30
+ end
31
+
32
+ extend AttrHelper
33
+
34
+ def initialize(opts = {})
35
+ extra_keys = opts.keys - attrs
36
+ unless extra_keys.empty?
37
+ fail Common::Exception,
38
+ "Unexpected extra keys: #{extra_keys.join(', ')}"
39
+ end
40
+
41
+ attrs.each do |attr|
42
+ instance_variable_set("@#{attr}", opts[attr])
43
+ end
44
+ end
45
+
46
+ def to_s
47
+ attrs.map do |attr|
48
+ v = instance_variable_get("@#{attr}")
49
+ "#{attr.to_s}: #{v}"
50
+ end.join(", ")
51
+ end
52
+ end # Base
53
+ end # Struct
54
+
55
+ end # Common
56
+ end # Aliyun
@@ -0,0 +1,16 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require_relative 'common'
4
+ require_relative 'oss/util'
5
+ require_relative 'oss/exception'
6
+ require_relative 'oss/struct'
7
+ require_relative 'oss/config'
8
+ require_relative 'oss/http'
9
+ require_relative 'oss/protocol'
10
+ require_relative 'oss/multipart'
11
+ require_relative 'oss/upload'
12
+ require_relative 'oss/download'
13
+ require_relative 'oss/iterator'
14
+ require_relative 'oss/object'
15
+ require_relative 'oss/bucket'
16
+ require_relative 'oss/client'
@@ -0,0 +1,661 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ module AliyunSDK
4
+ module OSS
5
+ ##
6
+ # Bucket是用户的Object相关的操作的client,主要包括三部分功能:
7
+ # 1. bucket相关:获取/设置bucket的属性(acl, logging, referer,
8
+ # website, lifecycle, cors)
9
+ # 2. object相关:上传、下载、追加、拷贝object等
10
+ # 3. multipart相关:断点续传、断点续载
11
+ class Bucket < Common::Struct::Base
12
+
13
+ attrs :name, :location, :creation_time
14
+
15
+ def initialize(opts = {}, protocol = nil)
16
+ super(opts)
17
+ @protocol = protocol
18
+ end
19
+
20
+ ### Bucket相关的API ###
21
+
22
+ # 获取Bucket的ACL
23
+ # @return [String] Bucket的{OSS::ACL ACL}
24
+ def acl
25
+ @protocol.get_bucket_acl(name)
26
+ end
27
+
28
+ # 设置Bucket的ACL
29
+ # @param acl [String] Bucket的{OSS::ACL ACL}
30
+ def acl=(acl)
31
+ @protocol.put_bucket_acl(name, acl)
32
+ end
33
+
34
+ # 获取Bucket的logging配置
35
+ # @return [BucketLogging] Bucket的logging配置
36
+ def logging
37
+ @protocol.get_bucket_logging(name)
38
+ end
39
+
40
+ # 设置Bucket的logging配置
41
+ # @param logging [BucketLogging] logging配置
42
+ def logging=(logging)
43
+ if logging.enabled?
44
+ @protocol.put_bucket_logging(name, logging)
45
+ else
46
+ @protocol.delete_bucket_logging(name)
47
+ end
48
+ end
49
+
50
+ # 获取Bucket的website配置
51
+ # @return [BucketWebsite] Bucket的website配置
52
+ def website
53
+ begin
54
+ w = @protocol.get_bucket_website(name)
55
+ rescue ServerError => e
56
+ raise unless e.http_code == 404
57
+ end
58
+
59
+ w || BucketWebsite.new
60
+ end
61
+
62
+ # 设置Bucket的website配置
63
+ # @param website [BucketWebsite] website配置
64
+ def website=(website)
65
+ if website.enabled?
66
+ @protocol.put_bucket_website(name, website)
67
+ else
68
+ @protocol.delete_bucket_website(name)
69
+ end
70
+ end
71
+
72
+ # 获取Bucket的Referer配置
73
+ # @return [BucketReferer] Bucket的Referer配置
74
+ def referer
75
+ @protocol.get_bucket_referer(name)
76
+ end
77
+
78
+ # 设置Bucket的Referer配置
79
+ # @param referer [BucketReferer] Referer配置
80
+ def referer=(referer)
81
+ @protocol.put_bucket_referer(name, referer)
82
+ end
83
+
84
+ # 获取Bucket的生命周期配置
85
+ # @return [Array<OSS::LifeCycleRule>] Bucket的生命周期规则,如果
86
+ # 当前Bucket未设置lifecycle,则返回[]
87
+ def lifecycle
88
+ begin
89
+ r = @protocol.get_bucket_lifecycle(name)
90
+ rescue ServerError => e
91
+ raise unless e.http_code == 404
92
+ end
93
+
94
+ r || []
95
+ end
96
+
97
+ # 设置Bucket的生命周期配置
98
+ # @param rules [Array<OSS::LifeCycleRule>] 生命
99
+ # 周期配置规则
100
+ # @see OSS::LifeCycleRule 查看如何设置生命周期规则
101
+ # @note 如果rules为空,则会删除这个bucket上的lifecycle配置
102
+ def lifecycle=(rules)
103
+ if rules.empty?
104
+ @protocol.delete_bucket_lifecycle(name)
105
+ else
106
+ @protocol.put_bucket_lifecycle(name, rules)
107
+ end
108
+ end
109
+
110
+ # 获取Bucket的跨域资源共享(CORS)的规则
111
+ # @return [Array<OSS::CORSRule>] Bucket的CORS规则,如果当前
112
+ # Bucket未设置CORS规则,则返回[]
113
+ def cors
114
+ begin
115
+ r = @protocol.get_bucket_cors(name)
116
+ rescue ServerError => e
117
+ raise unless e.http_code == 404
118
+ end
119
+
120
+ r || []
121
+ end
122
+
123
+ # 设置Bucket的跨域资源共享(CORS)的规则
124
+ # @param rules [Array<OSS::CORSRule>] CORS规则
125
+ # @note 如果rules为空,则会删除这个bucket上的CORS配置
126
+ def cors=(rules)
127
+ if rules.empty?
128
+ @protocol.delete_bucket_cors(name)
129
+ else
130
+ @protocol.set_bucket_cors(name, rules)
131
+ end
132
+ end
133
+
134
+ ### Object相关的API ###
135
+
136
+
137
+ # 列出bucket中的object
138
+ # @param opts [Hash] 查询选项
139
+ # @option opts [String] :prefix 返回的object的前缀,如果设置则只
140
+ # 返回那些名字以它为前缀的object
141
+ # @option opts [String] :marker 如果设置,则只返回名字在它之后
142
+ # (字典序,不包含marker)的object
143
+ # @option opts [String] :delimiter 用于获取公共前缀的分隔符,从
144
+ # 前缀后面开始到第一个分隔符出现的位置之前的字符,作为公共前缀。
145
+ # @example
146
+ # 假设我们有如下objects:
147
+ # /foo/bar/obj1
148
+ # /foo/bar/obj2
149
+ # ...
150
+ # /foo/bar/obj9999999
151
+ # /foo/xxx/
152
+ # 用'foo/'作为前缀, '/'作为分隔符, 则得到的公共前缀是:
153
+ # '/foo/bar/', '/foo/xxx/'。它们恰好就是目录'/foo/'下的所有子目
154
+ # 录。用delimiter获取公共前缀的方法避免了查询当前bucket下的所有
155
+ # object(可能数量巨大),是用于模拟目录结构的常用做法。
156
+ # @return [Enumerator<Object>] 其中Object可能是{OSS::Object},也
157
+ # 可能是{String},此时它是一个公共前缀
158
+ # @example
159
+ # all = bucket.list_objects
160
+ # all.each do |i|
161
+ # if i.is_a?(Object)
162
+ # puts "Object: #{i.key}"
163
+ # else
164
+ # puts "Common prefix: #{i}"
165
+ # end
166
+ # end
167
+ def list_objects(opts = {})
168
+ Iterator::Objects.new(
169
+ @protocol, name, opts.merge(encoding: KeyEncoding::URL)).to_enum
170
+ end
171
+
172
+ # 向Bucket中上传一个object
173
+ # @param key [String] Object的名字
174
+ # @param opts [Hash] 上传object时的选项(可选)
175
+ # @option opts [String] :file 设置所上传的文件
176
+ # @option opts [String] :content_type 设置所上传的内容的
177
+ # Content-Type,默认是application/octet-stream
178
+ # @option opts [Hash] :metas 设置object的meta,这是一些用户自定
179
+ # 义的属性,它们会和object一起存储,在{#get_object}的时候会
180
+ # 返回这些meta。属性的key不区分大小写。例如:{ 'year' => '2015' }
181
+ # @option opts [Callback] :callback 指定操作成功后OSS的
182
+ # 上传回调,上传成功后OSS会向用户的应用服务器发一个HTTP POST请
183
+ # 求,`:callback`参数指定这个请求的相关参数
184
+ # @option opts [Hash] :headers 指定请求的HTTP Header,不区分大小
185
+ # 写。这里指定的值会覆盖通过`:content_type`和`:metas`设置的值。
186
+ # @yield [HTTP::StreamWriter] 如果调用的时候传递了block,则写入
187
+ # 到object的数据由block指定
188
+ # @example 流式上传数据
189
+ # put_object('x'){ |stream| 100.times { |i| stream << i.to_s } }
190
+ # put_object('x'){ |stream| stream << get_data }
191
+ # @example 上传文件
192
+ # put_object('x', :file => '/tmp/x')
193
+ # @example 指定Content-Type和metas
194
+ # put_object('x', :file => '/tmp/x', :content_type => 'text/html',
195
+ # :metas => {'year' => '2015', 'people' => 'mary'})
196
+ # @example 指定Callback
197
+ # callback = AliyunSDK::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
+ # bucket.put_object('files/hello', callback: callback)
204
+ # @raise [CallbackError] 如果文件上传成功而Callback调用失败,抛
205
+ # 出此错误
206
+ # @note 如果opts中指定了`:file`,则block会被忽略
207
+ # @note 如果指定了`:callback`,则可能文件上传成功,但是callback
208
+ # 执行失败,此时会抛出{OSS::CallbackError},用户可以选择接住这
209
+ # 个异常,以忽略Callback调用错误
210
+ def put_object(key, opts = {}, &block)
211
+ args = opts.dup
212
+
213
+ file = args[:file]
214
+ args[:content_type] ||= get_content_type(file) if file
215
+ args[:content_type] ||= get_content_type(key)
216
+
217
+ if file
218
+ @protocol.put_object(name, key, args) do |sw|
219
+ File.open(File.expand_path(file), 'rb') do |f|
220
+ sw << f.read(Protocol::STREAM_CHUNK_SIZE) until f.eof?
221
+ end
222
+ end
223
+ else
224
+ @protocol.put_object(name, key, args, &block)
225
+ end
226
+ end
227
+
228
+ # 从Bucket中下载一个object
229
+ # @param key [String] Object的名字
230
+ # @param opts [Hash] 下载Object的选项(可选)
231
+ # @option opts [Array<Integer>] :range 指定下载object的部分数据,
232
+ # range应只包含两个数字,表示一个*左开右闭*的bytes range
233
+ # @option opts [String] :file 指定将下载的object写入到文件中
234
+ # @option opts [Hash] :condition 指定下载object需要满足的条件
235
+ # * :if_modified_since (Time) 指定如果object的修改时间晚于这个值,则下载
236
+ # * :if_unmodified_since (Time) 指定如果object从这个时间后再无修改,则下载
237
+ # * :if_match_etag (String) 指定如果object的etag等于这个值,则下载
238
+ # * :if_unmatch_etag (String) 指定如果object的etag不等于这个值,则下载
239
+ # @option opts [Hash] :headers 指定请求的HTTP Header,不区分大小
240
+ # 写。这里指定的值会覆盖通过`:range`和`:condition`设置的值。
241
+ # @option opts [Hash] :rewrite 指定下载object时Server端返回的响应头部字段的值
242
+ # * :content_type (String) 指定返回的响应中Content-Type的值
243
+ # * :content_language (String) 指定返回的响应中Content-Language的值
244
+ # * :expires (Time) 指定返回的响应中Expires的值
245
+ # * :cache_control (String) 指定返回的响应中Cache-Control的值
246
+ # * :content_disposition (String) 指定返回的响应中Content-Disposition的值
247
+ # * :content_encoding (String) 指定返回的响应中Content-Encoding的值
248
+ # @return [OSS::Object] 返回Object对象
249
+ # @yield [String] 如果调用的时候传递了block,则获取到的object的数据交由block处理
250
+ # @example 流式下载文件
251
+ # get_object('x'){ |chunk| handle_chunk_data(chunk) }
252
+ # @example 下载到本地文件
253
+ # get_object('x', :file => '/tmp/x')
254
+ # @example 指定检查条件
255
+ # get_object('x', :file => '/tmp/x', :condition => {:if_match_etag => 'etag'})
256
+ # @example 指定重写响应的header信息
257
+ # get_object('x', :file => '/tmp/x', :rewrite => {:content_type => 'text/html'})
258
+ # @note 如果opts中指定了`:file`,则block会被忽略
259
+ # @note 如果既没有指定`:file`也没有指定block,则只获取Object
260
+ # meta而不下载Object内容
261
+ def get_object(key, opts = {}, &block)
262
+ obj = nil
263
+ file = opts[:file]
264
+ if file
265
+ File.open(File.expand_path(file), 'wb') do |f|
266
+ obj = @protocol.get_object(name, key, opts) do |chunk|
267
+ f.write(chunk)
268
+ end
269
+ end
270
+ elsif block
271
+ obj = @protocol.get_object(name, key, opts, &block)
272
+ else
273
+ obj = @protocol.get_object_meta(name, key, opts)
274
+ end
275
+
276
+ obj
277
+ end
278
+
279
+ # 更新Object的metas
280
+ # @param key [String] Object的名字
281
+ # @param metas [Hash] Object的meta
282
+ # @param conditions [Hash] 指定更新Object meta需要满足的条件,
283
+ # 同{#get_object}
284
+ # @return [Hash] 更新后文件的信息
285
+ # * :etag [String] 更新后文件的ETag
286
+ # * :last_modified [Time] 更新后文件的最后修改时间
287
+ def update_object_metas(key, metas, conditions = {})
288
+ @protocol.copy_object(
289
+ name, key, key,
290
+ :meta_directive => MetaDirective::REPLACE,
291
+ :metas => metas,
292
+ :condition => conditions)
293
+ end
294
+
295
+ # 判断一个object是否存在
296
+ # @param key [String] Object的名字
297
+ # @return [Boolean] 如果Object存在返回true,否则返回false
298
+ def object_exists?(key)
299
+ begin
300
+ get_object(key)
301
+ return true
302
+ rescue ServerError => e
303
+ return false if e.http_code == 404
304
+ raise e
305
+ end
306
+
307
+ false
308
+ end
309
+
310
+ alias :object_exist? :object_exists?
311
+
312
+ # 向Bucket中的object追加内容。如果object不存在,则创建一个
313
+ # Appendable Object。
314
+ # @param key [String] Object的名字
315
+ # @param opts [Hash] 上传object时的选项(可选)
316
+ # @option opts [String] :file 指定追加的内容从文件中读取
317
+ # @option opts [String] :content_type 设置所上传的内容的
318
+ # Content-Type,默认是application/octet-stream
319
+ # @option opts [Hash] :metas 设置object的meta,这是一些用户自定
320
+ # 义的属性,它们会和object一起存储,在{#get_object}的时候会
321
+ # 返回这些meta。属性的key不区分大小写。例如:{ 'year' => '2015' }
322
+ # @option opts [Hash] :headers 指定请求的HTTP Header,不区分大小
323
+ # 写。这里指定的值会覆盖通过`:content_type`和`:metas`设置的值。
324
+ # @example 流式上传数据
325
+ # pos = append_object('x', 0){ |stream| 100.times { |i| stream << i.to_s } }
326
+ # append_object('x', pos){ |stream| stream << get_data }
327
+ # @example 上传文件
328
+ # append_object('x', 0, :file => '/tmp/x')
329
+ # @example 指定Content-Type和metas
330
+ # append_object('x', 0, :file => '/tmp/x', :content_type => 'text/html',
331
+ # :metas => {'year' => '2015', 'people' => 'mary'})
332
+ # @return [Integer] 返回下次append的位置
333
+ # @yield [HTTP::StreamWriter] 同 {#put_object}
334
+ def append_object(key, pos, opts = {}, &block)
335
+ args = opts.dup
336
+
337
+ file = args[:file]
338
+ args[:content_type] ||= get_content_type(file) if file
339
+ args[:content_type] ||= get_content_type(key)
340
+
341
+ if file
342
+ next_pos = @protocol.append_object(name, key, pos, args) do |sw|
343
+ File.open(File.expand_path(file), 'rb') do |f|
344
+ sw << f.read(Protocol::STREAM_CHUNK_SIZE) until f.eof?
345
+ end
346
+ end
347
+ else
348
+ next_pos = @protocol.append_object(name, key, pos, args, &block)
349
+ end
350
+
351
+ next_pos
352
+ end
353
+
354
+ # 将Bucket中的一个object拷贝成另外一个object
355
+ # @param source [String] 源object名字
356
+ # @param dest [String] 目标object名字
357
+ # @param opts [Hash] 拷贝object时的选项(可选)
358
+ # @option opts [String] :src_bucket 源object所属的Bucket,默认与
359
+ # 目标文件为同一个Bucket。源Bucket与目标Bucket必须属于同一个Region。
360
+ # @option opts [String] :acl 目标文件的acl属性,默认为private
361
+ # @option opts [String] :meta_directive 指定是否拷贝源object的
362
+ # meta信息,默认为{OSS::MetaDirective::COPY}:即拷贝object的时
363
+ # 候也拷贝meta信息。
364
+ # @option opts [Hash] :metas 设置object的meta,这是一些用户自定
365
+ # 义的属性,它们会和object一起存储,在{#get_object}的时候会
366
+ # 返回这些meta。属性的key不区分大小写。例如:{ 'year' => '2015'
367
+ # }。如果:meta_directive为{OSS::MetaDirective::COPY},则:metas
368
+ # 会被忽略。
369
+ # @option opts [Hash] :condition 指定拷贝object需要满足的条件,
370
+ # 同 {#get_object}
371
+ # @return [Hash] 目标文件的信息
372
+ # * :etag [String] 目标文件的ETag
373
+ # * :last_modified [Time] 目标文件的最后修改时间
374
+ def copy_object(source, dest, opts = {})
375
+ args = opts.dup
376
+
377
+ args[:content_type] ||= get_content_type(dest)
378
+ @protocol.copy_object(name, source, dest, args)
379
+ end
380
+
381
+ # 删除一个object
382
+ # @param key [String] Object的名字
383
+ def delete_object(key)
384
+ @protocol.delete_object(name, key)
385
+ end
386
+
387
+ # 批量删除object
388
+ # @param keys [Array<String>] Object的名字集合
389
+ # @param opts [Hash] 删除object的选项(可选)
390
+ # @option opts [Boolean] :quiet 指定是否允许Server返回成功删除的
391
+ # object,默认为false,即返回删除结果
392
+ # @return [Array<String>] 成功删除的object的名字,如果指定
393
+ # 了:quiet参数,则返回[]
394
+ def batch_delete_objects(keys, opts = {})
395
+ @protocol.batch_delete_objects(
396
+ name, keys, opts.merge(encoding: KeyEncoding::URL))
397
+ end
398
+
399
+ # 设置object的ACL
400
+ # @param key [String] Object的名字
401
+ # @param acl [String] Object的{OSS::ACL ACL}
402
+ def set_object_acl(key, acl)
403
+ @protocol.put_object_acl(name, key, acl)
404
+ end
405
+
406
+ # 获取object的ACL
407
+ # @param key [String] Object的名字
408
+ # @return [String] object的{OSS::ACL ACL}
409
+ def get_object_acl(key)
410
+ @protocol.get_object_acl(name, key)
411
+ end
412
+
413
+ # 获取object的CORS规则
414
+ # @param key [String] Object的名字
415
+ # @return [OSS::CORSRule]
416
+ def get_object_cors(key)
417
+ @protocol.get_object_cors(name, key)
418
+ end
419
+
420
+ ##
421
+ # 断点续传相关的API
422
+ #
423
+
424
+ # 上传一个本地文件到bucket中的一个object,支持断点续传。指定的文
425
+ # 件会被分成多个分片进行上传,只有所有分片都上传成功整个文件才
426
+ # 上传成功。
427
+ # @param key [String] Object的名字
428
+ # @param file [String] 本地文件的路径
429
+ # @param opts [Hash] 上传文件的可选项
430
+ # @option opts [String] :content_type 设置所上传的内容的
431
+ # Content-Type,默认是application/octet-stream
432
+ # @option opts [Hash] :metas 设置object的meta,这是一些用户自定
433
+ # 义的属性,它们会和object一起存储,在{#get_object}的时候会
434
+ # 返回这些meta。属性的key不区分大小写。例如:{ 'year' => '2015' }
435
+ # @option opts [Integer] :part_size 设置分片上传时每个分片的大小,
436
+ # 默认为10 MB。断点上传最多允许10000个分片,如果文件大于10000个
437
+ # 分片的大小,则每个分片的大小会大于10MB。
438
+ # @option opts [String] :cpt_file 断点续传的checkpoint文件,如果
439
+ # 指定的cpt文件不存在,则会在file所在目录创建一个默认的cpt文件,
440
+ # 命名方式为:file.cpt,其中file是用户要上传的文件。在上传的过
441
+ # 程中会不断更新此文件,成功完成上传后会删除此文件;如果指定的
442
+ # cpt文件已存在,则从cpt文件中记录的点继续上传。
443
+ # @option opts [Boolean] :disable_cpt 是否禁用checkpoint功能,如
444
+ # 果设置为true,则在上传的过程中不会写checkpoint文件,这意味着
445
+ # 上传失败后不能断点续传,而只能重新上传整个文件。如果这个值为
446
+ # true,则:cpt_file会被忽略。
447
+ # @option opts [Callback] :callback 指定文件上传成功后OSS的
448
+ # 上传回调,上传成功后OSS会向用户的应用服务器发一个HTTP POST请
449
+ # 求,`:callback`参数指定这个请求的相关参数
450
+ # @option opts [Hash] :headers 指定请求的HTTP Header,不区分大小
451
+ # 写。这里指定的值会覆盖通过`:content_type`和`:metas`设置的值。
452
+ # @yield [Float] 如果调用的时候传递了block,则会将上传进度交由
453
+ # block处理,进度值是一个0-1之间的小数
454
+ # @raise [CheckpointBrokenError] 如果cpt文件被损坏,则抛出此错误
455
+ # @raise [FileInconsistentError] 如果指定的文件与cpt中记录的不一
456
+ # 致,则抛出此错误
457
+ # @raise [CallbackError] 如果文件上传成功而Callback调用失败,抛
458
+ # 出此错误
459
+ # @example
460
+ # bucket.resumable_upload('my-object', '/tmp/x') do |p|
461
+ # puts "Progress: #{(p * 100).round(2)} %"
462
+ # end
463
+ # @example 指定Callback
464
+ # callback = AliyunSDK::OSS::Callback.new(
465
+ # url: 'http://10.101.168.94:1234/callback',
466
+ # query: {user: 'put_object'},
467
+ # body: 'bucket=${bucket}&object=${object}'
468
+ # )
469
+ #
470
+ # bucket.resumable_upload('files/hello', '/tmp/x', callback: callback)
471
+ # @note 如果指定了`:callback`,则可能文件上传成功,但是callback
472
+ # 执行失败,此时会抛出{OSS::CallbackError},用户可以选择接住这
473
+ # 个异常,以忽略Callback调用错误
474
+ def resumable_upload(key, file, opts = {}, &block)
475
+ args = opts.dup
476
+
477
+ args[:content_type] ||= get_content_type(file)
478
+ args[:content_type] ||= get_content_type(key)
479
+ cpt_file = args[:cpt_file] || get_cpt_file(file)
480
+
481
+ Multipart::Upload.new(
482
+ @protocol, options: args,
483
+ progress: block,
484
+ object: key, bucket: name, creation_time: Time.now,
485
+ file: File.expand_path(file), cpt_file: cpt_file
486
+ ).run
487
+ end
488
+
489
+ # 下载bucket中的一个object到本地文件,支持断点续传。指定的object
490
+ # 会被分成多个分片进行下载,只有所有的分片都下载成功整个object才
491
+ # 下载成功。对于每个下载的分片,会在file所在目录建立一个临时文件
492
+ # file.part.N,下载成功后这些part文件会被合并成最后的file然后删
493
+ # 除。
494
+ # @param key [String] Object的名字
495
+ # @param file [String] 本地文件的路径
496
+ # @param opts [Hash] 下载文件的可选项
497
+ # @option opts [Integer] :part_size 设置分片上传时每个分片的大小,
498
+ # 默认为10 MB。断点下载最多允许100个分片,如果文件大于100个分片,
499
+ # 则每个分片的大小会大于10 MB
500
+ # @option opts [String] :cpt_file 断点续传的checkpoint文件,如果
501
+ # 指定的cpt文件不存在,则会在file所在目录创建一个默认的cpt文件,
502
+ # 命名方式为:file.cpt,其中file是用户要下载的文件名。在下载的过
503
+ # 程中会不断更新此文件,成功完成下载后会删除此文件;如果指定的
504
+ # cpt文件已存在,则从cpt文件中记录的点继续下载。
505
+ # @option opts [Boolean] :disable_cpt 是否禁用checkpoint功能,如
506
+ # 果设置为true,则在下载的过程中不会写checkpoint文件,这意味着
507
+ # 下载失败后不能断点续传,而只能重新下载整个文件。如果这个值为true,
508
+ # 则:cpt_file会被忽略。
509
+ # @option opts [Hash] :condition 指定下载object需要满足的条件,
510
+ # 同 {#get_object}
511
+ # @option opts [Hash] :headers 指定请求的HTTP Header,不区分大小
512
+ # 写。这里指定的值会覆盖通过`:condition`设置的值。
513
+ # @option opts [Hash] :rewrite 指定下载object时Server端返回的响
514
+ # 应头部字段的值,同 {#get_object}
515
+ # @yield [Float] 如果调用的时候传递了block,则会将下载进度交由
516
+ # block处理,进度值是一个0-1之间的小数
517
+ # @raise [CheckpointBrokenError] 如果cpt文件被损坏,则抛出此错误
518
+ # @raise [ObjectInconsistentError] 如果指定的object的etag与cpt文
519
+ # 件中记录的不一致,则抛出错误
520
+ # @raise [PartMissingError] 如果已下载的部分(.part文件)找不到,
521
+ # 则抛出此错误
522
+ # @raise [PartInconsistentError] 如果已下载的部分(.part文件)的
523
+ # MD5值与cpt文件记录的不一致,则抛出此错误
524
+ # @note 已经下载的部分会在file所在的目录创建.part文件,命名方式
525
+ # 为file.part.N
526
+ # @example
527
+ # bucket.resumable_download('my-object', '/tmp/x') do |p|
528
+ # puts "Progress: #{(p * 100).round(2)} %"
529
+ # end
530
+ def resumable_download(key, file, opts = {}, &block)
531
+ args = opts.dup
532
+
533
+ args[:content_type] ||= get_content_type(file)
534
+ args[:content_type] ||= get_content_type(key)
535
+ cpt_file = args[:cpt_file] || get_cpt_file(file)
536
+
537
+ Multipart::Download.new(
538
+ @protocol, options: args,
539
+ progress: block,
540
+ object: key, bucket: name, creation_time: Time.now,
541
+ file: File.expand_path(file), cpt_file: cpt_file
542
+ ).run
543
+ end
544
+
545
+ # 列出此Bucket中正在进行的multipart上传请求,不包括已经完成或者
546
+ # 被取消的。
547
+ # @param [Hash] opts 可选项
548
+ # @option opts [String] :key_marker object key的标记,根据有没有
549
+ # 设置:id_marker,:key_marker的含义不同:
550
+ # 1. 如果未设置:id_marker,则只返回object key在:key_marker之后
551
+ # (字典序,不包含marker)的upload请求
552
+ # 2. 如果设置了:id_marker,则返回object key在:key_marker之后
553
+ # (字典序,不包含marker)的uplaod请求*和*Object
554
+ # key与:key_marker相等,*且*upload id在:id_marker之后(字母
555
+ # 表顺序排序,不包含marker)的upload请求
556
+ # @option opts [String] :id_marker upload id的标记,如
557
+ # 果:key_marker没有设置,则此参数会被忽略;否则与:key_marker一起
558
+ # 决定返回的结果(见上)
559
+ # @option opts [String] :prefix 如果指定,则只返回object key中符
560
+ # 合指定前缀的upload请求
561
+ # @return [Enumerator<Multipart::Transaction>] 其中每一个元素表
562
+ # 示一个upload请求
563
+ # @example
564
+ # key_marker = 1, id_marker = null
565
+ # # return <2, 0>, <2, 1>, <3, 0> ...
566
+ # key_marker = 1, id_marker = 5
567
+ # # return <1, 6>, <1, 7>, <2, 0>, <3, 0> ...
568
+ def list_uploads(opts = {})
569
+ Iterator::Uploads.new(
570
+ @protocol, name, opts.merge(encoding: KeyEncoding::URL)).to_enum
571
+ end
572
+
573
+ # 取消一个multipart上传请求,一般用于清除Bucket下因断点上传而产
574
+ # 生的文件碎片。成功取消后属于这个上传请求的分片都会被清除。
575
+ # @param [String] upload_id 上传请求的id,可通过{#list_uploads}
576
+ # 获得
577
+ # @param [String] key Object的名字
578
+ def abort_upload(upload_id, key)
579
+ @protocol.abort_multipart_upload(name, key, upload_id)
580
+ end
581
+
582
+ # 获取Bucket的URL
583
+ # @return [String] Bucket的URL
584
+ def bucket_url
585
+ @protocol.get_request_url(name)
586
+ end
587
+
588
+ # 获取Object的URL
589
+ # @param [String] key Object的key
590
+ # @param [Boolean] sign 是否对URL进行签名,默认为是
591
+ # @param [Fixnum] expiry URL的有效时间,单位为秒,默认为60s
592
+ # @return [String] 用于直接访问Object的URL
593
+ def object_url(key, sign = true, expiry = 60)
594
+ url = @protocol.get_request_url(name, key)
595
+ return url unless sign
596
+
597
+ expires = Time.now.to_i + expiry
598
+ query = {
599
+ 'Expires' => expires.to_s,
600
+ 'OSSAccessKeyId' => CGI.escape(access_key_id)
601
+ }
602
+
603
+ sub_res = []
604
+ if @protocol.get_sts_token
605
+ sub_res << "security-token=#{@protocol.get_sts_token}"
606
+ query['security-token'] = CGI.escape(@protocol.get_sts_token)
607
+ end
608
+
609
+ resource = "/#{name}/#{key}"
610
+ unless sub_res.empty?
611
+ resource << "?#{sub_res.join('&')}"
612
+ end
613
+
614
+ string_to_sign = "" <<
615
+ "GET\n" << # method
616
+ "\n" << # Content-MD5
617
+ "\n" << # Content-Type
618
+ "#{expires}\n" <<
619
+ "#{resource}"
620
+
621
+ signature = sign(string_to_sign)
622
+ query_string =
623
+ query.merge('Signature' => CGI.escape(signature))
624
+ .map { |k, v| "#{k}=#{v}" }.join('&')
625
+
626
+ [url, query_string].join('?')
627
+ end
628
+
629
+ # 获取用户所设置的ACCESS_KEY_ID
630
+ # @return [String] 用户的ACCESS_KEY_ID
631
+ def access_key_id
632
+ @protocol.get_access_key_id
633
+ end
634
+
635
+ # 用ACCESS_KEY_SECRET对内容进行签名
636
+ # @param [String] string_to_sign 要进行签名的内容
637
+ # @return [String] 生成的签名
638
+ def sign(string_to_sign)
639
+ @protocol.sign(string_to_sign)
640
+ end
641
+
642
+ private
643
+ # Infer the file's content type using MIME::Types
644
+ # @param file [String] the file path
645
+ # @return [String] the infered content type or nil if it fails
646
+ # to infer the content type
647
+ def get_content_type(file)
648
+ t = MIME::Types.of(file)
649
+ t.first.content_type unless t.empty?
650
+ end
651
+
652
+ # Get the checkpoint file path for file
653
+ # @param file [String] the file path
654
+ # @return [String] the checkpoint file path
655
+ def get_cpt_file(file)
656
+ "#{File.expand_path(file)}.cpt"
657
+ end
658
+
659
+ end # Bucket
660
+ end # OSS
661
+ end # Aliyun