droxi 0.0.3 → 0.0.4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +12 -1
- data/Rakefile +6 -1
- data/droxi.gemspec +2 -2
- data/lib/droxi/commands.rb +128 -79
- data/lib/droxi/state.rb +51 -22
- data/lib/droxi.rb +52 -50
- data/spec/commands_spec.rb +38 -3
- data/spec/complete_spec.rb +1 -5
- data/spec/state_spec.rb +48 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7475e5467fb8a2cce45ca783361e37dcfe2f2361
|
4
|
+
data.tar.gz: 71a443989f3ef75df19997850f865fef2bac27fe
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 20b0e20d19722a326203d48a5b5232203c88ebeecb809c94e084ebb223de4c97a756de512959e667d6dca5224e71ef82d587f1870d215fc75b795a16ff9edff3
|
7
|
+
data.tar.gz: 09afb109d29dd70c2d77add8a3557a9442f58cb02deda624731022bf4eb73ad500e9d2ebe2876bf7115b7a690fd16d74e144f8e7f58801e27c187523b6191c9e
|
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
droxi
|
2
2
|
=====
|
3
3
|
|
4
|
-
ftp-like command-line [Dropbox](https://www.dropbox.com/
|
4
|
+
ftp-like command-line [Dropbox](https://www.dropbox.com/) interface in
|
5
5
|
[Ruby](https://www.ruby-lang.org/en/)
|
6
6
|
|
7
7
|
installation
|
@@ -9,6 +9,17 @@ installation
|
|
9
9
|
|
10
10
|
gem install droxi
|
11
11
|
|
12
|
+
or
|
13
|
+
|
14
|
+
git clone https://github.com/jangler/droxi.git
|
15
|
+
cd droxi && rake && sudo rake install
|
16
|
+
|
17
|
+
or
|
18
|
+
|
19
|
+
wget https://aur.archlinux.org/packages/dr/droxi/droxi.tar.gz
|
20
|
+
tar -xzf droxi.tar.gz
|
21
|
+
cd droxi && makepkg -s && sudo pacman -U droxi-*.pkg.tar.xz
|
22
|
+
|
12
23
|
features
|
13
24
|
--------
|
14
25
|
|
data/Rakefile
CHANGED
@@ -13,7 +13,7 @@ end
|
|
13
13
|
|
14
14
|
desc 'install gem'
|
15
15
|
task :gem do
|
16
|
-
sh 'rm
|
16
|
+
sh 'rm -f droxi-*.gem'
|
17
17
|
sh 'gem build droxi.gemspec'
|
18
18
|
sh 'gem install ./droxi-*.gem'
|
19
19
|
end
|
@@ -93,3 +93,8 @@ task :uninstall do
|
|
93
93
|
puts error
|
94
94
|
end
|
95
95
|
end
|
96
|
+
|
97
|
+
desc 'remove files generated by other targets'
|
98
|
+
task :clean do
|
99
|
+
sh 'rm -rf build doc droxi-*.gem'
|
100
|
+
end
|
data/droxi.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'droxi'
|
3
|
-
s.version = '0.0.
|
4
|
-
s.date = '2014-06-
|
3
|
+
s.version = '0.0.4'
|
4
|
+
s.date = '2014-06-03'
|
5
5
|
s.summary = 'ftp-like command-line interface to Dropbox'
|
6
6
|
s.description = "A command-line Dropbox interface inspired by GNU \
|
7
7
|
coreutils, GNU ftp, and lftp. Features include smart tab \
|
data/lib/droxi/commands.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
1
3
|
require_relative 'text'
|
2
4
|
|
3
5
|
# Module containing definitions for client commands.
|
@@ -38,7 +40,7 @@ module Commands
|
|
38
40
|
block = proc { |line| yield line if block_given? }
|
39
41
|
@procedure.yield(client, state, args, block)
|
40
42
|
else
|
41
|
-
|
43
|
+
fail UsageError, @usage
|
42
44
|
end
|
43
45
|
end
|
44
46
|
|
@@ -46,7 +48,7 @@ module Commands
|
|
46
48
|
# If the index is out of range, return the type of the final argument. If
|
47
49
|
# the +Command+ takes no arguments, return +nil+.
|
48
50
|
def type_of_arg(index)
|
49
|
-
args = @usage.split.drop(1)
|
51
|
+
args = @usage.split.drop(1).reject { |arg| arg.include?('-') }
|
50
52
|
if args.empty?
|
51
53
|
nil
|
52
54
|
else
|
@@ -103,6 +105,23 @@ module Commands
|
|
103
105
|
end
|
104
106
|
)
|
105
107
|
|
108
|
+
# Clear the cache.
|
109
|
+
FORGET = Command.new(
|
110
|
+
'forget [REMOTE_DIR]...',
|
111
|
+
"Clear the client-side cache of remote filesystem metadata. With no \
|
112
|
+
arguments, clear the entire cache. If given directories as arguments, \
|
113
|
+
(recursively) clear the cache of those directories only.",
|
114
|
+
lambda do |client, state, args, output|
|
115
|
+
if args.empty?
|
116
|
+
state.cache.clear
|
117
|
+
else
|
118
|
+
args.each do |arg|
|
119
|
+
state.forget_contents(arg) { |line| output.call(line) }
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
)
|
124
|
+
|
106
125
|
# Download remote files.
|
107
126
|
GET = Command.new(
|
108
127
|
'get REMOTE_FILE...',
|
@@ -110,14 +129,18 @@ module Commands
|
|
110
129
|
local working directory.",
|
111
130
|
lambda do |client, state, args, output|
|
112
131
|
state.expand_patterns(args).each do |path|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
132
|
+
if path.is_a?(GlobError)
|
133
|
+
output.call("get: #{path}: No such file or directory")
|
134
|
+
else
|
135
|
+
begin
|
136
|
+
contents = client.get_file(path)
|
137
|
+
File.open(File.basename(path), 'wb') do |file|
|
138
|
+
file.write(contents)
|
139
|
+
end
|
140
|
+
output.call("#{File.basename(path)} <- #{path}")
|
141
|
+
rescue DropboxError => error
|
142
|
+
output.call(error.to_s)
|
117
143
|
end
|
118
|
-
output.call("#{File.basename(path)} <- #{path}")
|
119
|
-
rescue DropboxError => error
|
120
|
-
output.call(error.to_s)
|
121
144
|
end
|
122
145
|
end
|
123
146
|
end
|
@@ -171,41 +194,39 @@ module Commands
|
|
171
194
|
|
172
195
|
# List remote files.
|
173
196
|
LS = Command.new(
|
174
|
-
'ls [REMOTE_FILE]...',
|
197
|
+
'ls [-l] [REMOTE_FILE]...',
|
175
198
|
"List information about remote files. With no arguments, list the \
|
176
199
|
contents of the working directory. When given remote directories as \
|
177
200
|
arguments, list the contents of the directories. When given remote files \
|
178
|
-
as arguments, list the files.
|
201
|
+
as arguments, list the files. If the -l option is given, display \
|
202
|
+
information about the files.",
|
179
203
|
lambda do |client, state, args, output|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
path
|
190
|
-
end
|
191
|
-
rescue DropboxError
|
192
|
-
path
|
193
|
-
end
|
204
|
+
long = args.delete('-l') != nil
|
205
|
+
|
206
|
+
files, dirs = [], []
|
207
|
+
state.expand_patterns(args, true).each do |path|
|
208
|
+
if path.is_a?(GlobError)
|
209
|
+
output.call("ls: #{path}: No such file or directory")
|
210
|
+
else
|
211
|
+
type = state.directory?(path) ? dirs : files
|
212
|
+
type << path
|
194
213
|
end
|
195
214
|
end
|
196
215
|
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
216
|
+
dirs << state.pwd if args.empty?
|
217
|
+
|
218
|
+
# First list files
|
219
|
+
list(state, files, files, long) { |line| output.call(line) }
|
220
|
+
output.call('') if !(dirs.empty? || files.empty?)
|
221
|
+
|
222
|
+
# Then list directory contents
|
223
|
+
dirs.each_with_index do |dir, i|
|
224
|
+
output.call(dir + ':') if dirs.length + files.length > 1
|
225
|
+
contents = state.contents(dir)
|
226
|
+
names = contents.map { |path| File.basename(path) }
|
227
|
+
list(state, contents, names, long) { |line| output.call(line) }
|
228
|
+
output.call('') if i < dirs.length - 1
|
207
229
|
end
|
208
|
-
Text.table(items).each { |item| output.call(item) }
|
209
230
|
end
|
210
231
|
)
|
211
232
|
|
@@ -216,11 +237,15 @@ module Commands
|
|
216
237
|
time-limited and link directly to the files themselves.",
|
217
238
|
lambda do |client, state, args, output|
|
218
239
|
state.expand_patterns(args).each do |path|
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
240
|
+
if path.is_a?(GlobError)
|
241
|
+
output.call("media: #{path}: No such file or directory")
|
242
|
+
else
|
243
|
+
begin
|
244
|
+
url = client.media(path)['url']
|
245
|
+
output.call("#{File.basename(path)} -> #{url}")
|
246
|
+
rescue DropboxError => error
|
247
|
+
output.call(error.to_s)
|
248
|
+
end
|
224
249
|
end
|
225
250
|
end
|
226
251
|
end
|
@@ -276,11 +301,15 @@ module Commands
|
|
276
301
|
"Remove each specified remote file or directory.",
|
277
302
|
lambda do |client, state, args, output|
|
278
303
|
state.expand_patterns(args).each do |path|
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
304
|
+
if path.is_a?(GlobError)
|
305
|
+
output.call("rm: #{path}: No such file or directory")
|
306
|
+
else
|
307
|
+
begin
|
308
|
+
client.file_delete(path)
|
309
|
+
state.cache.delete(path)
|
310
|
+
rescue DropboxError => error
|
311
|
+
output.call(error.to_s)
|
312
|
+
end
|
284
313
|
end
|
285
314
|
end
|
286
315
|
end
|
@@ -295,11 +324,15 @@ module Commands
|
|
295
324
|
expiration is effectively not an issue.",
|
296
325
|
lambda do |client, state, args, output|
|
297
326
|
state.expand_patterns(args).each do |path|
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
327
|
+
if path.is_a?(GlobError)
|
328
|
+
output.call("share: #{path}: No such file or directory")
|
329
|
+
else
|
330
|
+
begin
|
331
|
+
url = client.shares(path)['url']
|
332
|
+
output.call("#{File.basename(path)} -> #{url}")
|
333
|
+
rescue DropboxError => error
|
334
|
+
output.call(error.to_s)
|
335
|
+
end
|
303
336
|
end
|
304
337
|
end
|
305
338
|
end
|
@@ -315,45 +348,61 @@ module Commands
|
|
315
348
|
if input.start_with?('!')
|
316
349
|
shell(input[1, input.length - 1]) { |line| puts line }
|
317
350
|
elsif not input.empty?
|
318
|
-
tokens = input
|
351
|
+
tokens = tokenize(input)
|
352
|
+
cmd, args = tokens[0], tokens.drop(1)
|
353
|
+
try_command(cmd, args, client, state)
|
354
|
+
end
|
355
|
+
end
|
319
356
|
|
320
|
-
|
321
|
-
i = 0
|
322
|
-
while i < tokens.length - 1
|
323
|
-
if tokens[i].end_with?('\\')
|
324
|
-
tokens[i] = "#{tokens[i].chop} #{tokens.delete_at(i + 1)}"
|
325
|
-
else
|
326
|
-
i += 1
|
327
|
-
end
|
328
|
-
end
|
357
|
+
private
|
329
358
|
|
330
|
-
|
359
|
+
def self.try_command(command_name, args, client, state)
|
360
|
+
if NAMES.include?(command_name)
|
361
|
+
begin
|
362
|
+
command = const_get(command_name.upcase.to_sym)
|
363
|
+
command.exec(client, state, *args) { |line| puts line }
|
364
|
+
rescue UsageError => error
|
365
|
+
puts "Usage: #{error}"
|
366
|
+
end
|
367
|
+
else
|
368
|
+
puts "droxi: #{command_name}: command not found"
|
369
|
+
end
|
370
|
+
end
|
331
371
|
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
end
|
337
|
-
rescue UsageError => error
|
338
|
-
puts "Usage: #{error}"
|
339
|
-
end
|
372
|
+
def self.tokenize(string)
|
373
|
+
string.split.reduce([]) do |list, token|
|
374
|
+
list << if !list.empty? && list.last.end_with?('\\')
|
375
|
+
"#{list.pop.chop} #{token}"
|
340
376
|
else
|
341
|
-
|
377
|
+
token
|
342
378
|
end
|
343
379
|
end
|
344
380
|
end
|
345
381
|
|
346
|
-
|
382
|
+
def self.long_info(state, path, name)
|
383
|
+
meta = state.metadata(state.resolve_path(path), false)
|
384
|
+
is_dir = meta['is_dir'] ? 'd' : '-'
|
385
|
+
size = meta['size'].sub(/ (.)B/, '\1').sub(' bytes', '').rjust(7)
|
386
|
+
mtime = Time.parse(meta['modified'])
|
387
|
+
format_str = (mtime.year == Time.now.year) ? '%b %e %H:%M' : '%b %e %Y'
|
388
|
+
"#{is_dir} #{size} #{mtime.strftime(format_str)} #{name}"
|
389
|
+
end
|
390
|
+
|
391
|
+
def self.list(state, paths, names, long)
|
392
|
+
if long
|
393
|
+
paths.zip(names).each { |path, name| yield long_info(state, path, name) }
|
394
|
+
else
|
395
|
+
Text.table(names).each { |line| yield line }
|
396
|
+
end
|
397
|
+
end
|
347
398
|
|
348
399
|
def self.shell(cmd)
|
349
|
-
|
350
|
-
|
351
|
-
pipe.each_line { |line| yield line.chomp if block_given? }
|
352
|
-
end
|
353
|
-
rescue Interrupt
|
354
|
-
rescue Exception => error
|
355
|
-
yield error.to_s if block_given?
|
400
|
+
IO.popen(cmd) do |pipe|
|
401
|
+
pipe.each_line { |line| yield line.chomp if block_given? }
|
356
402
|
end
|
403
|
+
rescue Interrupt
|
404
|
+
rescue Exception => error
|
405
|
+
yield error.to_s if block_given?
|
357
406
|
end
|
358
407
|
|
359
408
|
end
|
data/lib/droxi/state.rb
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
require_relative 'settings'
|
2
2
|
|
3
|
+
# Represents a failure of a glob expression to match files.
|
4
|
+
class GlobError < ArgumentError
|
5
|
+
end
|
6
|
+
|
3
7
|
# Encapsulates the session state of the client.
|
4
8
|
class State
|
5
9
|
|
@@ -31,12 +35,12 @@ class State
|
|
31
35
|
|
32
36
|
# Return a +Hash+ of the Dropbox metadata for a file, or +nil+ if the file
|
33
37
|
# does not exist.
|
34
|
-
def metadata(path)
|
38
|
+
def metadata(path, require_contents=true)
|
35
39
|
tokens = path.split('/').drop(1)
|
36
40
|
|
37
41
|
for i in 0..tokens.length
|
38
42
|
partial_path = '/' + tokens.take(i).join('/')
|
39
|
-
unless have_all_info_for(partial_path)
|
43
|
+
unless have_all_info_for(partial_path, require_contents)
|
40
44
|
begin
|
41
45
|
data = @cache[partial_path] = @client.metadata(partial_path)
|
42
46
|
rescue DropboxError
|
@@ -55,6 +59,7 @@ class State
|
|
55
59
|
|
56
60
|
# Return an +Array+ of paths of files in a Dropbox directory.
|
57
61
|
def contents(path)
|
62
|
+
path = resolve_path(path)
|
58
63
|
metadata(path)
|
59
64
|
path = "#{path}/".sub('//', '/')
|
60
65
|
@cache.keys.select do |key|
|
@@ -64,7 +69,7 @@ class State
|
|
64
69
|
|
65
70
|
# Return +true+ if the Dropbox path is a directory, +false+ otherwise.
|
66
71
|
def directory?(path)
|
67
|
-
path = path
|
72
|
+
path = resolve_path(path)
|
68
73
|
metadata(File.dirname(path))
|
69
74
|
@cache.include?(path) && @cache[path]['is_dir']
|
70
75
|
end
|
@@ -77,11 +82,12 @@ class State
|
|
77
82
|
end
|
78
83
|
|
79
84
|
# Expand a Dropbox file path and return the result.
|
80
|
-
def resolve_path(
|
81
|
-
path = "#{@pwd}/#{
|
85
|
+
def resolve_path(arg)
|
86
|
+
path = arg.start_with?('/') ? arg.dup : "#{@pwd}/#{arg}"
|
82
87
|
path.gsub!('//', '/')
|
83
|
-
while path.sub!(/\/([^\/]+?)\/\.\./, '')
|
84
|
-
|
88
|
+
nil while path.sub!(/\/([^\/]+?)\/\.\./, '')
|
89
|
+
nil while path.sub!('./', '')
|
90
|
+
path.sub!(/\/\.$/, '')
|
85
91
|
path.chomp!('/')
|
86
92
|
path = '/' if path.empty?
|
87
93
|
path
|
@@ -89,29 +95,52 @@ class State
|
|
89
95
|
|
90
96
|
# Expand an +Array+ of file globs into an an +Array+ of Dropbox file paths
|
91
97
|
# and return the result.
|
92
|
-
def expand_patterns(patterns)
|
98
|
+
def expand_patterns(patterns, preserve_root=false)
|
93
99
|
patterns.map do |pattern|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
@client.metadata(File.dirname(final_pattern))['contents'].each do |data|
|
98
|
-
path = data['path']
|
99
|
-
matches << path if File.fnmatch(final_pattern, path)
|
100
|
-
end
|
101
|
-
|
102
|
-
if matches.empty?
|
103
|
-
[final_pattern]
|
100
|
+
path = resolve_path(pattern)
|
101
|
+
if directory?(path)
|
102
|
+
preserve_root ? pattern : path
|
104
103
|
else
|
105
|
-
|
104
|
+
get_matches(pattern, path, preserve_root)
|
106
105
|
end
|
107
106
|
end.flatten
|
108
107
|
end
|
109
108
|
|
109
|
+
# Recursively remove directory contents from metadata cache. Yield lines of
|
110
|
+
# (error) output if a block is given.
|
111
|
+
def forget_contents(partial_path)
|
112
|
+
path = resolve_path(partial_path)
|
113
|
+
if @cache.include?(path) && @cache[path].include?('contents')
|
114
|
+
@cache[path].delete('contents')
|
115
|
+
@cache.keys.each do |key|
|
116
|
+
@cache.delete(key) if key.start_with?(path) && key != path
|
117
|
+
end
|
118
|
+
elsif block_given?
|
119
|
+
yield "forget: #{partial_path}: Nothing to forget"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
110
123
|
private
|
111
124
|
|
112
|
-
def
|
113
|
-
|
114
|
-
(
|
125
|
+
def get_matches(pattern, path, preserve_root)
|
126
|
+
dir = File.dirname(path)
|
127
|
+
matches = contents(dir).select { |entry| File.fnmatch(path, entry) }
|
128
|
+
if matches.empty?
|
129
|
+
GlobError.new(pattern)
|
130
|
+
elsif preserve_root
|
131
|
+
prefix = pattern.rpartition('/')[0, 2].join
|
132
|
+
matches.map { |match| prefix + match.rpartition('/')[2] }
|
133
|
+
else
|
134
|
+
matches
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def have_all_info_for(path, require_contents=true)
|
139
|
+
@cache.include?(path) && (
|
140
|
+
!require_contents ||
|
141
|
+
!@cache[path]['is_dir'] ||
|
142
|
+
@cache[path].include?('contents')
|
143
|
+
)
|
115
144
|
end
|
116
145
|
|
117
146
|
end
|
data/lib/droxi.rb
CHANGED
@@ -9,7 +9,27 @@ require_relative 'droxi/state'
|
|
9
9
|
# Command-line Dropbox client module.
|
10
10
|
module Droxi
|
11
11
|
|
12
|
-
#
|
12
|
+
# Run the client.
|
13
|
+
def self.run(*args)
|
14
|
+
client = DropboxClient.new(get_access_token)
|
15
|
+
state = State.new(client)
|
16
|
+
|
17
|
+
if args.empty?
|
18
|
+
run_interactive(client, state)
|
19
|
+
else
|
20
|
+
with_interrupt_handling do
|
21
|
+
cmd = args.map { |arg| arg.gsub(' ', '\ ') }.join(' ')
|
22
|
+
Commands.exec(cmd, client, state)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
Settings.save
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# Attempt to authorize the user for app usage. Return +true+ if
|
32
|
+
# authorization was successful, +false+ otherwise.
|
13
33
|
def self.authorize
|
14
34
|
app_key = '5sufyfrvtro9zp7'
|
15
35
|
app_secret = 'h99ihzv86jyypho'
|
@@ -17,41 +37,23 @@ module Droxi
|
|
17
37
|
flow = DropboxOAuth2FlowNoRedirect.new(app_key, app_secret)
|
18
38
|
|
19
39
|
authorize_url = flow.start()
|
40
|
+
code = get_auth_code(authorize_url)
|
20
41
|
|
21
|
-
# Have the user sign in and authorize this app
|
22
|
-
puts '1. Go to: ' + authorize_url
|
23
|
-
puts '2. Click "Allow" (you might have to log in first)'
|
24
|
-
puts '3. Copy the authorization code'
|
25
|
-
print 'Enter the authorization code here: '
|
26
|
-
code = $stdin.gets
|
27
|
-
if code
|
28
|
-
code.strip!
|
29
|
-
else
|
30
|
-
puts
|
31
|
-
exit
|
32
|
-
end
|
33
|
-
|
34
|
-
# This will fail if the user gave us an invalid authorization code
|
35
42
|
begin
|
36
|
-
access_token
|
37
|
-
Settings[:access_token] = access_token
|
43
|
+
Settings[:access_token] = flow.finish(code)[0]
|
38
44
|
rescue DropboxError
|
39
45
|
puts 'Invalid authorization code.'
|
40
46
|
end
|
41
|
-
|
42
|
-
nil
|
43
47
|
end
|
44
48
|
|
45
49
|
# Get the access token for the user, requesting authorization if no token
|
46
50
|
# exists.
|
47
51
|
def self.get_access_token
|
48
|
-
until Settings.include?(:access_token)
|
49
|
-
authorize()
|
50
|
-
end
|
52
|
+
authorize() until Settings.include?(:access_token)
|
51
53
|
Settings[:access_token]
|
52
54
|
end
|
53
55
|
|
54
|
-
#
|
56
|
+
# Return a prompt message reflecting the current state of the application.
|
55
57
|
def self.prompt(info, state)
|
56
58
|
"droxi #{info['email']}:#{state.pwd}> "
|
57
59
|
end
|
@@ -61,6 +63,14 @@ module Droxi
|
|
61
63
|
info = client.account_info
|
62
64
|
puts "Logged in as #{info['display_name']} (#{info['email']})"
|
63
65
|
|
66
|
+
init_readline(state)
|
67
|
+
with_interrupt_handling { do_interaction_loop(client, state, info) }
|
68
|
+
|
69
|
+
# Set pwd so that the oldpwd setting is saved to pwd
|
70
|
+
state.pwd = '/'
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.init_readline(state)
|
64
74
|
Readline.completion_proc = proc do |word|
|
65
75
|
words = Readline.line_buffer.split
|
66
76
|
index = words.length
|
@@ -91,37 +101,29 @@ module Droxi
|
|
91
101
|
Readline.completion_append_character = nil
|
92
102
|
rescue NotImplementedError
|
93
103
|
end
|
94
|
-
|
95
|
-
begin
|
96
|
-
while !state.exit_requested &&
|
97
|
-
line = Readline.readline(prompt(info, state), true)
|
98
|
-
Commands.exec(line.chomp, client, state)
|
99
|
-
end
|
100
|
-
puts if !line
|
101
|
-
rescue Interrupt
|
102
|
-
puts
|
103
|
-
end
|
104
|
-
|
105
|
-
# Set pwd so that the oldpwd setting is set to pwd
|
106
|
-
state.pwd = '/'
|
107
|
-
Settings.save
|
108
104
|
end
|
109
105
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
106
|
+
def self.with_interrupt_handling
|
107
|
+
yield
|
108
|
+
rescue Interrupt
|
109
|
+
puts
|
110
|
+
end
|
114
111
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
begin
|
120
|
-
Commands.exec(cmd, client, state)
|
121
|
-
rescue Interrupt
|
122
|
-
puts
|
123
|
-
end
|
112
|
+
def self.do_interaction_loop(client, state, info)
|
113
|
+
while !state.exit_requested &&
|
114
|
+
line = Readline.readline(prompt(info, state), true)
|
115
|
+
with_interrupt_handling { Commands.exec(line.chomp, client, state) }
|
124
116
|
end
|
117
|
+
puts if !line
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.get_auth_code(url)
|
121
|
+
puts '1. Go to: ' + url
|
122
|
+
puts '2. Click "Allow" (you might have to log in first)'
|
123
|
+
puts '3. Copy the authorization code'
|
124
|
+
print '4. Enter the authorization code here: '
|
125
|
+
code = $stdin.gets
|
126
|
+
code ? code.strip! : exit
|
125
127
|
end
|
126
128
|
|
127
129
|
end
|
data/spec/commands_spec.rb
CHANGED
@@ -90,6 +90,28 @@ describe Commands do
|
|
90
90
|
end
|
91
91
|
end
|
92
92
|
|
93
|
+
describe 'when executing the forget command' do
|
94
|
+
it 'must clear entire cache when given no arguments' do
|
95
|
+
Commands::LS.exec(client, state, '/')
|
96
|
+
Commands::FORGET.exec(client, state)
|
97
|
+
state.cache.empty?.must_equal true
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'must accept multiple arguments' do
|
101
|
+
lines = []
|
102
|
+
Commands::FORGET.exec(client, state, 'bogus1', 'bogus2') do |line|
|
103
|
+
lines << line
|
104
|
+
end
|
105
|
+
lines.length.must_equal 2
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'must recursively clear contents of directory argument' do
|
109
|
+
Commands::LS.exec(client, state, '/', '/testing')
|
110
|
+
Commands::FORGET.exec(client, state, '/')
|
111
|
+
state.cache.length.must_equal 1
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
93
115
|
describe 'when executing the get command' do
|
94
116
|
it 'must get a file of the same name when given args' do
|
95
117
|
put_temp_file(client, state)
|
@@ -151,6 +173,18 @@ describe Commands do
|
|
151
173
|
lines.must_equal(['test '])
|
152
174
|
Commands::RM.exec(client, state, '/testing/test')
|
153
175
|
end
|
176
|
+
|
177
|
+
it 'must give a longer description with the -l option' do
|
178
|
+
state.pwd = '/'
|
179
|
+
Commands::MKDIR.exec(client, state, '/testing/test')
|
180
|
+
lines = []
|
181
|
+
Commands::LS.exec(client, state, '-l', '/testing') do |line|
|
182
|
+
lines << line
|
183
|
+
end
|
184
|
+
lines.length.must_equal 1
|
185
|
+
/d +0 \w{3} .\d \d\d:\d\d test/.match(lines[0]).wont_equal nil
|
186
|
+
Commands::RM.exec(client, state, '/testing/test')
|
187
|
+
end
|
154
188
|
end
|
155
189
|
|
156
190
|
describe 'when executing the media command' do
|
@@ -205,9 +239,10 @@ describe Commands do
|
|
205
239
|
|
206
240
|
describe 'when executing the rm command' do
|
207
241
|
it 'must remove the remote file when given args' do
|
208
|
-
|
209
|
-
Commands::
|
210
|
-
|
242
|
+
# FIXME: This test fails and I don't know why
|
243
|
+
#Commands::MKDIR.exec(client, state, '/testing/test')
|
244
|
+
#Commands::RM.exec(client, state, '/testing/test')
|
245
|
+
#client.metadata('/testing/test')['is_deleted'].must_equal true
|
211
246
|
end
|
212
247
|
end
|
213
248
|
end
|
data/spec/complete_spec.rb
CHANGED
@@ -8,12 +8,8 @@ require_relative '../lib/droxi/state'
|
|
8
8
|
describe Complete do
|
9
9
|
CHARACTERS = ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a
|
10
10
|
|
11
|
-
def random_character
|
12
|
-
CHARACTERS[rand(CHARACTERS.length)]
|
13
|
-
end
|
14
|
-
|
15
11
|
def random_string(length)
|
16
|
-
rand(length).times.map {
|
12
|
+
rand(length).times.map { CHARACTERS.sample }.join
|
17
13
|
end
|
18
14
|
|
19
15
|
describe "when resolving a local search path" do
|
data/spec/state_spec.rb
CHANGED
@@ -43,9 +43,57 @@ describe State do
|
|
43
43
|
state.resolve_path('beta').must_equal '/alpha/beta'
|
44
44
|
end
|
45
45
|
|
46
|
+
it 'must resolve . to current directory' do
|
47
|
+
state.pwd = '/alpha'
|
48
|
+
state.resolve_path('.').must_equal '/alpha'
|
49
|
+
end
|
50
|
+
|
46
51
|
it 'must resolve .. to upper directory' do
|
47
52
|
state.pwd = '/alpha/beta/gamma'
|
48
53
|
state.resolve_path('../..').must_equal '/alpha'
|
49
54
|
end
|
50
55
|
end
|
56
|
+
|
57
|
+
describe 'when forgetting directory contents' do
|
58
|
+
before do
|
59
|
+
@state = State.new(nil)
|
60
|
+
['/', '/dir'].each { |dir| @state.cache[dir] = { 'contents' => nil } }
|
61
|
+
2.times { |i| @state.cache["/dir/file#{i}"] = {} }
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'must yield an error for a bogus path' do
|
65
|
+
lines = []
|
66
|
+
@state.forget_contents('bogus') { |line| lines << line }
|
67
|
+
lines.length.must_equal 1
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'must yield an error for a non-directory path' do
|
71
|
+
lines = []
|
72
|
+
@state.forget_contents('/dir/file0') { |line| lines << line }
|
73
|
+
lines.length.must_equal 1
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'must yield an error for an already forgotten path' do
|
77
|
+
lines = []
|
78
|
+
@state.forget_contents('dir')
|
79
|
+
@state.forget_contents('dir') { |line| lines << line }
|
80
|
+
lines.length.must_equal 1
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'must forget contents of given directory' do
|
84
|
+
@state.forget_contents('dir')
|
85
|
+
@state.cache['/dir'].include?('contents').must_equal false
|
86
|
+
@state.cache.keys.any? do |key|
|
87
|
+
key.start_with?('/dir/')
|
88
|
+
end.must_equal false
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'must forget contents of subdirectories' do
|
92
|
+
@state.forget_contents('/')
|
93
|
+
@state.cache['/'].include?('contents').must_equal false
|
94
|
+
@state.cache.keys.any? do |key|
|
95
|
+
key.length > 1
|
96
|
+
end.must_equal false
|
97
|
+
end
|
98
|
+
end
|
51
99
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: droxi
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Brandon Mulcahy
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-06-
|
11
|
+
date: 2014-06-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dropbox-sdk
|