cos 0.1.0 → 0.1.1

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