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 +26 -0
- data/README.rdoc +38 -0
- data/bin/everbox +5 -0
- data/lib/everbox_client.rb +11 -0
- data/lib/everbox_client/cli.rb +129 -0
- data/lib/everbox_client/exceptions.rb +9 -0
- data/lib/everbox_client/models/account.rb +25 -0
- data/lib/everbox_client/models/path_entry.rb +47 -0
- data/lib/everbox_client/models/user.rb +23 -0
- data/lib/everbox_client/runner.rb +658 -0
- data/lib/everbox_client/version.rb +3 -0
- data/spec/everbox_client/models/path_entry_spec.rb +35 -0
- data/spec/everbox_client/runner_spec.rb +11 -0
- data/spec/everbox_client_spec.rb +7 -0
- data/spec/spec_helper.rb +0 -0
- metadata +151 -0
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,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,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,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
|
data/spec/spec_helper.rb
ADDED
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
|