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