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.
- checksums.yaml +4 -4
- data/.gitignore +12 -0
- data/.rspec +2 -0
- data/.travis.yml +13 -2
- data/Gemfile +4 -1
- data/LICENSE +191 -0
- data/README.md +2014 -17
- data/Rakefile +23 -6
- data/bin/cos +325 -0
- data/bin/setup +1 -3
- data/cos.gemspec +24 -13
- data/lib/cos.rb +41 -4
- data/lib/cos/api.rb +289 -0
- data/lib/cos/bucket.rb +731 -0
- data/lib/cos/checkpoint.rb +62 -0
- data/lib/cos/client.rb +58 -0
- data/lib/cos/config.rb +102 -0
- data/lib/cos/dir.rb +301 -0
- data/lib/cos/download.rb +252 -0
- data/lib/cos/exception.rb +62 -0
- data/lib/cos/file.rb +152 -0
- data/lib/cos/http.rb +95 -0
- data/lib/cos/logging.rb +47 -0
- data/lib/cos/resource.rb +201 -0
- data/lib/cos/signature.rb +119 -0
- data/lib/cos/slice.rb +292 -0
- data/lib/cos/struct.rb +49 -0
- data/lib/cos/tree.rb +165 -0
- data/lib/cos/util.rb +82 -0
- data/lib/cos/version.rb +2 -2
- data/spec/cos/bucket_spec.rb +562 -0
- data/spec/cos/client_spec.rb +77 -0
- data/spec/cos/dir_spec.rb +195 -0
- data/spec/cos/download_spec.rb +105 -0
- data/spec/cos/http_spec.rb +70 -0
- data/spec/cos/signature_spec.rb +83 -0
- data/spec/cos/slice_spec.rb +302 -0
- data/spec/cos/struct_spec.rb +38 -0
- data/spec/cos/tree_spec.rb +322 -0
- data/spec/cos/util_spec.rb +106 -0
- data/test/download_test.rb +44 -0
- data/test/list_test.rb +43 -0
- data/test/upload_test.rb +48 -0
- metadata +132 -21
- data/.idea/.name +0 -1
- data/.idea/cos.iml +0 -49
- data/.idea/encodings.xml +0 -6
- data/.idea/misc.xml +0 -14
- data/.idea/modules.xml +0 -8
- data/.idea/workspace.xml +0 -465
- data/bin/console +0 -14
data/lib/cos/slice.rb
ADDED
@@ -0,0 +1,292 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
require 'tempfile'
|
4
|
+
|
5
|
+
module COS
|
6
|
+
|
7
|
+
# 分片大文件上传, 支持断点续传, 支持多线程
|
8
|
+
class Slice < Checkpoint
|
9
|
+
|
10
|
+
include Logging
|
11
|
+
|
12
|
+
# 默认分片大小 3M
|
13
|
+
DEFAULT_SLICE_SIZE = 3 * 1024 * 1024
|
14
|
+
|
15
|
+
required_attrs :config, :http, :path, :file_name, :file_src, :options
|
16
|
+
optional_attrs :progress
|
17
|
+
|
18
|
+
attr_accessor :cpt_file, :result, :offset, :slice_size, :session
|
19
|
+
|
20
|
+
def initialize(opts = {})
|
21
|
+
super(opts)
|
22
|
+
|
23
|
+
@cpt_file = options[:cpt_file] || "#{File.expand_path(file_src)}.cpt"
|
24
|
+
end
|
25
|
+
|
26
|
+
# 开始上传
|
27
|
+
def upload
|
28
|
+
logger.info("Begin upload, file: #{file_src}, threads: #{@num_threads}")
|
29
|
+
|
30
|
+
# 重建断点续传或重新从服务器初始化分片上传
|
31
|
+
# 有可能sha命中直接完成
|
32
|
+
data = rebuild
|
33
|
+
return data if data
|
34
|
+
|
35
|
+
# 文件分片
|
36
|
+
divide_parts if @parts.empty?
|
37
|
+
|
38
|
+
# 未完成的片段
|
39
|
+
@todo_parts = @parts.reject { |p| p[:done] }
|
40
|
+
|
41
|
+
begin
|
42
|
+
# 多线程上传
|
43
|
+
(1..@num_threads).map do
|
44
|
+
Thread.new do
|
45
|
+
loop do
|
46
|
+
# 获取下一个未上传的片段
|
47
|
+
p = sync_get_todo_part
|
48
|
+
break unless p
|
49
|
+
|
50
|
+
# 上传片段
|
51
|
+
upload_part(p)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end.map(&:join)
|
55
|
+
rescue => error
|
56
|
+
unless finish?
|
57
|
+
# 部分服务端异常需要重新初始化, 可能上传已经完成了
|
58
|
+
if error.is_a?(ServerError) and error.error_code == -288
|
59
|
+
File.delete(cpt_file) unless options[:disable_cpt]
|
60
|
+
end
|
61
|
+
raise error
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
# 返回100%的进度
|
66
|
+
progress.call(1.to_f) if progress
|
67
|
+
|
68
|
+
# 上传完成, 删除checkpoint文件
|
69
|
+
File.delete(cpt_file) unless options[:disable_cpt]
|
70
|
+
|
71
|
+
logger.info("Done upload, file: #{@file_src}")
|
72
|
+
|
73
|
+
# 返回文件信息
|
74
|
+
result
|
75
|
+
end
|
76
|
+
|
77
|
+
# 断点续传状态记录
|
78
|
+
# @example
|
79
|
+
# states = {
|
80
|
+
# :session => 'session',
|
81
|
+
# :offset => 0,
|
82
|
+
# :slice_size => 2048,
|
83
|
+
# :file => 'file',
|
84
|
+
# :file_meta => {
|
85
|
+
# :mtime => Time.now,
|
86
|
+
# :sha1 => 'file sha1',
|
87
|
+
# :size => 10000,
|
88
|
+
# },
|
89
|
+
# :parts => [
|
90
|
+
# {:number => 1, :range => [0, 100], :done => false},
|
91
|
+
# {:number => 2, :range => [100, 200], :done => true}
|
92
|
+
# ],
|
93
|
+
# :sha1 => 'checkpoint file sha1'
|
94
|
+
# }
|
95
|
+
def checkpoint
|
96
|
+
logger.debug("Make checkpoint, options[:disable_cpt]: #{options[:disable_cpt] == true}")
|
97
|
+
|
98
|
+
ensure_file_not_changed
|
99
|
+
|
100
|
+
parts = sync_get_all_parts
|
101
|
+
states = {
|
102
|
+
:session => session,
|
103
|
+
:slice_size => slice_size,
|
104
|
+
:offset => offset,
|
105
|
+
:file => file_src,
|
106
|
+
:file_meta => @file_meta,
|
107
|
+
:parts => parts
|
108
|
+
}
|
109
|
+
|
110
|
+
done = parts.count { |p| p[:done] }
|
111
|
+
|
112
|
+
# 上传进度回调
|
113
|
+
if progress
|
114
|
+
percent = (offset + done*slice_size).to_f / states[:file_meta][:size]
|
115
|
+
progress.call(percent > 1 ? 1.to_f : percent)
|
116
|
+
end
|
117
|
+
|
118
|
+
write_checkpoint(states, cpt_file) unless options[:disable_cpt]
|
119
|
+
|
120
|
+
logger.debug("Upload Parts #{done}/#{states[:parts].count}")
|
121
|
+
end
|
122
|
+
|
123
|
+
private
|
124
|
+
|
125
|
+
# 是否完成上传
|
126
|
+
def finish?
|
127
|
+
result != nil and result[:access_url] != nil
|
128
|
+
end
|
129
|
+
|
130
|
+
# 断点续传文件重建
|
131
|
+
def rebuild
|
132
|
+
logger.info("Begin rebuild session, checkpoint: #{cpt_file}")
|
133
|
+
|
134
|
+
# 是否启用断点续传并且记录文件存在
|
135
|
+
if options[:disable_cpt] || !File.exist?(cpt_file)
|
136
|
+
# 从服务器初始化
|
137
|
+
data = initiate
|
138
|
+
return data if data
|
139
|
+
else
|
140
|
+
# 加载断点续传
|
141
|
+
states = load_checkpoint(cpt_file)
|
142
|
+
|
143
|
+
# 确保上传的文件未变化
|
144
|
+
if states[:file_sha1] != @file_meta[:sha1]
|
145
|
+
raise FileInconsistentError, 'The file to upload is changed'
|
146
|
+
end
|
147
|
+
|
148
|
+
@session = states[:session]
|
149
|
+
@file_meta = states[:file_meta]
|
150
|
+
@parts = states[:parts]
|
151
|
+
@slice_size = states[:slice_size]
|
152
|
+
@offset = states[:offset]
|
153
|
+
end
|
154
|
+
|
155
|
+
logger.info("Done rebuild session, Parts: #{@parts.count}")
|
156
|
+
|
157
|
+
false
|
158
|
+
end
|
159
|
+
|
160
|
+
# 初始化分块上传
|
161
|
+
def initiate
|
162
|
+
logger.info('Begin initiate session')
|
163
|
+
|
164
|
+
bucket = config.get_bucket(options[:bucket])
|
165
|
+
sign = http.signature.multiple(bucket)
|
166
|
+
resource_path = Util.get_resource_path(config.app_id, bucket, path, file_name)
|
167
|
+
file_size = File.size(file_src)
|
168
|
+
file_sha1 = Util.file_sha1(file_src)
|
169
|
+
|
170
|
+
payload = {
|
171
|
+
op: 'upload_slice',
|
172
|
+
slice_size: options[:slice_size] || DEFAULT_SLICE_SIZE,
|
173
|
+
sha: file_sha1,
|
174
|
+
filesize: file_size,
|
175
|
+
biz_attr: options[:biz_attr],
|
176
|
+
session: session,
|
177
|
+
multipart: true
|
178
|
+
}
|
179
|
+
|
180
|
+
resp = http.post(resource_path, {}, sign, payload)
|
181
|
+
|
182
|
+
# 上一次已传完或秒传成功
|
183
|
+
return resp if resp[:access_url]
|
184
|
+
|
185
|
+
@session = resp[:session]
|
186
|
+
@slice_size = resp[:slice_size]
|
187
|
+
@offset = resp[:offset]
|
188
|
+
|
189
|
+
@file_meta = {
|
190
|
+
:mtime => File.mtime(file_src),
|
191
|
+
:sha1 => file_sha1,
|
192
|
+
:size => file_size
|
193
|
+
}
|
194
|
+
|
195
|
+
# 保存断点
|
196
|
+
checkpoint
|
197
|
+
|
198
|
+
logger.info("Done initiate session: #{@session}")
|
199
|
+
|
200
|
+
false
|
201
|
+
end
|
202
|
+
|
203
|
+
# 上传块
|
204
|
+
def upload_part(p)
|
205
|
+
logger.debug("Begin upload slice: #{p}")
|
206
|
+
|
207
|
+
bucket = config.get_bucket(options[:bucket])
|
208
|
+
sign = http.signature.multiple(bucket)
|
209
|
+
resource_path = Util.get_resource_path(config.app_id, bucket, path, file_name)
|
210
|
+
temp_file = Tempfile.new("#{session}-#{p[:number]}")
|
211
|
+
|
212
|
+
begin
|
213
|
+
# 复制文件分片至临时文件
|
214
|
+
IO.copy_stream(file_src, temp_file, p[:range].at(1) - p[:range].at(0), p[:range].at(0))
|
215
|
+
|
216
|
+
payload = {
|
217
|
+
op: 'upload_slice',
|
218
|
+
sha: Util.file_sha1(temp_file),
|
219
|
+
offset: p[:range].at(0),
|
220
|
+
session: session,
|
221
|
+
filecontent: temp_file,
|
222
|
+
multipart: true
|
223
|
+
}
|
224
|
+
|
225
|
+
re = http.post(resource_path, {}, sign, payload)
|
226
|
+
@result = re if re[:access_url]
|
227
|
+
ensure
|
228
|
+
# 确保清除临时文件
|
229
|
+
temp_file.close
|
230
|
+
temp_file.unlink
|
231
|
+
end
|
232
|
+
|
233
|
+
sync_update_part(p.merge(done: true))
|
234
|
+
|
235
|
+
checkpoint
|
236
|
+
|
237
|
+
logger.debug("Done upload part: #{p}")
|
238
|
+
end
|
239
|
+
|
240
|
+
# 文件片段拆分
|
241
|
+
def divide_parts
|
242
|
+
logger.info("Begin divide parts, file: #{file_src}")
|
243
|
+
|
244
|
+
file_size = File.size(file_src)
|
245
|
+
num_parts = (file_size - offset - 1) / slice_size + 1
|
246
|
+
@parts = (1..num_parts).map do |i|
|
247
|
+
{
|
248
|
+
:number => i,
|
249
|
+
:range => [offset + (i-1) * slice_size, [offset + i * slice_size, file_size].min],
|
250
|
+
:done => false
|
251
|
+
}
|
252
|
+
end
|
253
|
+
|
254
|
+
checkpoint
|
255
|
+
|
256
|
+
logger.info("Done divide parts, parts: #{@parts.size}")
|
257
|
+
end
|
258
|
+
|
259
|
+
# 同步获取下一片段
|
260
|
+
def sync_get_todo_part
|
261
|
+
@todo_mutex.synchronize {
|
262
|
+
@todo_parts.shift
|
263
|
+
}
|
264
|
+
end
|
265
|
+
|
266
|
+
# 同步更新片段
|
267
|
+
def sync_update_part(p)
|
268
|
+
@all_mutex.synchronize {
|
269
|
+
@parts[p[:number] - 1] = p
|
270
|
+
}
|
271
|
+
end
|
272
|
+
|
273
|
+
# 同步获取所有片段
|
274
|
+
def sync_get_all_parts
|
275
|
+
@all_mutex.synchronize {
|
276
|
+
@parts.dup
|
277
|
+
}
|
278
|
+
end
|
279
|
+
|
280
|
+
# 确保上传中文件没有变化
|
281
|
+
def ensure_file_not_changed
|
282
|
+
return if File.mtime(file_src) == @file_meta[:mtime]
|
283
|
+
|
284
|
+
if @file_meta[:sha1] != Util.file_sha1(file_src)
|
285
|
+
# p Util.file_sha1(file_src)
|
286
|
+
raise FileInconsistentError, 'The file to upload is changed'
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
end
|
291
|
+
|
292
|
+
end
|
data/lib/cos/struct.rb
ADDED
@@ -0,0 +1,49 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module COS
|
4
|
+
|
5
|
+
module Struct
|
6
|
+
class Base
|
7
|
+
module AttrHelper
|
8
|
+
|
9
|
+
# 动态创建必选参数
|
10
|
+
def required_attrs(*s)
|
11
|
+
define_method(:required_attrs) {s}
|
12
|
+
attr_reader(*s)
|
13
|
+
end
|
14
|
+
|
15
|
+
# 动态创建可选参数
|
16
|
+
def optional_attrs(*s)
|
17
|
+
define_method(:optional_attrs) {s}
|
18
|
+
attr_reader(*s)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
extend AttrHelper
|
23
|
+
|
24
|
+
def initialize(options = {})
|
25
|
+
# 意外参数检测
|
26
|
+
unless optional_attrs.include?(:SKIP_EXTRA)
|
27
|
+
extra_keys = options.keys - required_attrs - optional_attrs
|
28
|
+
unless extra_keys.empty?
|
29
|
+
raise AttrError, "Unexpected extra keys: #{extra_keys.join(', ')}"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# 必选参数检测
|
34
|
+
required_keys = required_attrs - options.keys
|
35
|
+
unless required_keys.empty?
|
36
|
+
raise AttrError, "Keys: #{required_keys.join(', ')} is Required"
|
37
|
+
end
|
38
|
+
|
39
|
+
# 动态创建实例变量
|
40
|
+
(required_attrs + optional_attrs).each do |attr|
|
41
|
+
instance_variable_set("@#{attr}", options[attr])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
end
|
49
|
+
|
data/lib/cos/tree.rb
ADDED
@@ -0,0 +1,165 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
|
3
|
+
module COS
|
4
|
+
|
5
|
+
class Tree < Struct::Base
|
6
|
+
|
7
|
+
MAX_DEPTH = 5
|
8
|
+
|
9
|
+
required_attrs :path
|
10
|
+
optional_attrs :depth, :files_count, :files
|
11
|
+
|
12
|
+
def initialize(options = {})
|
13
|
+
super(options)
|
14
|
+
@tree_str = ''
|
15
|
+
@depth = depth || MAX_DEPTH
|
16
|
+
end
|
17
|
+
|
18
|
+
# 输出Object格式, 可以直接用于链式调用
|
19
|
+
# @example
|
20
|
+
# {
|
21
|
+
# :resource => resource,
|
22
|
+
# :children => [
|
23
|
+
# {:resource => resource, :children => [...]},
|
24
|
+
# {:resource => resource, :children => [...]},
|
25
|
+
# ...
|
26
|
+
# ]
|
27
|
+
# }
|
28
|
+
def to_object
|
29
|
+
create_tree(path, [], :object)
|
30
|
+
end
|
31
|
+
|
32
|
+
# 输出Hash格式, 可以直接.to_json转化为json string
|
33
|
+
# @example
|
34
|
+
# {
|
35
|
+
# :resource => {name: '', mtime: ''...},
|
36
|
+
# :children => [
|
37
|
+
# {:resource => resource, :children => [...]},
|
38
|
+
# {:resource => resource, :children => [...]},
|
39
|
+
# ...
|
40
|
+
# ]
|
41
|
+
# }
|
42
|
+
def to_hash
|
43
|
+
create_tree(path, [], :hash)
|
44
|
+
end
|
45
|
+
|
46
|
+
# 命令行打印树结构
|
47
|
+
def print_tree
|
48
|
+
create_tree(path, [])
|
49
|
+
puts @tree_str
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
# 递归创建树
|
55
|
+
def create_tree(dir, level, type = nil)
|
56
|
+
@tree_str << row_string_for_dir(dir, level) if type.nil?
|
57
|
+
children = []
|
58
|
+
|
59
|
+
if level.count < depth and dir.is_a?(COS::COSDir)
|
60
|
+
cd = child_directories(dir)
|
61
|
+
|
62
|
+
i = 0
|
63
|
+
while i < cd.count do
|
64
|
+
level_dup = level.dup
|
65
|
+
is_last = i + 1 == cd.count
|
66
|
+
la = level_dup << is_last
|
67
|
+
|
68
|
+
ct = create_tree(cd[i], la, type)
|
69
|
+
children << ct if type != nil
|
70
|
+
i += 1
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
if type != nil
|
75
|
+
if type == :hash
|
76
|
+
resource = dir.to_hash
|
77
|
+
else
|
78
|
+
resource = dir
|
79
|
+
end
|
80
|
+
{resource: resource, children: children}
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# 获取子目录结构
|
85
|
+
def child_directories(dir)
|
86
|
+
dirs = []
|
87
|
+
|
88
|
+
pattern = @files ? :both : :dir_only
|
89
|
+
|
90
|
+
dir.list(pattern: pattern).each do |d|
|
91
|
+
if d.is_a?(COS::COSDir)
|
92
|
+
dirs << d
|
93
|
+
else
|
94
|
+
dirs << d if @files
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
dirs
|
99
|
+
end
|
100
|
+
|
101
|
+
# 打印输出目录行
|
102
|
+
def row_string_for_dir(dir, level)
|
103
|
+
if dir.name == ''
|
104
|
+
# 根目录显示Bucket
|
105
|
+
dirname = "Bucket #{dir.bucket.bucket_name}"
|
106
|
+
else
|
107
|
+
|
108
|
+
if dir.is_a?(COS::COSDir)
|
109
|
+
dirname = "#{dir.name}"
|
110
|
+
|
111
|
+
if @files_count
|
112
|
+
counts = dir.count_files
|
113
|
+
dirname << " \033[32m(#{counts})\033[0m" if counts > 0
|
114
|
+
end
|
115
|
+
|
116
|
+
else
|
117
|
+
dirname = "\033[34m#{dir.name}\033[0m"
|
118
|
+
dirname << " \033[31m(#{dir.format_size})\033[0m"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
dirname << " \033[35m[#{dir.biz_attr}]\033[0m" if dir.biz_attr != ''
|
123
|
+
|
124
|
+
row_str = ''
|
125
|
+
row_str << level_header_for_row(level)
|
126
|
+
row_str << dirname
|
127
|
+
row_str << "\n"
|
128
|
+
end
|
129
|
+
|
130
|
+
# 打印输出层级关系
|
131
|
+
def level_header_for_row(level)
|
132
|
+
header_str = "\033[33m"
|
133
|
+
lc = level.count
|
134
|
+
if lc > 0
|
135
|
+
i = 0
|
136
|
+
|
137
|
+
while i < lc
|
138
|
+
if i + 1 == lc
|
139
|
+
|
140
|
+
if level[i]
|
141
|
+
header_str << "└── "
|
142
|
+
else
|
143
|
+
header_str << "├── "
|
144
|
+
end
|
145
|
+
|
146
|
+
else
|
147
|
+
|
148
|
+
if level[i]
|
149
|
+
header_str << " "
|
150
|
+
else
|
151
|
+
header_str << "│ "
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
i += 1
|
156
|
+
end
|
157
|
+
|
158
|
+
end
|
159
|
+
header_str << "\033[0m"
|
160
|
+
header_str
|
161
|
+
end
|
162
|
+
|
163
|
+
end
|
164
|
+
|
165
|
+
end
|