everbox_client 0.0.9

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