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,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