cos 0.1.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +12 -0
  3. data/.rspec +2 -0
  4. data/.travis.yml +13 -2
  5. data/Gemfile +4 -1
  6. data/LICENSE +191 -0
  7. data/README.md +2014 -17
  8. data/Rakefile +23 -6
  9. data/bin/cos +325 -0
  10. data/bin/setup +1 -3
  11. data/cos.gemspec +24 -13
  12. data/lib/cos.rb +41 -4
  13. data/lib/cos/api.rb +289 -0
  14. data/lib/cos/bucket.rb +731 -0
  15. data/lib/cos/checkpoint.rb +62 -0
  16. data/lib/cos/client.rb +58 -0
  17. data/lib/cos/config.rb +102 -0
  18. data/lib/cos/dir.rb +301 -0
  19. data/lib/cos/download.rb +252 -0
  20. data/lib/cos/exception.rb +62 -0
  21. data/lib/cos/file.rb +152 -0
  22. data/lib/cos/http.rb +95 -0
  23. data/lib/cos/logging.rb +47 -0
  24. data/lib/cos/resource.rb +201 -0
  25. data/lib/cos/signature.rb +119 -0
  26. data/lib/cos/slice.rb +292 -0
  27. data/lib/cos/struct.rb +49 -0
  28. data/lib/cos/tree.rb +165 -0
  29. data/lib/cos/util.rb +82 -0
  30. data/lib/cos/version.rb +2 -2
  31. data/spec/cos/bucket_spec.rb +562 -0
  32. data/spec/cos/client_spec.rb +77 -0
  33. data/spec/cos/dir_spec.rb +195 -0
  34. data/spec/cos/download_spec.rb +105 -0
  35. data/spec/cos/http_spec.rb +70 -0
  36. data/spec/cos/signature_spec.rb +83 -0
  37. data/spec/cos/slice_spec.rb +302 -0
  38. data/spec/cos/struct_spec.rb +38 -0
  39. data/spec/cos/tree_spec.rb +322 -0
  40. data/spec/cos/util_spec.rb +106 -0
  41. data/test/download_test.rb +44 -0
  42. data/test/list_test.rb +43 -0
  43. data/test/upload_test.rb +48 -0
  44. metadata +132 -21
  45. data/.idea/.name +0 -1
  46. data/.idea/cos.iml +0 -49
  47. data/.idea/encodings.xml +0 -6
  48. data/.idea/misc.xml +0 -14
  49. data/.idea/modules.xml +0 -8
  50. data/.idea/workspace.xml +0 -465
  51. data/bin/console +0 -14
