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