everbox_client 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
data/CHANGELOG.rdoc ADDED
@@ -0,0 +1,26 @@
1
+ == 0.0.9 (2011-04-22)
2
+ * now works under ruby 1.9.
3
+ * now works under Windows.
4
+ * everbox cat use stream download (0.0.8 will print it to stdout after all data
5
+ downloaded).
6
+
7
+ == 0.0.8 (2011-04-22)
8
+
9
+ * everbox help and everbox cat works.
10
+
11
+ == 0.0.7 (2011-01-21)
12
+
13
+ * everbox prepare_put works
14
+
15
+ == 0.0.6 (2011-01-06)
16
+
17
+ * everbox ls support argument as path
18
+ * everbox config (print config or set config)
19
+
20
+ == 0.0.5 (2010-12-02)
21
+
22
+ * OAuth login: "everbox login --oauth"
23
+ * show user info and space info: "everbox info"
24
+ * only get download url (do not download it): "everbox get -u FILENAME"
25
+ * gemspec no longer depends on git
26
+
data/README.rdoc ADDED
@@ -0,0 +1,38 @@
1
+ = everbox_client - EverBox 命令行及调试工具
2
+
3
+ == 安装
4
+
5
+ $ gem install everbox_client-0.0.8.gem
6
+
7
+ == 使用方法
8
+
9
+ === 列出全部可用命令
10
+ $ everbox
11
+ Usage: everbox [options] <command>
12
+
13
+ Available commands:
14
+ cat cat file
15
+ cd change dir
16
+ config set config
17
+ get download file
18
+ help print help info
19
+ info show user info
20
+ login login
21
+ ls list files and directories
22
+ lsdir list directories
23
+ mirror download dir
24
+ mkdir make directory
25
+ prepare_put prepare put
26
+ put upload file
27
+ pwd print working dir
28
+ rm delete file or directory
29
+
30
+ === 显示单个命令的帮助
31
+ $ everbox help login
32
+ Usage:
33
+
34
+ everbox login [username [password]]
35
+ 登录 everbox, 登录完成后的 token 保存在 $HOME/.everbox_client/config
36
+
37
+ everbox login --oauth
38
+ 以 OAuth 方式登录
data/bin/everbox ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "everbox_client/cli"
4
+
5
+ EverboxClient::CLI.execute(STDOUT, STDIN, STDERR, ARGV)
@@ -0,0 +1,11 @@
1
+ require 'logger'
2
+
3
+ module EverboxClient
4
+ autoload :PathEntry, 'everbox_client/models/path_entry'
5
+ autoload :User, 'everbox_client/models/user'
6
+ autoload :Account, 'everbox_client/models/account'
7
+
8
+ def self.logger
9
+ @logger ||= Logger.new(STDERR)
10
+ end
11
+ end
@@ -0,0 +1,129 @@
1
+ require 'pp'
2
+
3
+ require 'everbox_client/runner'
4
+
5
+ module EverboxClient
6
+ class CLI
7
+ SUPPORTED_COMMANDS = {
8
+ "login" => "login",
9
+ "ls" => "list files and directories",
10
+ "lsdir" => "list directories",
11
+ "get" => "download file",
12
+ "put" => "upload file",
13
+ "prepare_put" => "prepare put",
14
+ "cd" => "change dir",
15
+ "pwd" => "print working dir",
16
+ "mkdir" => "make directory",
17
+ "rm" => "delete file or directory",
18
+ "mirror" => "download dir",
19
+ "info" => "show user info",
20
+ "config" => "set config",
21
+ "cat" => "cat file",
22
+ "help" => "print help info",
23
+ "thumbnail" => "get thumbnail url"
24
+ }
25
+
26
+ attr_reader :command
27
+ attr_reader :options
28
+ attr_reader :stdout, :stdin
29
+
30
+ def self.execute(stdout, stdin, stderr, arguments = [])
31
+ self.new.execute(stdout, stdin, stderr, arguments)
32
+ end
33
+
34
+ def initialize
35
+ @options = {}
36
+
37
+ # don't dump a backtrace on a ^C
38
+ trap(:INT) {
39
+ exit
40
+ }
41
+ end
42
+
43
+ def execute(stdout, stdin, stderr, arguments = [])
44
+ @stdout = stdout
45
+ @stdin = stdin
46
+ @stderr = stderr
47
+ extract_command_and_parse_options(arguments)
48
+
49
+ if valid_command?
50
+ begin
51
+ runner = EverboxClient::Runner.new @opts
52
+ runner.send @command, *@args
53
+ runner.dump_config
54
+ rescue => e
55
+ raise e
56
+ STDERR.write "Error: #{e.message}\n"
57
+ exit 1
58
+ end
59
+ else
60
+ usage
61
+ end
62
+ end
63
+ protected
64
+
65
+
66
+ def extract_command_and_parse_options(arguments)
67
+ parse_options(arguments)
68
+ @command, *@args = ARGV
69
+ end
70
+ def option_parser(arguments = "")
71
+ option_parser = OptionParser.new do |opts|
72
+ opts.banner = "Usage: #{File.basename($0)} [options] <command>"
73
+
74
+ ## Common Options
75
+
76
+
77
+ #opts.on("--scope SCOPE", "Specifies the scope (Google-specific).") do |v|
78
+ # options[:scope] = v
79
+ #end
80
+ end
81
+ end
82
+
83
+ def parse_options(arguments)
84
+ option_parser(arguments).order!(arguments)
85
+ end
86
+
87
+ def prepare_parameters
88
+ escaped_pairs = options[:params].collect do |pair|
89
+ if pair =~ /:/
90
+ Hash[*pair.split(":", 2)].collect do |k,v|
91
+ [CGI.escape(k.strip), CGI.escape(v.strip)] * "="
92
+ end
93
+ else
94
+ pair
95
+ end
96
+ end
97
+
98
+ querystring = escaped_pairs * "&"
99
+ cli_params = CGI.parse(querystring)
100
+
101
+ {
102
+ "oauth_consumer_key" => options[:oauth_consumer_key],
103
+ "oauth_nonce" => options[:oauth_nonce],
104
+ "oauth_timestamp" => options[:oauth_timestamp],
105
+ "oauth_token" => options[:oauth_token],
106
+ "oauth_signature_method" => options[:oauth_signature_method],
107
+ "oauth_version" => options[:oauth_version]
108
+ }.reject { |k,v| v.nil? || v == "" }.merge(cli_params)
109
+ end
110
+
111
+ def usage
112
+ stdout.puts option_parser.help
113
+ stdout.puts
114
+ stdout.puts "Available commands:"
115
+ SUPPORTED_COMMANDS.keys.sort.each do |command|
116
+ desc = SUPPORTED_COMMANDS[command]
117
+ puts " #{command.ljust(15)}#{desc}"
118
+ end
119
+ end
120
+
121
+ def valid_command?
122
+ SUPPORTED_COMMANDS.keys.include?(command)
123
+ end
124
+
125
+ def verbose?
126
+ options[:verbose]
127
+ end
128
+ end
129
+ end
@@ -0,0 +1,9 @@
1
+ module EverboxClient
2
+ class Exception < RuntimeError; end
3
+
4
+ class UnknownResponseException < Exception
5
+ def initialize(response)
6
+ super("unknown response code: #{response.code}\n#{response.body}")
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,25 @@
1
+ module EverboxClient
2
+ class Account
3
+ def initialize(data)
4
+ @data = data
5
+ end
6
+
7
+ def google?
8
+ @data["type"] == 'google'
9
+ end
10
+
11
+ def sdo?
12
+ @data["type"] == 'sdo'
13
+ end
14
+
15
+ def to_s
16
+ if google?
17
+ "Google Account: #{@data["email"]}"
18
+ elsif sdo?
19
+ "SDO Account: #{@data["name"]}"
20
+ else
21
+ "Unknown Account Type: #{@data["type"]}"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,47 @@
1
+ module EverboxClient
2
+ class PathEntry
3
+ MASK_FILE = 0x1
4
+ MASK_DIR = 0x2
5
+ MASK_DELETED = 0x8000
6
+
7
+ def initialize(data)
8
+ @data = data
9
+ end
10
+
11
+ def basename
12
+ @data["path"].split('/')[-1]
13
+ end
14
+
15
+ def file?
16
+ (@data["type"] & MASK_FILE) != 0
17
+ end
18
+
19
+ def dir?
20
+ (@data["type"] & MASK_DIR) != 0
21
+ end
22
+
23
+ def deleted?
24
+ (@data["type"] & MASK_DELETED) != 0
25
+ end
26
+
27
+ def entries
28
+ @entries ||=
29
+ begin
30
+ if @data["entries"].nil?
31
+ []
32
+ else
33
+ @data["entries"].map {|x| PathEntry.new(x)}
34
+ end
35
+ end
36
+ end
37
+
38
+ def path
39
+ @data["path"]
40
+ end
41
+
42
+ def to_line
43
+ suffix = dir? ? '/' : ''
44
+ "#{"%10d" % @data["fileSize"]}\t#{basename}#{suffix}\n"
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,23 @@
1
+ require 'everbox_client'
2
+
3
+ module EverboxClient
4
+ class User
5
+ def initialize(data)
6
+ @data = data
7
+ end
8
+
9
+ def to_s
10
+ res = ""
11
+ res << "username: #{@data["username"]}\n"
12
+ res << "email: #{@data["email"]}\n"
13
+ res << "\n"
14
+ unless @data["accounts"].empty?
15
+ res << "Accounts:\n"
16
+ @data["accounts"].each do |account|
17
+ res << " " << Account.new(account).to_s << "\n"
18
+ end
19
+ end
20
+ res
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,658 @@
1
+ # encoding: utf-8
2
+ require 'fileutils'
3
+ require 'yaml'
4
+ require 'digest/sha1'
5
+ require 'pp'
6
+
7
+ require 'oauth'
8
+ require 'json'
9
+ require 'highline/import'
10
+ require 'restclient'
11
+ require 'launchy'
12
+
13
+ require 'everbox_client'
14
+ require 'everbox_client/exceptions'
15
+
16
+
17
+ class ResponseError < RuntimeError
18
+ attr_accessor :response
19
+ def initialize(response)
20
+ @response = response
21
+ end
22
+
23
+ def to_s
24
+ "code=#{response.code}|body=#{response.body}"
25
+ end
26
+ end
27
+
28
+ class File
29
+ # expan_path in unix style
30
+ def self.expand_path_unix(*args)
31
+ res = self.expand_path(*args)
32
+ if res[0] != '/'
33
+ res = res[res.index("/"), res.size]
34
+ end
35
+ res
36
+ end
37
+ end
38
+
39
+ module EverboxClient
40
+ class Runner
41
+ DEFAULT_OPTIONS = {
42
+ :pwd => '/home',
43
+ :config_file => '~/.everbox_client/config',
44
+ :consumer_key => 'TFshQutcGMifMPCtcUFWsMtTIqBg8bAqB55XJO8P',
45
+ :consumer_secret => '9tego848novn68kboENkhW3gTy9rE2woHWpRwAwQ',
46
+ :oauth_site => 'http://account.everbox.com',
47
+ :fs_site => 'http://fs.everbox.com',
48
+ :chunk_size => 1024*1024*4
49
+
50
+ }
51
+
52
+ def initialize(opts={})
53
+ opts ||= {}
54
+ config_file = opts[:config_file] || DEFAULT_OPTIONS[:config_file]
55
+ @options = DEFAULT_OPTIONS.merge(load_config(config_file)).merge(opts)
56
+ end
57
+
58
+ def help(arg = nil)
59
+ case arg
60
+ when nil
61
+ puts "Usage: everbox help COMMAND"
62
+ when 'cat'
63
+ puts "Usage: everbox cat [PATH]..."
64
+ when 'cd'
65
+ puts <<DOC
66
+ Usage: everbox cd [newpath]
67
+ 切换工作目录, newpath 可以是相对路径或者绝对路径, 没有 newpath 会切换目录到 "/home"
68
+ 注意: cd 并不会检查服务器上该目录是否真的存在
69
+ DOC
70
+ when 'config'
71
+ puts <<DOC
72
+ Usage:
73
+ everbox config
74
+ 显示当前的配置
75
+
76
+ everbox config KEY VALUE
77
+ 设置配置
78
+ DOC
79
+ when 'get'
80
+ puts <<DOC
81
+ Usage: everbox get [-u] FILENAME
82
+ 下载文件, 注意可能会覆盖本地的同名文件, 如果指定 "-u", 则只打印下载 url
83
+ DOC
84
+ when 'info'
85
+ puts <<DOC
86
+ Usage: everbox info
87
+ 显示用户信息
88
+ DOC
89
+ when 'login'
90
+ puts <<DOC
91
+ Usage:
92
+
93
+ everbox login [username [password]]
94
+ 登录 everbox, 登录完成后的 token 保存在 $HOME/.everbox_client/config
95
+
96
+ everbox login --oauth
97
+ 以 OAuth 方式登录
98
+ DOC
99
+ when 'ls'
100
+ puts <<DOC
101
+ Usage: everbox ls [path]
102
+ 显示当前目录下的文件和目录
103
+ DOC
104
+ when 'lsdir'
105
+ puts <<DOC
106
+ Usage: everbox lsdir
107
+ 显示当前目录下的目录
108
+ DOC
109
+
110
+ when 'mirror'
111
+ puts <<DOC
112
+ Usage:
113
+ everbox mirror DIRNAME
114
+ 下载目录到本地
115
+
116
+ everbox mirror -R DIRNAME
117
+ 上传目录到服务器
118
+ DOC
119
+ when 'mkdir'
120
+ puts <<DOC
121
+ Usage: everbox mkdir DIRNAME
122
+ 创建目录
123
+ DOC
124
+ when 'prepare_put'
125
+ puts <<DOC
126
+ Usage: everbox prepare_put FILENAME
127
+ 只运行 prepare_put 部分
128
+ DOC
129
+ when 'put'
130
+ puts <<DOC
131
+ Usage: everbox put FILENAME [FILENAME]...
132
+ 上传文件
133
+ DOC
134
+ when 'pwd'
135
+ puts <<DOC
136
+ Usage: everbox pwd
137
+ 显示当前目录
138
+ DOC
139
+ when 'rm'
140
+ puts <<DOC
141
+ Usage: everbox rm DIRNAME [DIRNAME]...
142
+ 删除文件或目录
143
+ DOC
144
+ else
145
+ puts "Not Documented"
146
+ end
147
+ end
148
+
149
+ def config(*args)
150
+ if args.size == 0
151
+ @options.each do |k, v|
152
+ puts "#{k}\t#{v}"
153
+ end
154
+ elsif args.size == 2
155
+ @options[args[0].to_sym] = args[1]
156
+ else
157
+ raise "Usage: everbox config [KEY VALUE]"
158
+ end
159
+ end
160
+
161
+
162
+ def login(*args)
163
+ if args[0] == '-o' or args[0] == '--oauth'
164
+ return login_oauth
165
+ end
166
+
167
+ @username = args.shift
168
+ @password = args.shift
169
+
170
+ raise "too many arguments" unless args.empty?
171
+
172
+ if @username.nil?
173
+ @username = ask("Enter your username: ") { |q| q.echo = true }
174
+ end
175
+ if @password.nil?
176
+ @password = ask("Enter your password: ") { |q| q.echo = "*" }
177
+ end
178
+
179
+ response = consumer.request(:post, "/oauth/quick_token?login=#{CGI.escape @username}&password=#{CGI.escape @password}")
180
+ if response.code.to_i != 200
181
+ raise "login failed: #{response.body}"
182
+ end
183
+
184
+ d = CGI.parse(response.body).inject({}) do |h,(k,v)|
185
+ h[k.strip.to_sym] = v.first
186
+ h[k.strip] = v.first
187
+ h
188
+ end
189
+
190
+ access_token = OAuth::AccessToken.from_hash(self, d)
191
+ @options[:access_token] = access_token.token
192
+ @options[:access_secret] = access_token.secret
193
+ puts @options.inspect
194
+ end
195
+
196
+ def ls(path = '.')
197
+ data = {:path => File.expand_path_unix(path, @options[:pwd])}
198
+ response = access_token.post(fs(:get), JSON.dump(data), {'Content-Type' => 'text/plain' })
199
+ fail response.inspect if response.code != "200"
200
+ info = JSON.parse(response.body)
201
+ info["entries"].each do |entry|
202
+ entry = PathEntry.new(entry)
203
+ puts entry.to_line
204
+ end
205
+ end
206
+
207
+ def lsdir
208
+ data = {:path => @options[:pwd]}
209
+ info = JSON.parse(access_token.post(fs(:get), JSON.dump(data), {'Content-Type' => 'text/plain' }).body)
210
+ info["entries"].each do |entry|
211
+ entry = PathEntry.new(entry)
212
+ puts entry.to_line if entry.dir?
213
+ end
214
+ end
215
+
216
+ def mkdir(path)
217
+ path = File.expand_path_unix(path, @options[:pwd])
218
+ make_remote_path(path)
219
+ end
220
+
221
+ def make_remote_path(path, opts = {})
222
+ data = {
223
+ :path => path,
224
+ :editTime => edit_time
225
+ }
226
+ response = access_token.post(fs(:mkdir), data.to_json, {'Content-Type' => 'text/plain'})
227
+ case response.code
228
+ when "200"
229
+ #
230
+ when "409"
231
+ unless opts[:ignore_conflict]
232
+ raise Exception, "directory already exist: `#{path}'"
233
+ end
234
+ end
235
+ end
236
+
237
+ def rm(*pathes)
238
+ fail "at least one path required" if pathes.empty?
239
+ pathes = pathes.map {|path| File.expand_path_unix(path, @options[:pwd])}
240
+
241
+ data = {
242
+ :paths => pathes,
243
+ }
244
+ response = access_token.post(fs(:delete), data.to_json, {'Content-Type' => 'text/plain'})
245
+ case response.code
246
+ when "200"
247
+ #
248
+ when "409"
249
+ raise Exception, "directory already exist: `#{path}'"
250
+ else
251
+ raise UnknownResponseException, response
252
+ end
253
+ end
254
+
255
+
256
+ def get(*args)
257
+ filename = args.shift
258
+ url_only = false
259
+ if filename == '-u'
260
+ url_only = true
261
+ filename = args.shift
262
+ end
263
+
264
+ raise ArgumentError, "filename is required" if filename.nil?
265
+
266
+ path = File.expand_path_unix(filename, @options[:pwd])
267
+ download_file(path, File.expand_path_unix('.'), :url_only => url_only)
268
+ end
269
+
270
+ def cat(*args)
271
+ args.each do |filename|
272
+ path = File.expand_path_unix(filename, @options[:pwd])
273
+ data = {:path => path}
274
+ response = access_token.post(fs(:get), JSON.dump(data), {'Content-Type' => 'text/plain' })
275
+ fail response.inspect if response.code != "200"
276
+ url = JSON.parse(response.body)["dataurl"]
277
+ Net::HTTP.get_response(URI.parse(url)) do |response|
278
+ fail response.inspect if response.code != "200"
279
+ response.read_body do |seg|
280
+ STDOUT.write(seg)
281
+ STDOUT.flush
282
+ end
283
+ end
284
+ end
285
+ end
286
+
287
+ def thumbnail(*args)
288
+ args.each do |filename|
289
+ path = File.expand_path_unix(filename, @options[:pwd])
290
+ if ['.flv', '.mp4', '.3gp'].include? File.extname(path).downcase
291
+ aimType = '0x20000'
292
+ else
293
+ aimType = '0'
294
+ end
295
+ data = {:path => path, :aimType => aimType}
296
+ response = access_token.post(fs('/2/thumbnail'), JSON.dump(data), {'Content-Type' => 'text/plain' })
297
+ fail response.inspect if response.code != "200"
298
+ puts JSON.parse(response.body)["url"]
299
+ end
300
+ end
301
+
302
+ def download_file(path, local_path, opts = {})
303
+ data = {:path => path}
304
+ info = JSON.parse(access_token.post(fs(:get), JSON.dump(data), {'Content-Type' => 'text/plain' }).body)
305
+ if opts[:url_only]
306
+ puts info["dataurl"]
307
+ else
308
+ puts "Downloading `#{File.basename(path)}' with curl"
309
+ ofname = File.expand_path_unix(File.basename(path), local_path)
310
+ system('curl', '-o', ofname, info["dataurl"])
311
+ end
312
+ end
313
+
314
+ def put(*filenames)
315
+ fail "at lease one FILE required" if filenames.empty?
316
+ filenames.each do |filename|
317
+ puts "uploading #{filename}"
318
+ upload_file(filename, @options[:pwd])
319
+ puts
320
+ end
321
+ end
322
+
323
+ def prepare_put(filename)
324
+ _get_upload_urls(filename, @options[:pwd])["required"].each do |x|
325
+ puts "#{x["index"]}\t#{x["url"]}"
326
+ end
327
+ end
328
+
329
+
330
+ def _get_upload_urls(filename, remote_path)
331
+ basename = File.basename(filename)
332
+ target_path = File.expand_path_unix(basename, remote_path)
333
+ keys = calc_digests(filename)
334
+ params = {
335
+ :path => target_path,
336
+ :keys => keys,
337
+ :chunkSize => @options[:chunk_size],
338
+ :fileSize => File.open(filename).stat.size,
339
+ :base => ''
340
+ }
341
+ JSON.parse(access_token.post(fs(:prepare_put), JSON.dump(params), {'Content-Type' => 'text/plain' }).body)
342
+ end
343
+
344
+ def upload_file(filename, remote_path)
345
+ basename = File.basename(filename)
346
+ target_path = "#{remote_path}/#{basename}"
347
+ keys = calc_digests(filename)
348
+ params = {
349
+ :path => target_path,
350
+ :keys => keys,
351
+ :chunkSize => @options[:chunk_size],
352
+ :fileSize => File.open(filename).stat.size,
353
+ :base => ''
354
+ }
355
+ begin
356
+ response = access_token.post(fs(:prepare_put), JSON.dump(params), {'Content-Type' => 'text/plain' })
357
+ raise ResponseError.new(response) if response.code != '200'
358
+ rescue ResponseError => e
359
+ if e.response.code != '409'
360
+ puts "[PREPARE_PUT] meet #{e.message}, retry"
361
+ retry
362
+ else
363
+ raise
364
+ end
365
+ end
366
+ info = JSON.parse(response.body)
367
+ raise "bad response: #{info}" if info["required"].nil?
368
+ File.open(filename) do |f|
369
+ info["required"].each do |x|
370
+ begin
371
+ puts "upload block ##{x["index"]}"
372
+ f.seek(x["index"] * @options[:chunk_size])
373
+ code, response = http_request x['url'], f.read(@options[:chunk_size]), :method => :put
374
+ if code != 200
375
+ raise code.to_s
376
+ end
377
+ rescue => e
378
+ puts "[UPLOAD_BLOCK] meet #{e.class}: #{e.message}, retry"
379
+ retry
380
+ end
381
+ end
382
+ end
383
+
384
+
385
+ ftime = (Time.now.to_i * 1000 * 1000 * 10).to_s
386
+ params = params.merge :editTime => ftime, :mimeType => 'application/octet-stream'
387
+ code, response = access_token.post(fs(:commit_put), params.to_json, {'Content-Type' => 'text/plain'})
388
+ pp code, response
389
+ rescue ResponseError
390
+ raise
391
+ rescue => e
392
+ puts "[UPLOAD_FILE] meet #{e.class}: #{e.message}, retry"
393
+ retry
394
+ end
395
+
396
+
397
+ def dump_config
398
+ config = {}
399
+ [:access_token, :access_secret, :pwd, :consumer_key, :consumer_secret, :oauth_site, :fs_site].each do |k|
400
+ config[k] = @options[k] unless @options[k].nil?
401
+ end
402
+ save_config(@options[:config_file], config)
403
+ end
404
+
405
+ def pwd
406
+ puts @options[:pwd]
407
+ end
408
+
409
+ def cd(newpath = nil)
410
+ newpath ||= "/home"
411
+ @options[:pwd] = File.expand_path_unix(newpath, @options[:pwd])
412
+ @options[:pwd] = "/home" unless @options[:pwd].start_with? "/home"
413
+ puts "current dir: #{@options[:pwd]}"
414
+ end
415
+
416
+ def mirror(*args)
417
+ raise Exception, "everbox mirror [-R] pathname" if args.empty?
418
+
419
+ upload = false
420
+ if args[0] == '-R'
421
+ upload = true
422
+ args.shift
423
+ end
424
+
425
+ path = args.shift
426
+ if path == '--'
427
+ path = args.shift
428
+ end
429
+
430
+ if path.nil? or ! args.empty?
431
+ raise Exception, "everbox mirror [-R] pathname"
432
+ end
433
+
434
+ if upload
435
+ upload_path(path)
436
+ else
437
+ download_path(path)
438
+ end
439
+ end
440
+
441
+ def upload_path(path)
442
+ local_path = File.expand_path_unix(path)
443
+ remote_path = @options[:pwd]
444
+
445
+ jobs = []
446
+ jobs << [remote_path, local_path]
447
+ until jobs.empty?
448
+ remote_path, local_path = jobs.pop
449
+ if File.directory? local_path
450
+ puts "upload dir: #{local_path}"
451
+ new_remote_path = File.expand_path_unix(File.basename(local_path), remote_path)
452
+ begin
453
+ make_remote_path(new_remote_path, :ignore_conflict=>true)
454
+ rescue => e
455
+ puts "[MAKE_REMOTE_PATH] meet #{e.class}: #{e}, retry"
456
+ retry
457
+ end
458
+
459
+ remote_filenames = []
460
+ data = {:path => new_remote_path}
461
+ response = access_token.post(fs(:get), JSON.dump(data), {'Content-Type' => 'text/plain' })
462
+ fail response.inspect if response.code != "200"
463
+ info = JSON.parse(response.body)
464
+ info["entries"].each do |entry|
465
+ entry = PathEntry.new(entry)
466
+ remote_filenames << entry.basename if entry.file?
467
+ end
468
+
469
+ Dir.entries(local_path).each do |filename|
470
+ next if ['.', '..'].include? filename
471
+ if remote_filenames.include? filename
472
+ puts "file already exist, ignored: #{filename}"
473
+ next
474
+ end
475
+ x = "#{local_path}/#{filename}"
476
+ if ! File.symlink? x and (File.directory? x or File.file? x)
477
+ jobs << [new_remote_path, x]
478
+ end
479
+ end
480
+ elsif File.file? local_path and ! File.symlink? local_path
481
+ puts "uploading #{local_path}"
482
+ begin
483
+ upload_file(local_path, remote_path)
484
+ rescue ResponseError => e
485
+ if e.response.code == "409"
486
+ puts "file already exist, ignored"
487
+ else
488
+ raise e
489
+ end
490
+ end
491
+ end
492
+ end
493
+ end
494
+
495
+ def download_path(path)
496
+ local_path = File.expand_path_unix('.')
497
+ remote_path = File.expand_path_unix(path, @options[:pwd])
498
+
499
+ jobs = []
500
+ jobs << [remote_path, local_path]
501
+
502
+ until jobs.empty?
503
+ remote_path, local_path = jobs.pop
504
+ entry = path_info(remote_path)
505
+ if entry.nil? or entry.deleted?
506
+ next
507
+ end
508
+
509
+ if entry.dir?
510
+ new_local_path = File.expand_path_unix(entry.basename, local_path)
511
+ FileUtils.makedirs(new_local_path)
512
+ entry.entries.each do |x|
513
+ jobs << [x.path, new_local_path]
514
+ end
515
+ else
516
+ download_file(remote_path, local_path)
517
+ end
518
+ end
519
+ end
520
+
521
+ # 显示用户信息
522
+ def info
523
+ user_info
524
+ puts
525
+ fs_info
526
+ end
527
+
528
+ def user_info
529
+ response = access_token.get '/api/1/user_info'
530
+ if response.code == '200'
531
+ res = JSON.parse(response.body)
532
+ if res["code"] == 0
533
+ user = User.new(res["user"])
534
+ puts user
535
+ return
536
+ end
537
+ end
538
+ puts "fetch user info failed"
539
+ puts " code: #{response.code}"
540
+ puts " body: #{response.body}"
541
+ end
542
+
543
+ def fs_info
544
+ response = access_token.get fs :info
545
+ data = JSON.parse(response.body)
546
+ puts "Disk Space Info"
547
+ puts " used: #{data["used"]}"
548
+ puts " total: #{data["total"]}"
549
+ end
550
+ protected
551
+
552
+ def fs(path)
553
+ path = path.to_s
554
+ path = '/' + path unless path.start_with? '/'
555
+ @options[:fs_site] + path
556
+ end
557
+
558
+ def login_oauth
559
+ request_token = consumer.get_request_token
560
+ url = request_token.authorize_url
561
+ puts "open url in your browser: #{url}"
562
+ Launchy.open(url)
563
+ STDOUT.write "please input the verification code: "
564
+ STDOUT.flush
565
+ verification_code = STDIN.readline.strip
566
+ access_token = request_token.get_access_token :oauth_verifier => verification_code
567
+ @options[:access_token] = access_token.token
568
+ @options[:access_secret] = access_token.secret
569
+ puts @options.inspect
570
+ end
571
+
572
+ def path_info(path)
573
+ data = {:path => path}
574
+ info = JSON.parse(access_token.post(fs(:get), JSON.dump(data), {'Content-Type' => 'text/plain' }).body)
575
+ PathEntry.new(info)
576
+ end
577
+
578
+ def consumer
579
+ OAuth::Consumer.new @options[:consumer_key], @options[:consumer_secret], {
580
+ :site => @options[:oauth_site]
581
+ }
582
+ end
583
+
584
+ def access_token
585
+ raise "please login first" if @options[:access_token].nil? or @options[:access_secret].nil?
586
+ OAuth::AccessToken.new(consumer, @options[:access_token], @options[:access_secret])
587
+ end
588
+
589
+ def load_config(config_file)
590
+ YAML.load_file(File.expand_path_unix(config_file))
591
+ rescue Errno::ENOENT
592
+ {}
593
+ rescue => e
594
+ EverboxClient.logger.info("load config file #{config_file} failed: #{e.class}")
595
+ {}
596
+ end
597
+
598
+ def save_config(config_file, config)
599
+ config_file = File.expand_path_unix(config_file)
600
+ FileUtils.makedirs(File.dirname(config_file))
601
+ File.open(config_file, 'w') do |ofile|
602
+ YAML.dump(config, ofile)
603
+ end
604
+ end
605
+
606
+ def calc_digests(fname)
607
+ res = []
608
+ File.open(fname) do |ifile|
609
+ while (data = ifile.read(@options[:chunk_size])) do
610
+ res << urlsafe_base64(Digest::SHA1.digest(data))
611
+ end
612
+ end
613
+ res
614
+ end
615
+
616
+ def urlsafe_base64(content)
617
+ Base64.encode64(content).strip.gsub('+', '-').gsub('/','_')
618
+ end
619
+
620
+ def http_request url, data = nil, options = {}
621
+ begin
622
+ options[:method] = :post unless options[:method]
623
+ case options[:method]
624
+ when :get
625
+ response = RestClient.get url, data, :content_type => options[:content_type]
626
+ when :post
627
+ response = RestClient.post url, data, :content_type => options[:content_type]
628
+ when :put
629
+ response = RestClient.put url, data
630
+ end
631
+ body = response.body
632
+ data = nil
633
+ data = JSON.parse body unless body.empty?
634
+ [response.code.to_i, data]
635
+ rescue => e
636
+ EverboxClient.logger.error e
637
+ code = 0
638
+ data = nil
639
+ body = nil
640
+ res = e.response if e.respond_to? :response
641
+ begin
642
+ code = res.code if res.respond_to? :code
643
+ body = res.body if res.respond_to? :body
644
+ data = JSON.parse body unless body.empty?
645
+ rescue
646
+ data = body
647
+ end
648
+ [code, data]
649
+ end
650
+ end
651
+
652
+ def edit_time(time = nil)
653
+ time ||= Time.now
654
+ (time.to_i * 1000 * 1000 * 10).to_s
655
+ end
656
+
657
+ end
658
+ end
@@ -0,0 +1,3 @@
1
+ module EverboxClient
2
+ VERSION = "0.0.9"
3
+ end
@@ -0,0 +1,35 @@
1
+ require 'spec_helper'
2
+
3
+ require 'json'
4
+
5
+ require 'everbox_client'
6
+
7
+ module EverboxClient
8
+ describe PathEntry do
9
+ context "path" do
10
+ ENTRY_1 = %Q[{"editTime":"12898149764580000","ver":"MDAwMDAwMDAwMDAwMDAwOQ==","type":2,"path":"/home/foo","fileSize":"1932891"}]
11
+
12
+ it "should works" do
13
+ entry = PathEntry.new(JSON.parse(ENTRY_1))
14
+ entry.file?.should == false
15
+ entry.dir?.should == true
16
+ entry.deleted?.should == false
17
+ entry.basename.should == "foo"
18
+ entry.to_line.should == "1932891\tfoo/\n"
19
+ end
20
+ end
21
+
22
+ context "file" do
23
+ ENTRY_1 = %Q[{"editTime":"12898149764580000","ver":"MDAwMDAwMDAwMDAwMDAwOQ==","type":1,"path":"/home/foo","fileSize":"1932891"}]
24
+
25
+ it "should works" do
26
+ entry = PathEntry.new(JSON.parse(ENTRY_1))
27
+ entry.file?.should == true
28
+ entry.dir?.should == false
29
+ entry.deleted?.should == false
30
+ entry.basename.should == "foo"
31
+ entry.to_line.should == "1932891\tfoo\n"
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,11 @@
1
+ require 'spec_helper'
2
+
3
+ require 'everbox_client/runner'
4
+
5
+ module EverboxClient
6
+ describe Runner do
7
+ it "should works" do
8
+ Runner.new.ls
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ require 'everbox_client'
2
+
3
+ describe EverboxClient do
4
+ it "autoload should works" do
5
+ EverboxClient::PathEntry.should be_a(Class)
6
+ end
7
+ end
File without changes
metadata ADDED
@@ -0,0 +1,151 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: everbox_client
3
+ version: !ruby/object:Gem::Version
4
+ prerelease:
5
+ version: 0.0.9
6
+ platform: ruby
7
+ authors:
8
+ - LI Daobing
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+
13
+ date: 2011-12-13 00:00:00 +08:00
14
+ default_executable:
15
+ dependencies:
16
+ - !ruby/object:Gem::Dependency
17
+ name: oauth
18
+ prerelease: false
19
+ requirement: &id001 !ruby/object:Gem::Requirement
20
+ none: false
21
+ requirements:
22
+ - - ">="
23
+ - !ruby/object:Gem::Version
24
+ version: "0"
25
+ type: :runtime
26
+ version_requirements: *id001
27
+ - !ruby/object:Gem::Dependency
28
+ name: highline
29
+ prerelease: false
30
+ requirement: &id002 !ruby/object:Gem::Requirement
31
+ none: false
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: "0"
36
+ type: :runtime
37
+ version_requirements: *id002
38
+ - !ruby/object:Gem::Dependency
39
+ name: json_pure
40
+ prerelease: false
41
+ requirement: &id003 !ruby/object:Gem::Requirement
42
+ none: false
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: "0"
47
+ type: :runtime
48
+ version_requirements: *id003
49
+ - !ruby/object:Gem::Dependency
50
+ name: rest-client
51
+ prerelease: false
52
+ requirement: &id004 !ruby/object:Gem::Requirement
53
+ none: false
54
+ requirements:
55
+ - - ">="
56
+ - !ruby/object:Gem::Version
57
+ version: "0"
58
+ type: :runtime
59
+ version_requirements: *id004
60
+ - !ruby/object:Gem::Dependency
61
+ name: launchy
62
+ prerelease: false
63
+ requirement: &id005 !ruby/object:Gem::Requirement
64
+ none: false
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: "0"
69
+ type: :runtime
70
+ version_requirements: *id005
71
+ - !ruby/object:Gem::Dependency
72
+ name: rspec
73
+ prerelease: false
74
+ requirement: &id006 !ruby/object:Gem::Requirement
75
+ none: false
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: "0"
80
+ type: :development
81
+ version_requirements: *id006
82
+ - !ruby/object:Gem::Dependency
83
+ name: rdoc
84
+ prerelease: false
85
+ requirement: &id007 !ruby/object:Gem::Requirement
86
+ none: false
87
+ requirements:
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: "0"
91
+ type: :development
92
+ version_requirements: *id007
93
+ description: EverBox Command Tool
94
+ email:
95
+ - lidaobing@gmail.com
96
+ executables:
97
+ - everbox
98
+ extensions: []
99
+
100
+ extra_rdoc_files:
101
+ - README.rdoc
102
+ - CHANGELOG.rdoc
103
+ files:
104
+ - bin/everbox
105
+ - lib/everbox_client.rb
106
+ - lib/everbox_client/version.rb
107
+ - lib/everbox_client/models/path_entry.rb
108
+ - lib/everbox_client/models/account.rb
109
+ - lib/everbox_client/models/user.rb
110
+ - lib/everbox_client/runner.rb
111
+ - lib/everbox_client/cli.rb
112
+ - lib/everbox_client/exceptions.rb
113
+ - spec/everbox_client_spec.rb
114
+ - spec/spec_helper.rb
115
+ - spec/everbox_client/models/path_entry_spec.rb
116
+ - spec/everbox_client/runner_spec.rb
117
+ - README.rdoc
118
+ - CHANGELOG.rdoc
119
+ has_rdoc: true
120
+ homepage: http://www.everbox.com/
121
+ licenses: []
122
+
123
+ post_install_message:
124
+ rdoc_options: []
125
+
126
+ require_paths:
127
+ - lib
128
+ required_ruby_version: !ruby/object:Gem::Requirement
129
+ none: false
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: "0"
134
+ required_rubygems_version: !ruby/object:Gem::Requirement
135
+ none: false
136
+ requirements:
137
+ - - ">="
138
+ - !ruby/object:Gem::Version
139
+ version: "0"
140
+ requirements: []
141
+
142
+ rubyforge_project:
143
+ rubygems_version: 1.5.3
144
+ signing_key:
145
+ specification_version: 3
146
+ summary: EverBox Command Tool
147
+ test_files:
148
+ - spec/everbox_client_spec.rb
149
+ - spec/spec_helper.rb
150
+ - spec/everbox_client/models/path_entry_spec.rb
151
+ - spec/everbox_client/runner_spec.rb