@@ -0,0 +1,289 @@
1
+ # coding: utf-8
2
+
3
+ require 'json'
4
+ require 'rest-client'
5
+
6
+ module COS
7
+
8
+ # 腾讯云对象存储服务RestfulAPI
9
+ # @see {http://www.qcloud.com/wiki/RESTful_API%E6%96%87%E6%A1%A3}
10
+ class API
11
+
12
+ attr_reader :config, :http
13
+
14
+ # 初始化
15
+ #
16
+ # @example
17
+ # COS::API.new(config)
18
+ #
19
+ # @param config [COS::Config] 客户端设置
20
+ #
21
+ # @see COS::Config
22
+ def initialize(config)
23
+ @config = config
24
+ @http = COS::HTTP.new(config)
25
+ end
26
+
27
+ # 创建目录
28
+ #
29
+ # @see {http://www.qcloud.com/wiki/%E5%88%9B%E5%BB%BA%E7%9B%AE%E5%BD%95:}
30
+ #
31
+ # @param path [String] 目录路径, 如: 'path1', 'path1/path2', sdk会补齐末尾的 '/'
32
+ # @param options [Hash] 高级参数
33
+ # @option options [String] :biz_attr 目录属性, 业务端维护
34
+ # @option options [String] :bucket bucket名称
35
+ #
36
+ # @return Hash
37
+ # * :ctime [String] 创建时间Unix时间戳
38
+ # * :resource_path [String] 创建的资源路径
39
+ #
40
+ # @raise [ServerError] 服务端异常返回
41
+ def create_folder(path, options = {})
42
+ bucket = config.get_bucket(options[:bucket])
43
+ sign = http.signature.multiple(bucket)
44
+ payload = {op: 'create', biz_attr: options[:biz_attr]}
45
+ resource_path = Util.get_resource_path(config.app_id, bucket, path)
46
+
47
+ http.post(resource_path, {}, sign, payload.to_json)
48
+ .merge({name: resource_path.split('/').at(-1)})
49
+ end
50
+
51
+ # 上传文件(完整上传)
52
+ #
53
+ # @see {http://www.qcloud.com/wiki/%E5%88%9B%E5%BB%BA%E6%96%87%E4%BB%B6:_(%E5%AE%8C%E6%95%B4%E4%B8%8A%E4%BC%A0)}
54
+ #
55
+ # @param path [String] 目录路径, 如: '/', 'path1', 'path1/path2', sdk会补齐末尾的 '/'
56
+ # @param file_name [String] 文件名
57
+ # @param file_src [String] 本地文件路径
58
+ # @param options [Hash] 高级参数
59
+ # @option options [String] :biz_attr 目录属性, 业务端维护
60
+ # @option options [String] :bucket bucket名称
61
+ #
62
+ # @return Hash
63
+ # * :access_url [String] 生成的文件下载url
64
+ # * :url [String] 操作文件的url
65
+ # * :resource_path [String] 资源路径
66
+ #
67
+ # @raise [ServerError] 服务端异常返回
68
+ def upload(path, file_name, file_src, options = {})
69
+ bucket = config.get_bucket(options[:bucket])
70
+ sign = http.signature.multiple(bucket)
71
+ resource_path = Util.get_resource_path(config.app_id, bucket, path, file_name)
72
+
73
+ payload = {
74
+ op: 'upload',
75
+ sha: Util.file_sha1(file_src),
76
+ filecontent: File.new(file_src, 'rb'),
77
+ biz_attr: options[:biz_attr]
78
+ }
79
+
80
+ http.post(resource_path, {}, sign, payload)
81
+ end
82
+
83
+ # 上传文件(分片上传)
84
+ #
85
+ # @see {http://www.qcloud.com/wiki/%E5%88%9B%E5%BB%BA%E6%96%87%E4%BB%B6(%E5%88%86%E7%89%87%E4%B8%8A%E4%BC%A0,_%E7%AC%AC%E4%B8%80%E7%89%87):}
86
+ # @see {http://www.qcloud.com/wiki/%E5%88%9B%E5%BB%BA%E6%96%87%E4%BB%B6(%E5%88%86%E7%89%87%E4%B8%8A%E4%BC%A0,_%E5%90%8E%E7%BB%AD%E5%88%86%E7%89%87)}
87
+ #
88
+ # @param path [String] 目录路径, 如: '/', 'path1', 'path1/path2', sdk会补齐末尾的 '/'
89
+ # @param file_name [String] 文件名
90
+ # @param file_src [String] 本地文件路径
91
+ # @param options [Hash] 高级参数
92
+ # @option options [String] :biz_attr 目录属性, 业务端维护
93
+ # @option options [String] :bucket bucket名称
94
+ # @option options [Boolean] :disable_cpt 是否禁用checkpoint功能,如
95
+ # 果设置为true,则在上传的过程中不会写checkpoint文件,这意味着
96
+ # 上传失败后不能断点续传,而只能重新上传整个文件。如果这个值为
97
+ # true,则:cpt_file会被忽略。
98
+ # @option options [Integer] :threads 多线程上传线程数, 默认为10
99
+ # @option options [Integer] :slice_size 设置分片上传时每个分片的大小
100
+ # 默认为3 MB, 目前服务端最大限制也为3MB。
101
+ # @option options [String] :cpt_file 断点续传的checkpoint文件,如果
102
+ # 指定的cpt文件不存在,则会在file所在目录创建一个默认的cpt文件,
103
+ # 命名方式为:file.cpt,其中file是用户要下载的文件名。在下载的过
104
+ # 程中会不断更新此文件,成功完成下载后会删除此文件;如果指定的
105
+ # cpt文件已存在,则从cpt文件中记录的点继续下载。
106
+ #
107
+ # @yield [Float] 上传进度百分比回调, 进度值是一个0-1之间的小数
108
+ #
109
+ # @return Hash
110
+ # * :access_url [String] 生成的文件下载url
111
+ # * :url [String] 操作文件的url
112
+ # * :resource_path [String] 资源路径
113
+ #
114
+ # @raise [ServerError] 服务端异常返回
115
+ def upload_slice(path, file_name, file_src, options = {}, &block)
116
+ slice = Slice.new(
117
+ config: config,
118
+ http: http,
119
+ path: path,
120
+ file_name: file_name,
121
+ file_src: file_src,
122
+ options: options,
123
+ progress: block
124
+ ).upload
125
+
126
+ {
127
+ access_url: slice[:access_url],
128
+ url: slice[:url],
129
+ resource_path: slice[:resource_path]
130
+ }
131
+ end
132
+
133
+ # 目录列表/前缀搜索
134
+ #
135
+ # @see {http://www.qcloud.com/wiki/%E7%9B%AE%E5%BD%95%E5%88%97%E8%A1%A8,%E5%89%8D%E7%BC%80%E6%90%9C%E7%B4%A2:}
136
+ #
137
+ # @param path [String] 目录路径, 如: '/', 'path1', 'path1/path2', sdk会补齐末尾的 '/'
138
+ # @param options [Hash]
139
+ # @option options [String] :bucket bucket名称
140
+ # @option options [String] :prefix 搜索前缀
141
+ # 如果填写prefix, 则列出含此前缀的所有文件及目录
142
+ # @option options [Integer] :num 每页拉取的数量, 默认20条
143
+ # @option options [Symbol] :pattern 获取方式
144
+ # :dir_only 只获取目录, :file_only 只获取文件, 默认为 :both 全部获取
145
+ # @option options [Symbol] :order 排序方式
146
+ # :asc 正序, :desc 倒序 默认为 :asc
147
+ # @option options [String] :context 页码
148
+ # 若需要翻页,需要将前一页返回值中的context透传到参数中
149
+ # 若order为:asc,则从当前页正序/往下翻页;若order为:desc,则从当前页倒序/往上翻
150
+ #
151
+ # @return Hash
152
+ # * :context [String] 透传字段,用于翻页,需要往前/往后翻页则透传回来
153
+ # * :has_more [Boolean] 是否有内容可以继续往前/往后翻页
154
+ # * :dircount [Integer] 子目录数量(总)
155
+ # * :filecount [Integer] 子文件数量(总)
156
+ # * :infos [Array<Hash>] 列表结果(可能为空)
157
+ # * * :name [String] 目录名/文件名
158
+ # * * :biz_attr [String] 目录/文件属性,业务端维护
159
+ # * * :filesize [Integer] 文件大小(当类型为文件时返回)
160
+ # * * :filelen [Integer] 文件已传输大小(通过与filesize对比可知文件传输进度,当类型为文件时返回)
161
+ # * * :sha [String] 文件sha1(当类型为文件时返回)
162
+ # * * :ctime [String] 创建时间(Unix时间戳)
163
+ # * * :mtime [String] 修改时间(Unix时间戳)
164
+ # * * :access_url [String] 生成的资源可访问的url(当类型为文件时返回)
165
+ #
166
+ # @raise [ServerError] 服务端异常返回
167
+ def list(path, options = {})
168
+ bucket = config.get_bucket(options[:bucket])
169
+ sign = http.signature.multiple(bucket)
170
+ resource_path = Util.get_resource_path(config.app_id, bucket, path, options[:prefix])
171
+
172
+ pattern = case options[:pattern].to_s.to_sym
173
+ when :dir_only
174
+ 'eListDirOnly'
175
+ when :file_only
176
+ 'eListFileOnly'
177
+ else
178
+ 'eListBoth'
179
+ end
180
+
181
+ query = {
182
+ op: 'list',
183
+ num: options[:num] || 20,
184
+ pattern: pattern,
185
+ order: options[:order].to_s.to_sym == :desc ? 1 : 0,
186
+ context: options[:context]
187
+ }
188
+
189
+ http.get(resource_path, {params: query}, sign)
190
+ end
191
+
192
+ # 更新目录/文件信息(biz_attr)
193
+ #
194
+ # @see {http://www.qcloud.com/wiki/%E7%9B%AE%E5%BD%95/%E6%96%87%E4%BB%B6%E4%BF%A1%E6%81%AF_update}
195
+ #
196
+ # @param path [String] 资源路径,
197
+ # 如: 目录'path1/', 文件'path1/file'
198
+ # @param biz_attr [String] 目录/文件属性,业务端维护
199
+ # @param options [Hash]
200
+ # @option options [String] :bucket bucket名称
201
+ #
202
+ # @raise [ServerError] 服务端异常返回
203
+ def update(path, biz_attr, options = {})
204
+ bucket = config.get_bucket(options[:bucket])
205
+ resource_path = Util.get_resource_path_or_file(config.app_id, bucket, path)
206
+ sign = http.signature.once(bucket, path)
207
+ payload = {op: 'update', biz_attr: biz_attr}
208
+
209
+ http.post(resource_path, {}, sign, payload.to_json)
210
+ end
211
+
212
+ # 目录/文件信息查询
213
+ #
214
+ # @see {http://www.qcloud.com/wiki/%E7%9B%AE%E5%BD%95/%E6%96%87%E4%BB%B6%E4%BF%A1%E6%81%AF_%E6%9F%A5%E8%AF%A2}
215
+ #
216
+ # @param path [String] 资源路径, 如: 目录'path1/', 文件'path1/file'
217
+ # @param options [Hash]
218
+ # @option options [String] :bucket bucket名称
219
+ #
220
+ # @return Hash
221
+ # * :name [String] 目录名/文件名
222
+ # * :biz_attr [String] 目录/文件属性,业务端维护
223
+ # * :filesize [Integer] 文件大小(当类型为文件时返回)
224
+ # * :filelen [Integer] 文件已传输大小(通过与filesize对比可知文件传输进度,当类型为文件时返回)
225
+ # * :sha [String] 文件sha1(当类型为文件时返回)
226
+ # * :ctime [String] 创建时间(Unix时间戳)
227
+ # * :mtime [String] 修改时间(Unix时间戳)
228
+ # * :access_url [String] 生成的资源可访问的url(当类型为文件时返回)
229
+ #
230
+ # @raise [ServerError] 服务端异常返回
231
+ def stat(path, options = {})
232
+ bucket = config.get_bucket(options[:bucket])
233
+ sign = http.signature.multiple(bucket)
234
+ resource_path = Util.get_resource_path_or_file(config.app_id, bucket, path)
235
+
236
+ http.get(resource_path, {params: {op: 'stat'}}, sign)
237
+ end
238
+
239
+ # 删除文件及目录
240
+ #
241
+ # @see {http://www.qcloud.com/wiki/%E5%88%A0%E9%99%A4%E6%96%87%E4%BB%B6%E5%8F%8A%E7%9B%AE%E5%BD%95}
242
+ #
243
+ # @param path [String] 资源路径, 如: 目录'path1/', 文件'path1/file'
244
+ # @param options [Hash]
245
+ # @option options [String] :bucket bucket名称
246
+ #
247
+ # @raise [ServerError] 服务端异常返回
248
+ def delete(path, options = {})
249
+ bucket = config.get_bucket(options[:bucket])
250
+ resource_path = Util.get_resource_path_or_file(config.app_id, bucket, path)
251
+ sign = http.signature.once(bucket, path)
252
+ payload = {op: 'delete'}
253
+
254
+ http.post(resource_path, {}, sign, payload.to_json)
255
+ end
256
+
257
+ # 下载文件
258
+ #
259
+ # @note SDK会自动对私有读的Bucket进行签名
260
+ #
261
+ # @param access_url [String] 资源的下载URL地址可以从list,stat接口中获取
262
+ # @param file_store [String] 本地文件存储路径
263
+ # @param options [Hash]
264
+ # @option options [String] :bucket bucket名称
265
+ # @option options [Hash] :headers 设置下载请求头,如:range
266
+ #
267
+ # @raise [DownloadError] 下载失败,服务器返回状态异常
268
+ def download(access_url, file_store, options = {})
269
+ bucket = config.get_bucket(options[:bucket])
270
+ sign = http.signature.multiple(bucket)
271
+
272
+ response = RestClient::Request.execute(
273
+ :method => 'GET',
274
+ :url => "#{access_url}?sign=#{sign}",
275
+ :headers => options[:headers]
276
+ )
277
+
278
+ if response.code < 300
279
+ File.open(file_store, 'wb') do |w|
280
+ w.write(response.body)
281
+ end
282
+ else
283
+ raise DownloadError, "server response http code: #{response.code}"
284
+ end
285
+ end
286
+
287
+ end
288
+
289
+ end
@@ -0,0 +1,731 @@
1
+ # coding: utf-8
2
+
3
+ require 'uri'
4
+
5
+ module COS
6
+
7
+ class Bucket
8
+
9
+ include Logging
10
+
11
+ attr_reader :client, :bucket_name, :authority, :bucket_type,
12
+ :migrate_source_domain, :need_preview, :refers,
13
+ :blackrefers, :brower_exec, :cnames, :nugc_flag
14
+
15
+ # 最小完整上传大小
16
+ MIN_UPLOAD_SLICE_SIZE = 10 * 1024 * 1024
17
+
18
+ # 最小下载分块大小
19
+ MIN_DOWNLOAD_SLICE_SIZE = 5 * 1024 * 1024
20
+
21
+ # 默认上传重试次数
22
+ DEFAULT_UPLOAD_RETRY = 10
23
+
24
+ # 默认下载重试次数
25
+ DEFAULT_DOWNLOAD_RETRY = 10
26
+
27
+ # 初始化
28
+ #
29
+ # @note SDK会自动获取bucket的信息,包括读取权限等并进行缓存
30
+ # 如需在后台修改了bucket信息请重新初始化Client
31
+ #
32
+ # @param client [COS::Client]
33
+ # @param bucket_name [String] bucket名称
34
+ # 如果在初始化时的配置中设置了default_bucket则该字段可以为空,会获取默认的bucket
35
+ #
36
+ # @return [COS::Bucket]DEFAULT_UPLOAD_RETRY
37
+ #
38
+ # @raise [ClientError] 未指定bucket
39
+ # @raise [ServerError] bucket不存在
40
+ def initialize(client, bucket_name = nil)
41
+ @client = client
42
+ @bucket_name = client.config.get_bucket(bucket_name)
43
+
44
+ # 使用stat API 获取根目录信息可获取到bucket信息
45
+ data = client.api.stat('/', bucket: bucket_name)
46
+ @authority = data[:authority]
47
+ @bucket_type = data[:bucket_type]
48
+ @need_preview = data[:need_preview]
49
+ @refers = data[:refers]
50
+ @migrate_source_domain = data[:migrate_source_domain]
51
+ @blackrefers = data[:blackrefers]
52
+ @brower_exec = data[:brower_exec]
53
+ @cnames = data[:cnames]
54
+ @nugc_flag = data[:nugc_flag]
55
+ end
56
+
57
+ # 创建目录
58
+ #
59
+ # @see API#create_folder
60
+ #
61
+ # @param path [String] 目录路径, 如: 'path1', 'path1/path2', sdk会补齐末尾的 '/'
62
+ # @param options [Hash] 高级参数
63
+ # @option options [String] :biz_attr 目录属性, 业务端维护
64
+ #
65
+ # @return [COS::COSDir]
66
+ #
67
+ # @raise [ServerError] 服务端异常返回
68
+ def create_folder(path, options = {})
69
+ data = client.api.create_folder(path, options.merge({bucket: bucket_name}))
70
+ dir = {
71
+ mtime: data[:mtime],
72
+ ctime: data[:ctime],
73
+ name: data[:name],
74
+ biz_attr: options[:biz_attr],
75
+ bucket: self,
76
+ path: path
77
+ }
78
+
79
+ COSDir.new(dir)
80
+ end
81
+
82
+ alias :mkdir :create_folder
83
+
84
+ # 获取list中的文件及目录个数
85
+ #
86
+ # @param path [String] 目录路径, 如: 'path1', 'path1/path2', sdk会补齐末尾的 '/'
87
+ #
88
+ # @param options [Hash]
89
+ # @option options [String] :prefix 搜索前缀
90
+ # 如果填写prefix, 则计算含此前缀的所有文件及目录个数
91
+ #
92
+ # @return [Hash]
93
+ # * :total [Integer] 文件和目录总数
94
+ # * :files [Integer] 文件数
95
+ # * :dirs [Integer] 目录数
96
+ #
97
+ # @raise [ServerError] 服务端异常返回
98
+ def list_count(path = '', options = {})
99
+ options = {}
100
+ result = client.api.list(path, options.merge({num: 1, bucket: bucket_name}))
101
+ total = result[:filecount] + result[:dircount]
102
+
103
+ {total: total, files: result[:filecount], dirs: result[:dircount]}
104
+ end
105
+
106
+ # 获取文件及目录总数
107
+ #
108
+ # @param path [String] 目录路径, 如: 'path1', 'path1/path2', sdk会补齐末尾的 '/'
109
+ #
110
+ # @return [Integer] 文件及目录总数
111
+ #
112
+ # @raise [ServerError] 服务端异常返回
113
+ def count(path = '')
114
+ lc = list_count(path)
115
+ lc[:total]
116
+ end
117
+
118
+ alias :size :count
119
+
120
+ # 获取文件数
121
+ #
122
+ # @param path [String] 目录路径, 如: 'path1', 'path1/path2', sdk会补齐末尾的 '/'
123
+ #
124
+ # @return [Integer] 文件数
125
+ #
126
+ # @raise [ServerError] 服务端异常返回
127
+ def count_files(path = '')
128
+ lc = list_count(path)
129
+ lc[:files]
130
+ end
131
+
132
+ # 获取目录数
133
+ #
134
+ # @param path [String] 目录路径, 如: 'path1', 'path1/path2', sdk会补齐末尾的 '/'
135
+ #
136
+ # @return [Integer] 目录数
137
+ #
138
+ # @raise [ServerError] 服务端异常返回
139
+ def count_dirs(path = '')
140
+ lc = list_count(path)
141
+ lc[:dirs]
142
+ end
143
+
144
+ # 列出目录
145
+ #
146
+ # @param path [String] 目录路径, 如: '/', 'path1', 'path1/path2', sdk会补齐末尾的 '/'
147
+ # @param options [Hash]
148
+ # @option options [String] :prefix 搜索前缀
149
+ # 如果填写prefix, 则列出含此前缀的所有文件及目录
150
+ # @option options [Integer] :num 每页拉取的数量, 默认20条
151
+ # @option options [Symbol] :pattern 获取方式
152
+ # :dir_only 只获取目录, :file_only 只获取文件, 默认为 :both 全部获取
153
+ # @option options [Symbol] :order 排序方式 :asc 正序, :desc 倒序 默认为 :asc
154
+ #
155
+ # @raise [ServerError] 服务端异常返回
156
+ #
157
+ # @return [Enumerator<Object>] 迭代器, 其中Object可能是COS::COSFile或COS::COSDir
158
+ #
159
+ # @example
160
+ # all = bucket.list
161
+ # all.each do |o|
162
+ # if o.is_a?(COS::COSFile)
163
+ # puts "File: #{o.name} #{o.format_size}"
164
+ # else
165
+ # puts "Dir: #{o.name} #{o.created_at}"
166
+ # end
167
+ # end
168
+ def list(path = '', options = {})
169
+ Resource.new(self, path, options).to_enum
170
+ end
171
+
172
+ alias :ls :list
173
+
174
+ # 上传文件, 大文件自动断点续传, 多线程上传
175
+ #
176
+ # @param path_or_dir [String|COS::COSDir] 目录路径或目录对象COSDir
177
+ # 目录路径如: '/', 'path1', 'path1/path2', sdk会补齐末尾的 '/'
178
+ # @param file_name [String] 文件名
179
+ # @param file_src [String] 本地文件路径
180
+ # @param options [Hash] 高级参数
181
+ # @option options [Boolean] :auto_create_folder 自动创建远端目录
182
+ # @option options [Integer] :min_slice_size 完整上传最小文件大小,
183
+ # 超过此大小将会使用分片多线程断点续传
184
+ # @option options [Integer] :upload_retry 上传重试次数, 默认10
185
+ # @option options [String] :biz_attr 目录属性, 业务端维护
186
+ # @option options [Boolean] :disable_cpt 是否禁用checkpoint功能,如
187
+ # 果设置为true,则在上传的过程中不会写checkpoint文件,这意味着
188
+ # 上传失败后不能断点续传,而只能重新上传整个文件。如果这个值为
189
+ # true,则:cpt_file会被忽略。
190
+ # @option options [Integer] :threads 多线程上传线程数, 默认为10
191
+ # @option options [Integer] :slice_size 设置分片上传时每个分片的大小
192
+ # 默认为3 MB, 目前服务端最大限制也为3MB。
193
+ # @option options [String] :cpt_file 断点续传的checkpoint文件,如果
194
+ # 指定的cpt文件不存在,则会在file所在目录创建一个默认的cpt文件,
195
+ # 命名方式为:file.cpt,其中file是用户要上传的文件名。在上传的过
196
+ # 程中会不断更新此文件,成功完成上传后会删除此文件;如果指定的
197
+ # cpt文件已存在,则从cpt文件中记录的点继续上传。
198
+ #
199
+ # @yield [Float] 上传进度百分比回调, 进度值是一个0-1之间的小数
200
+ #
201
+ # @raise [ServerError] 服务端异常返回
202
+ #
203
+ # @return [COS::COSFile]
204
+ #
205
+ # @example
206
+ # file = bucket.upload('path', 'file1', '~/test/file1') do |p|
207
+ # puts "上传进度: #{(p*100).round(2)}%")
208
+ # end
209
+ # puts file.url
210
+ def upload(path_or_dir, file_name, file_src, options = {}, &block)
211
+ dir = get_dir(path_or_dir, options[:auto_create_folder])
212
+
213
+ min_size = options[:min_slice_size] || MIN_UPLOAD_SLICE_SIZE
214
+ retry_times = options[:upload_retry] || DEFAULT_UPLOAD_RETRY
215
+
216
+ options.merge!({bucket: bucket_name})
217
+ file_src = File.expand_path(file_src)
218
+ file_size = File.size(file_src)
219
+
220
+ retry_loop(retry_times) do
221
+ if file_size > min_size
222
+ # 分块上传
223
+ client.api.upload_slice(dir.path, file_name, file_src, options, &block)
224
+ else
225
+ # 完整上传
226
+ client.api.upload(dir.path, file_name, file_src, options)
227
+ end
228
+ end
229
+
230
+ # 获取上传完成文件的状态, 只会返回<COSFile>
231
+ stat(Util.get_list_path(dir.path, file_name, true))
232
+ end
233
+
234
+ # 批量上传目录下的全部文件(不包含子目录)
235
+ #
236
+ # @note 已存在的文件不会再次上传, 本地目录中的隐藏文件(已"."开头的)不会上传
237
+ # ".cpt"文件不会上传, 不会上传子目录
238
+ #
239
+ # @param path_or_dir [String|COS::COSDir] 目录路径或目录对象COSDir
240
+ # 目录路径如: '/', 'path1', 'path1/path2', sdk会补齐末尾的 '/'
241
+ # @param file_src_path [String] 本地文件夹路径
242
+ # @param options [Hash] 高级参数
243
+ # @option options [Boolean] :auto_create_folder 自动创建远端目录
244
+ # @option options [Integer] :min_slice_size 完整上传最小文件大小,
245
+ # 超过此大小将会使用分片多线程断点续传
246
+ # @option options [Integer] :upload_retry 上传重试次数, 默认10
247
+ # @option options [String] :biz_attr 目录属性, 业务端维护
248
+ # @option options [Boolean] :disable_cpt 是否禁用checkpoint功能,如
249
+ # 果设置为true,则在上传的过程中不会写checkpoint文件,这意味着
250
+ # 上传失败后不能断点续传,而只能重新上传整个文件。如果这个值为
251
+ # true,则:cpt_file会被忽略。
252
+ # @option options [Integer] :threads 多线程上传线程数, 默认为10
253
+ # @option options [Integer] :slice_size 设置分片上传时每个分片的大小
254
+ # 默认为3 MB, 目前服务端最大限制也为3MB。
255
+ # @option options [String] :cpt_file 断点续传的checkpoint文件,如果
256
+ # 指定的cpt文件不存在,则会在file所在目录创建一个默认的cpt文件,
257
+ # 命名方式为:file.cpt,其中file是用户要上传的文件名。在上传的过
258
+ # 程中会不断更新此文件,成功完成上传后会删除此文件;如果指定的
259
+ # cpt文件已存在,则从cpt文件中记录的点继续上传。
260
+ #
261
+ # @yield [Float] 上传进度百分比回调, 进度值是一个0-1之间的小数
262
+ #
263
+ # @raise [ServerError] 服务端异常返回
264
+ #
265
+ # @return [Array<COS::COSFile>]
266
+ #
267
+ # @example
268
+ # files = bucket.upload_all('path', '~/test') do |p|
269
+ # puts "上传进度: #{(p*100).round(2)}%")
270
+ # end
271
+ # files.each do |file|
272
+ # puts file.url
273
+ # end
274
+ def upload_all(path_or_dir, file_src_path, options = {}, &block)
275
+ local_path = Util.get_local_path(file_src_path, false)
276
+ uploaded = []
277
+
278
+ Dir.foreach(local_path) do |file|
279
+
280
+ if !file.start_with?('.') and !file.end_with?('.cpt') and !File.directory?(file)
281
+ logger.info("Begin to upload file >> #{file}")
282
+
283
+ begin
284
+ # 逐个上传
285
+ uploaded << upload(path_or_dir, file, "#{local_path}/#{file}", options, &block)
286
+ rescue => error
287
+ # 跳过错误
288
+ if options[:skip_error]
289
+ logger.info("#{file} error skipped")
290
+ next
291
+ else
292
+ # 终止上传抛出异常
293
+ raise error
294
+ end
295
+ end
296
+
297
+ logger.info("#{file} upload finished")
298
+ end
299
+
300
+ end
301
+
302
+ uploaded
303
+ end
304
+
305
+ # 获取文件或目录信息
306
+ #
307
+ # @note 如查询根目录('/', '')可以获取到bucket信息, 返回COSDir
308
+ #
309
+ # @param path [String] 资源路径, 如: 目录'path1/', 文件'path1/file'
310
+ #
311
+ # @raise [ServerError] 服务端异常返回
312
+ #
313
+ # @return [COSFile|COSDir] 如果是目录则返回COSDir资源对象,是文件则返回COSFile资源对象
314
+ #
315
+ # @example
316
+ # puts bucket.stat('path1/file').name
317
+ def stat(path = '')
318
+ data = client.api.stat(path, bucket: bucket_name)
319
+
320
+ # 查询'/'获取的是bucket信息, 无name参数, 需要补全
321
+ data[:name] = '' if data[:name].nil?
322
+
323
+ if data[:filesize].nil?
324
+ # 目录
325
+ COSDir.new(data.merge({bucket: self, path: path}))
326
+ else
327
+ # 文件
328
+ COSFile.new(data.merge({bucket: self, path: path}))
329
+ end
330
+ end
331
+
332
+ # 更新文件及目录业务属性
333
+ #
334
+ # @param path [String] 资源路径, 如: 目录'path1/', 文件'path1/file'
335
+ # @param biz_attr [String] 目录/文件属性,业务端维护
336
+ #
337
+ # @raise [ServerError] 服务端异常返回
338
+ #
339
+ # @example
340
+ # bucket.update('path1/file', 'i am the attr')
341
+ def update(path, biz_attr)
342
+ client.api.update(path, biz_attr, bucket: bucket_name)
343
+ end
344
+
345
+ # 删除文件或目录
346
+ #
347
+ # @note 非空目录及根目录不可删除,会抛出异常
348
+ #
349
+ # @param path [String] 资源路径, 如: 目录'path1/', 文件'path1/file'
350
+ #
351
+ # @raise [ServerError] 服务端异常返回
352
+ #
353
+ # @example
354
+ # bucket.delete('path1/file')
355
+ def delete(path)
356
+ client.api.delete(path, bucket: bucket_name)
357
+ end
358
+
359
+ # 删除文件或目录, 不会抛出异常而是返回布尔值
360
+ #
361
+ # @note 非空目录及根目录不可删除, 返回false
362
+ #
363
+ # @param path [String] 资源路径, 如: 目录'path1/', 文件'path1/file'
364
+ #
365
+ # @example
366
+ # puts bucket.delete!('path1/file')
367
+ def delete!(path)
368
+ delete(path)
369
+ true
370
+ rescue
371
+ false
372
+ end
373
+
374
+ # 目录是否是空的
375
+ #
376
+ # @param path [String] 资源路径, 如: 目录'path1/'
377
+ #
378
+ # @raise [ServerError] 服务端异常返回
379
+ #
380
+ # @return [Boolean] 是否为空
381
+ #
382
+ # @example
383
+ # puts bucket.empty?('path1/')
384
+ def empty?(path = '')
385
+ count(path) == 0
386
+ end
387
+
388
+ # 文件或目录是否存在
389
+ #
390
+ # @param path [String] 资源路径, 如: 目录'path1/', 文件'path1/file'
391
+ #
392
+ # @raise [ServerError] 服务端异常返回
393
+ #
394
+ # @return [Boolean] 是否存在
395
+ #
396
+ # @example
397
+ # puts bucket.exist?('path1/file1')
398
+ def exist?(path)
399
+ begin
400
+ stat(path)
401
+ rescue ServerError => e
402
+ return false if e.error_code == -166
403
+ raise e
404
+ end
405
+
406
+ true
407
+ end
408
+
409
+ alias :exists? :exist?
410
+
411
+ # 判断文件是否上传完成
412
+ #
413
+ # @param path [String] 文件资源路径, 如: 'path1/file'
414
+ #
415
+ # @raise [ServerError] 服务端异常返回
416
+ #
417
+ # @return [Boolean] 是否完成
418
+ #
419
+ # @example
420
+ # puts bucket.complete?('path1/file1')
421
+ def complete?(path)
422
+ get_file(path).complete?
423
+ end
424
+
425
+ # 获取文件可访问的URL
426
+ #
427
+ # @note 私有读取的bucket会自动生成带签名的URL
428
+ #
429
+ # @param path_or_file [String|COS::COSFile] 文件资源COSFile或路径, 如: 'path1/file'
430
+ # @param options [Hash] 高级参数
431
+ # @option options [String] :cname 在cos控制台设置的cname域名
432
+ # @option options [Boolean] :https 是否生成https的URL
433
+ # @option options [Integer] :expire_seconds 签名有效时间(秒,私有读取bucket时需要)
434
+ #
435
+ # @raise [ServerError] 服务端异常返回
436
+ #
437
+ # @return [String] 文件访问URL
438
+ #
439
+ # @example
440
+ # puts bucket.url('path1/file1', https: true, cname: 'static.domain.com')
441
+ def url(path_or_file, options = {})
442
+
443
+ file = get_file(path_or_file)
444
+
445
+ url = file.access_url
446
+
447
+ # 使用cname
448
+ if options[:cname]
449
+ host = URI.parse(url).host.downcase
450
+ url.gsub!(host, options[:cname])
451
+ end
452
+
453
+ # 使用https
454
+ if options[:https]
455
+ url.gsub!('http://', 'https://')
456
+ end
457
+
458
+ if authority == 'eWRPrivate'
459
+ # 私有读取的bucket自动生成带签名的URL
460
+ sign = client.signature.multiple(
461
+ bucket_name,
462
+ options[:expire_seconds] || client.config.multiple_sign_expire)
463
+
464
+ "#{url}?sign=#{sign}"
465
+ else
466
+ url
467
+ end
468
+ end
469
+
470
+ # 下载文件, 支持断点续传, 支持多线程
471
+ #
472
+ # @param path_or_file [String|COS::COSFile] 文件路径或文件对象COSFile
473
+ # @param file_store [String] 本地文件存储路径
474
+ # @param options [Hash] 高级参数
475
+ # @option options [Integer] :min_slice_size 完整下载最小文件大小,
476
+ # 超过此大小将会使用分片多线程断点续传
477
+ # @option options [Integer] :download_retry 下载重试次数, 默认10
478
+ # @option options [Boolean] :disable_cpt 是否禁用checkpoint功能,如
479
+ # 果设置为true,则在下载的过程中不会写checkpoint文件,这意味着
480
+ # 下载失败后不能断点续传,而只能重新下载整个文件。如果这个值为
481
+ # true,则:cpt_file会被忽略。
482
+ # @option options [Integer] :threads 多线程下载线程数, 默认为10
483
+ # @option options [Integer] :slice_size 设置分片下载时每个分片的大小
484
+ # 默认为5 MB。
485
+ # @option options [String] :cpt_file 断点续传的checkpoint文件,如果
486
+ # 指定的cpt文件不存在,则会在file所在目录创建一个默认的cpt文件,
487
+ # 命名方式为:file.cpt,其中file是用户要下载的文件名。在下载的过
488
+ # 程中会不断更新此文件,成功完成下载后会删除此文件;如果指定的
489
+ # cpt文件已存在,则从cpt文件中记录的点继续下载。
490
+ #
491
+ # @yield [Float] 下载进度百分比回调, 进度值是一个0-1之间的小数
492
+ #
493
+ # @raise [ServerError] 服务端异常返回
494
+ #
495
+ # @return [String]
496
+ #
497
+ # @example
498
+ # file = bucket.download('path/file1', '~/test/file1') do |p|
499
+ # puts "下载进度: #{(p*100).round(2)}%")
500
+ # end
501
+ # puts file
502
+ def download(path_or_file, file_store, options = {}, &block)
503
+ min_size = options[:min_slice_size] || MIN_DOWNLOAD_SLICE_SIZE
504
+ retry_times = options[:download_retry] || DEFAULT_DOWNLOAD_RETRY
505
+
506
+ # 如果传入的是一个路径需要先获取文件信息
507
+ file = get_file(path_or_file)
508
+
509
+ # 检查文件是否上传完整才能下载
510
+ unless file.complete?
511
+ raise FileUploadNotComplete, 'file upload not complete'
512
+ end
513
+
514
+ # 检查本地文件sha1是否一致, 如一致就已下载完成了
515
+ if file.sha1_match?(file_store)
516
+ logger.info("File #{file_store} exist and sha1 match, skip download.")
517
+ return file_store
518
+ end
519
+
520
+ retry_loop(retry_times) do
521
+ if file.filesize > min_size
522
+ # 分块下载
523
+ Download.new(
524
+ bucket: self,
525
+ cos_file: file,
526
+ file_store: file_store,
527
+ options: options,
528
+ progress: block
529
+ ).download
530
+
531
+ else
532
+ # 直接下载
533
+ client.api.download(file.access_url, file_store, bucket: bucket_name)
534
+
535
+ end
536
+ end
537
+
538
+ # 返回本地文件路径
539
+ file_store
540
+ end
541
+
542
+ # 批量下载目录下的全部文件(不包含子目录)
543
+ #
544
+ # @note sdk会自动创建本地目录
545
+ #
546
+ # @param path_or_dir [String|COS::COSDir] 目录路径或目录对象COSDir
547
+ # 目录路径如: '/', 'path1', 'path1/path2', sdk会补齐末尾的 '/'
548
+ # @param file_store_path [String] 本地文件存储目录
549
+ # @param options [Hash] 高级参数
550
+ # @option options [Integer] :min_slice_size 完整下载最小文件大小,
551
+ # 超过此大小将会使用分片多线程断点续传
552
+ # @option options [Integer] :disable_mkdir 禁止自动创建本地文件夹, 默认会创建
553
+ # @option options [Integer] :download_retry 下载重试次数, 默认10
554
+ # @option options [Boolean] :disable_cpt 是否禁用checkpoint功能,如
555
+ # 果设置为true,则在下载的过程中不会写checkpoint文件,这意味着
556
+ # 下载失败后不能断点续传,而只能重新下载整个文件。如果这个值为
557
+ # true,则:cpt_file会被忽略。
558
+ # @option options [Integer] :threads 多线程下载线程数, 默认为10
559
+ # @option options [Integer] :slice_size 设置分片下载时每个分片的大小
560
+ # 默认为5 MB。
561
+ # @option options [String] :cpt_file 断点续传的checkpoint文件,如果
562
+ # 指定的cpt文件不存在,则会在file所在目录创建一个默认的cpt文件,
563
+ # 命名方式为:file.cpt,其中file是用户要下载的文件名。在下载的过
564
+ # 程中会不断更新此文件,成功完成下载后会删除此文件;如果指定的
565
+ # cpt文件已存在,则从cpt文件中记录的点继续下载。
566
+ #
567
+ # @yield [Float] 下载进度百分比回调, 进度值是一个0-1之间的小数
568
+ #
569
+ # @raise [ServerError] 服务端异常返回
570
+ #
571
+ # @return [Array<String>] 本地文件路径数组
572
+ #
573
+ # @example
574
+ # files = bucket.download_all('path/', '~/test/') do |p|
575
+ # puts "下载进度: #{(p*100).round(2)}%")
576
+ # end
577
+ # puts files
578
+ def download_all(path_or_dir, file_store_path, options = {}, &block)
579
+ local_path = Util.get_local_path(file_store_path, options[:disable_mkdir])
580
+ dir = get_dir(path_or_dir)
581
+ downloaded = []
582
+
583
+ # 遍历目录下的所有文件
584
+ dir.list(pattern: :file_only).each do |file|
585
+ logger.info("Begin to download file >> #{file.name}")
586
+
587
+ downloaded << file.download("#{local_path}/#{file.name}", options, &block)
588
+
589
+ logger.info("#{file.name} download finished")
590
+ end
591
+
592
+ downloaded
593
+ end
594
+
595
+ # 获取目录树形结构
596
+ #
597
+ # @param path_or_dir [String|COS::COSDir] 目录路径或目录对象COSDir
598
+ # 目录路径如: '/', 'path1', 'path1/path2', sdk会补齐末尾的 '/'
599
+ # @param options [Hash]
600
+ # @option options [Integer] :depth 子目录深度,默认为5
601
+ #
602
+ # @raise [ServerError] 服务端异常返回
603
+ #
604
+ # @return [Hash]
605
+ #
606
+ # @example
607
+ # tree = bucket.tree
608
+ # puts tree[:resource].name
609
+ # tree[:children].each do |r|
610
+ # puts r[:resource].name
611
+ # end
612
+ #
613
+ # {
614
+ # :resource => resource,
615
+ # :children => [
616
+ # {:resource => resource, :children => [...]},
617
+ # {:resource => resource, :children => [...]},
618
+ # ...
619
+ # ]
620
+ # }
621
+ def tree(path_or_dir = '', options = {})
622
+ dir = get_dir(path_or_dir)
623
+ Tree.new(options.merge({path: dir})).to_object
624
+ end
625
+
626
+ # 获取Hash格式的目录树形结构, 可用于直接to_json
627
+ #
628
+ # @param path_or_dir [String|COS::COSDir] 目录路径或目录对象COSDir
629
+ # 目录路径如: '/', 'path1', 'path1/path2', sdk会补齐末尾的 '/'
630
+ # @param options [Hash]
631
+ # @option options [Integer] :depth 子目录深度,默认为5
632
+ #
633
+ # @raise [ServerError] 服务端异常返回
634
+ #
635
+ # @return [Hash<Object>]
636
+ #
637
+ # @example
638
+ # puts bucket.hash_tree.to_json
639
+ #
640
+ # {
641
+ # :resource => {name: '', mtime: ''...},
642
+ # :children => [
643
+ # {:resource => resource, :children => [...]},
644
+ # {:resource => resource, :children => [...]},
645
+ # ...
646
+ # ]
647
+ # }
648
+ def hash_tree(path_or_dir = '', options = {})
649
+ dir = get_dir(path_or_dir)
650
+ Tree.new(options.merge({path: dir})).to_hash
651
+ end
652
+
653
+ private
654
+
655
+ # 重试循环
656
+ def retry_loop(retry_times, &block)
657
+ begin
658
+ block.call
659
+ rescue => error
660
+
661
+ if retry_times > 0
662
+ retry_times -= 1
663
+ logger.info('Retrying...')
664
+ retry
665
+ else
666
+ raise error
667
+ end
668
+
669
+ end
670
+ end
671
+
672
+ # 获取文件对象, 可接受path string或COSFile
673
+ def get_file(path_or_file)
674
+ if path_or_file.is_a?(COS::COSFile)
675
+ # 传入的是COSFile
676
+ path_or_file
677
+
678
+ elsif path_or_file.is_a?(String)
679
+ # 传入的是path string
680
+ file = stat(path_or_file)
681
+ get_file(file)
682
+
683
+ else
684
+ raise ClientError,
685
+ "can't get file from #{path_or_file.class}, " \
686
+ 'must be a file path string or COS::COSFile'
687
+
688
+ end
689
+ end
690
+
691
+ # 获取目录对象, 可接受path string或COSDir
692
+ def get_dir(path_or_dir, auto_create_folder = false)
693
+ if path_or_dir.is_a?(COS::COSDir)
694
+ # 传入的是COSDir
695
+ path_or_dir
696
+
697
+ elsif path_or_dir.is_a?(String)
698
+ # 传入的是path string
699
+ path_or_dir = "#{path_or_dir}/" unless path_or_dir.end_with?('/')
700
+
701
+ dir = handle_folder(path_or_dir, auto_create_folder)
702
+ get_dir(dir)
703
+
704
+ else
705
+ raise ClientError,
706
+ "can't get dir from #{path_or_dir.class}, " \
707
+ 'must be a file path string or COS::COSDir'
708
+
709
+ end
710
+ end
711
+
712
+ # 获取目录信息并可以自动创建目录
713
+ def handle_folder(path_or_dir, auto_create_folder = false)
714
+ return stat(path_or_dir)
715
+ rescue => error
716
+ unless auto_create_folder
717
+ raise error
718
+ end
719
+
720
+ # 自动创建目录
721
+ if error.is_a?(COS::ServerError) and error.error_code == -166
722
+ logger.info('path not exist, auto create folder...')
723
+ return create_folder(path_or_dir)
724
+ end
725
+
726
+ raise error
727
+ end
728
+
729
+ end
730
+
731
+ end