droxi 0.0.4 → 0.0.5
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/droxi.gemspec +2 -2
- data/lib/droxi/commands.rb +101 -20
- data/lib/droxi/complete.rb +11 -7
- data/lib/droxi/settings.rb +6 -0
- data/lib/droxi/state.rb +34 -6
- data/lib/droxi/text.rb +3 -0
- data/lib/droxi.rb +12 -6
- data/spec/commands_spec.rb +76 -4
- 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: ed8c5f6d9c0df8aff83fc755d298fda9dda25adf
|
4
|
+
data.tar.gz: f882a46c0a98a0f4f69cb88f3610fc403abc23e3
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c5cbb62624375b1515ff9e7de15167ead211bb535dd427a87954c3a585865e5d25ba90236a0f717c4a491b307d6bbb01fdd430dbee3c4edfaf43e8c49a30b3da
|
7
|
+
data.tar.gz: 4c9b4bc88008c441cb8f7284be5c90470b147233f134dceb035e9dc681e3ce303168220ecfdea5deb27f2bac94fad43109aeb52b3da18d23bafa730ba8b5580d
|
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.5'
|
4
|
+
s.date = '2014-06-04'
|
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
@@ -59,12 +59,14 @@ module Commands
|
|
59
59
|
|
60
60
|
private
|
61
61
|
|
62
|
+
# Return +true+ if the given number of arguments is acceptable for the
|
63
|
+
# command, +false+ otherwise.
|
62
64
|
def num_args_ok?(num_args)
|
63
65
|
args = @usage.split.drop(1)
|
64
66
|
min_args = args.reject { |arg| arg.start_with?('[') }.length
|
65
67
|
if args.empty?
|
66
68
|
max_args = 0
|
67
|
-
elsif args.
|
69
|
+
elsif args.any? { |arg| arg.end_with?('...') }
|
68
70
|
max_args = num_args
|
69
71
|
else
|
70
72
|
max_args = args.length
|
@@ -96,6 +98,18 @@ module Commands
|
|
96
98
|
end
|
97
99
|
)
|
98
100
|
|
101
|
+
# Copy remote files.
|
102
|
+
CP = Command.new(
|
103
|
+
'cp REMOTE_FILE... REMOTE_FILE',
|
104
|
+
"When given two arguments, copies the remote file or folder at the first \
|
105
|
+
path to the second path. When given more than two arguments or when the \
|
106
|
+
final argument is a directory, copies each remote file or folder into \
|
107
|
+
that directory.",
|
108
|
+
lambda do |client, state, args, output|
|
109
|
+
cp_mv(client, state, args, output, 'cp', :file_copy)
|
110
|
+
end
|
111
|
+
)
|
112
|
+
|
99
113
|
# Terminate the session.
|
100
114
|
EXIT = Command.new(
|
101
115
|
'exit',
|
@@ -132,14 +146,12 @@ module Commands
|
|
132
146
|
if path.is_a?(GlobError)
|
133
147
|
output.call("get: #{path}: No such file or directory")
|
134
148
|
else
|
135
|
-
|
149
|
+
try_and_handle(DropboxError, output) do
|
136
150
|
contents = client.get_file(path)
|
137
151
|
File.open(File.basename(path), 'wb') do |file|
|
138
152
|
file.write(contents)
|
139
153
|
end
|
140
154
|
output.call("#{File.basename(path)} <- #{path}")
|
141
|
-
rescue DropboxError => error
|
142
|
-
output.call(error.to_s)
|
143
155
|
end
|
144
156
|
end
|
145
157
|
end
|
@@ -240,11 +252,9 @@ module Commands
|
|
240
252
|
if path.is_a?(GlobError)
|
241
253
|
output.call("media: #{path}: No such file or directory")
|
242
254
|
else
|
243
|
-
|
255
|
+
try_and_handle(DropboxError, output) do
|
244
256
|
url = client.media(path)['url']
|
245
257
|
output.call("#{File.basename(path)} -> #{url}")
|
246
|
-
rescue DropboxError => error
|
247
|
-
output.call(error.to_s)
|
248
258
|
end
|
249
259
|
end
|
250
260
|
end
|
@@ -257,16 +267,26 @@ module Commands
|
|
257
267
|
"Create remote directories.",
|
258
268
|
lambda do |client, state, args, output|
|
259
269
|
args.each do |arg|
|
260
|
-
|
270
|
+
try_and_handle(DropboxError, output) do
|
261
271
|
path = state.resolve_path(arg)
|
262
272
|
state.cache[path] = client.file_create_folder(path)
|
263
|
-
rescue DropboxError => error
|
264
|
-
output.call(error.to_s)
|
265
273
|
end
|
266
274
|
end
|
267
275
|
end
|
268
276
|
)
|
269
277
|
|
278
|
+
# Move/rename remote files.
|
279
|
+
MV = Command.new(
|
280
|
+
'mv REMOTE_FILE... REMOTE_FILE',
|
281
|
+
"When given two arguments, moves the remote file or folder at the first \
|
282
|
+
path to the second path. When given more than two arguments or when the \
|
283
|
+
final argument is a directory, moves each remote file or folder into \
|
284
|
+
that directory.",
|
285
|
+
lambda do |client, state, args, output|
|
286
|
+
cp_mv(client, state, args, output, 'mv', :file_move)
|
287
|
+
end
|
288
|
+
)
|
289
|
+
|
270
290
|
# Upload a local file.
|
271
291
|
PUT = Command.new(
|
272
292
|
'put LOCAL_FILE [REMOTE_FILE]',
|
@@ -283,14 +303,12 @@ module Commands
|
|
283
303
|
end
|
284
304
|
to_path = state.resolve_path(to_path)
|
285
305
|
|
286
|
-
|
306
|
+
try_and_handle(Exception, output) do
|
287
307
|
File.open(File.expand_path(from_path), 'rb') do |file|
|
288
308
|
data = client.put_file(to_path, file)
|
289
309
|
state.cache[data['path']] = data
|
290
310
|
output.call("#{from_path} -> #{data['path']}")
|
291
311
|
end
|
292
|
-
rescue Exception => error
|
293
|
-
output.call(error.to_s)
|
294
312
|
end
|
295
313
|
end
|
296
314
|
)
|
@@ -304,14 +322,13 @@ module Commands
|
|
304
322
|
if path.is_a?(GlobError)
|
305
323
|
output.call("rm: #{path}: No such file or directory")
|
306
324
|
else
|
307
|
-
|
325
|
+
try_and_handle(DropboxError, output) do
|
308
326
|
client.file_delete(path)
|
309
|
-
state.
|
310
|
-
rescue DropboxError => error
|
311
|
-
output.call(error.to_s)
|
327
|
+
state.cache_remove(path)
|
312
328
|
end
|
313
329
|
end
|
314
330
|
end
|
331
|
+
check_pwd(state)
|
315
332
|
end
|
316
333
|
)
|
317
334
|
|
@@ -327,11 +344,9 @@ module Commands
|
|
327
344
|
if path.is_a?(GlobError)
|
328
345
|
output.call("share: #{path}: No such file or directory")
|
329
346
|
else
|
330
|
-
|
347
|
+
try_and_handle(DropboxError, output) do
|
331
348
|
url = client.shares(path)['url']
|
332
349
|
output.call("#{File.basename(path)} -> #{url}")
|
333
|
-
rescue DropboxError => error
|
334
|
-
output.call(error.to_s)
|
335
350
|
end
|
336
351
|
end
|
337
352
|
end
|
@@ -356,6 +371,16 @@ module Commands
|
|
356
371
|
|
357
372
|
private
|
358
373
|
|
374
|
+
# Attempt to run the associated block, handling the given type of +Exception+
|
375
|
+
# by passing its +String+ representation to an output +Proc+.
|
376
|
+
def self.try_and_handle(exception_class, output)
|
377
|
+
yield
|
378
|
+
rescue exception_class => error
|
379
|
+
output.call(error.to_s)
|
380
|
+
end
|
381
|
+
|
382
|
+
# Run a command with the given name, or print an error message if usage is
|
383
|
+
# incorrect or no such command exists.
|
359
384
|
def self.try_command(command_name, args, client, state)
|
360
385
|
if NAMES.include?(command_name)
|
361
386
|
begin
|
@@ -369,6 +394,8 @@ module Commands
|
|
369
394
|
end
|
370
395
|
end
|
371
396
|
|
397
|
+
# Split a +String+ into tokens, allowing for backslash-escaped spaces, and
|
398
|
+
# return the resulting +Array+.
|
372
399
|
def self.tokenize(string)
|
373
400
|
string.split.reduce([]) do |list, token|
|
374
401
|
list << if !list.empty? && list.last.end_with?('\\')
|
@@ -379,6 +406,7 @@ module Commands
|
|
379
406
|
end
|
380
407
|
end
|
381
408
|
|
409
|
+
# Return a +String+ of information about a remote file for ls -l.
|
382
410
|
def self.long_info(state, path, name)
|
383
411
|
meta = state.metadata(state.resolve_path(path), false)
|
384
412
|
is_dir = meta['is_dir'] ? 'd' : '-'
|
@@ -388,6 +416,8 @@ module Commands
|
|
388
416
|
"#{is_dir} #{size} #{mtime.strftime(format_str)} #{name}"
|
389
417
|
end
|
390
418
|
|
419
|
+
# Yield lines of output for the ls command executed on the given file paths
|
420
|
+
# and names.
|
391
421
|
def self.list(state, paths, names, long)
|
392
422
|
if long
|
393
423
|
paths.zip(names).each { |path, name| yield long_info(state, path, name) }
|
@@ -396,6 +426,7 @@ module Commands
|
|
396
426
|
end
|
397
427
|
end
|
398
428
|
|
429
|
+
# Run a command in the system shell and yield lines of output.
|
399
430
|
def self.shell(cmd)
|
400
431
|
IO.popen(cmd) do |pipe|
|
401
432
|
pipe.each_line { |line| yield line.chomp if block_given? }
|
@@ -405,4 +436,54 @@ module Commands
|
|
405
436
|
yield error.to_s if block_given?
|
406
437
|
end
|
407
438
|
|
439
|
+
# Return an +Array+ of paths from an +Array+ of globs, passing error messages
|
440
|
+
# to the output +Proc+ for non-matches.
|
441
|
+
def self.expand(state, paths, preserve_root, output, cmd_name)
|
442
|
+
state.expand_patterns(paths, true).map do |item|
|
443
|
+
if item.is_a?(GlobError)
|
444
|
+
output.call("#{cmd_name}: #{item}: no such file or directory")
|
445
|
+
nil
|
446
|
+
else
|
447
|
+
item
|
448
|
+
end
|
449
|
+
end.compact
|
450
|
+
end
|
451
|
+
|
452
|
+
# Copies or moves the file at +source+ to +dest+ and passes a description of
|
453
|
+
# the operation to the output +Proc+.
|
454
|
+
def self.copy_move(method, source, dest, client, state, output)
|
455
|
+
from_path, to_path = [source, dest].map { |p| state.resolve_path(p) }
|
456
|
+
try_and_handle(DropboxError, output) do
|
457
|
+
metadata = client.send(method, from_path, to_path)
|
458
|
+
state.cache_remove(from_path) if method == :file_move
|
459
|
+
state.cache_add(metadata)
|
460
|
+
output.call("#{source} -> #{dest}")
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
# Execute a 'mv' or 'cp' operation depending on arguments given.
|
465
|
+
def self.cp_mv(client, state, args, output, cmd, method)
|
466
|
+
sources = expand(state, args.take(args.length - 1), true, output, cmd)
|
467
|
+
dest = state.resolve_path(args.last)
|
468
|
+
|
469
|
+
if sources.length == 1 && !state.directory?(dest)
|
470
|
+
copy_move(method, sources[0], args.last, client, state, output)
|
471
|
+
else
|
472
|
+
if state.metadata(dest)
|
473
|
+
sources.each do |source|
|
474
|
+
to_path = args.last.chomp('/') + '/' + File.basename(source)
|
475
|
+
copy_move(method, source, to_path, client, state, output)
|
476
|
+
end
|
477
|
+
else
|
478
|
+
output.call("#{cmd}: #{args.last}: no such directory")
|
479
|
+
end
|
480
|
+
end
|
481
|
+
end
|
482
|
+
|
483
|
+
# If the remote working directory does not exist, move up the directory
|
484
|
+
# tree until at a real location.
|
485
|
+
def self.check_pwd(state)
|
486
|
+
state.pwd = File.dirname(state.pwd) until state.metadata(state.pwd)
|
487
|
+
end
|
488
|
+
|
408
489
|
end
|
data/lib/droxi/complete.rb
CHANGED
@@ -11,7 +11,7 @@ module Complete
|
|
11
11
|
end
|
12
12
|
end
|
13
13
|
|
14
|
-
#
|
14
|
+
# Return an +Array+ of potential local tab-completions for a +String+.
|
15
15
|
def self.local(string)
|
16
16
|
dir = local_search_path(string)
|
17
17
|
name = string.end_with?('/') ? '' : File.basename(string)
|
@@ -24,7 +24,7 @@ module Complete
|
|
24
24
|
end
|
25
25
|
end
|
26
26
|
|
27
|
-
#
|
27
|
+
# Return an +Array+ of potential local tab-completions for a +String+,
|
28
28
|
# including only directories.
|
29
29
|
def self.local_dir(string)
|
30
30
|
local(string).select { |result| result.end_with?('/') }
|
@@ -42,7 +42,7 @@ module Complete
|
|
42
42
|
strip_filename(collapse(path))
|
43
43
|
end
|
44
44
|
|
45
|
-
#
|
45
|
+
# Return an +Array+ of potential remote tab-completions for a +String+.
|
46
46
|
def self.remote(string, state)
|
47
47
|
dir = remote_search_path(string, state)
|
48
48
|
name = string.end_with?('/') ? '' : File.basename(string)
|
@@ -57,7 +57,7 @@ module Complete
|
|
57
57
|
end
|
58
58
|
end
|
59
59
|
|
60
|
-
#
|
60
|
+
# Return an +Array+ of potential remote tab-completions for a +String+,
|
61
61
|
# including only directories.
|
62
62
|
def self.remote_dir(string, state)
|
63
63
|
remote(string, state).select { |result| result.end_with?('/') }
|
@@ -65,6 +65,7 @@ module Complete
|
|
65
65
|
|
66
66
|
private
|
67
67
|
|
68
|
+
# Return the name of the directory indicated by a path.
|
68
69
|
def self.strip_filename(path)
|
69
70
|
if path != '/'
|
70
71
|
path.end_with?('/') ? path.sub(/\/$/, '') : File.dirname(path)
|
@@ -73,10 +74,13 @@ module Complete
|
|
73
74
|
end
|
74
75
|
end
|
75
76
|
|
77
|
+
# Return a version of a path with .. and . resolved to appropriate
|
78
|
+
# directories.
|
76
79
|
def self.collapse(path)
|
77
|
-
|
78
|
-
nil while
|
79
|
-
|
80
|
+
new_path = path.dup
|
81
|
+
nil while new_path.sub!(/[^\/]+\/\.\.\//, '/')
|
82
|
+
nil while new_path.sub!('./', '')
|
83
|
+
new_path
|
80
84
|
end
|
81
85
|
|
82
86
|
end
|
data/lib/droxi/settings.rb
CHANGED
@@ -38,6 +38,7 @@ class Settings
|
|
38
38
|
FileUtils.mkdir_p(File.dirname(CONFIG_FILE_PATH))
|
39
39
|
File.open(CONFIG_FILE_PATH, 'w') do |file|
|
40
40
|
@@settings.each_pair { |k, v| file.write("#{k}=#{v}\n") }
|
41
|
+
file.chmod(0600)
|
41
42
|
end
|
42
43
|
end
|
43
44
|
nil
|
@@ -45,11 +46,15 @@ class Settings
|
|
45
46
|
|
46
47
|
private
|
47
48
|
|
49
|
+
# Print a warning for an invalid setting and return an empty +Hash+ (the
|
50
|
+
# result of an invalid setting).
|
48
51
|
def Settings.warn_invalid(line)
|
49
52
|
warn "invalid setting: #{line}"
|
50
53
|
{}
|
51
54
|
end
|
52
55
|
|
56
|
+
# Parse a line of the rc file and return a +Hash+ containing the resulting
|
57
|
+
# setting data.
|
53
58
|
def Settings.parse(line)
|
54
59
|
if /^(.+?)=(.+)$/ =~ line
|
55
60
|
key, value = $1.to_sym, $2
|
@@ -63,6 +68,7 @@ class Settings
|
|
63
68
|
end
|
64
69
|
end
|
65
70
|
|
71
|
+
# Read and parse the rc file.
|
66
72
|
def Settings.read
|
67
73
|
if File.exists?(CONFIG_FILE_PATH)
|
68
74
|
File.open(CONFIG_FILE_PATH) do |file|
|
data/lib/droxi/state.rb
CHANGED
@@ -33,6 +33,28 @@ class State
|
|
33
33
|
@local_oldpwd = Dir.pwd
|
34
34
|
end
|
35
35
|
|
36
|
+
# Adds a metadata +Hash+ and its contents to the metadata cache.
|
37
|
+
def cache_add(metadata)
|
38
|
+
@cache[metadata['path']] = metadata
|
39
|
+
if metadata.include?('contents')
|
40
|
+
metadata['contents'].each { |content| cache_add(content) }
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
# Removes a path from the metadata cache.
|
45
|
+
def cache_remove(path)
|
46
|
+
if directory?(path) && @cache[path].include?('contents')
|
47
|
+
@cache[path]['contents'].each { |item| cache_remove(item['path']) }
|
48
|
+
end
|
49
|
+
|
50
|
+
@cache.delete(path)
|
51
|
+
|
52
|
+
dir = File.dirname(path)
|
53
|
+
if @cache.include?(dir) && @cache[dir].include?('contents')
|
54
|
+
@cache[dir]['contents'].delete_if { |item| item['path'] == path }
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
36
58
|
# Return a +Hash+ of the Dropbox metadata for a file, or +nil+ if the file
|
37
59
|
# does not exist.
|
38
60
|
def metadata(path, require_contents=true)
|
@@ -42,15 +64,18 @@ class State
|
|
42
64
|
partial_path = '/' + tokens.take(i).join('/')
|
43
65
|
unless have_all_info_for(partial_path, require_contents)
|
44
66
|
begin
|
45
|
-
data = @
|
67
|
+
data = @client.metadata(partial_path)
|
68
|
+
if !data['is_deleted']
|
69
|
+
@cache[partial_path] = data
|
70
|
+
if data.include?('contents')
|
71
|
+
data['contents'].each do |datum|
|
72
|
+
@cache[datum['path']] = datum
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
46
76
|
rescue DropboxError
|
47
77
|
return nil
|
48
78
|
end
|
49
|
-
if data.include?('contents')
|
50
|
-
data['contents'].each do |datum|
|
51
|
-
@cache[datum['path']] = datum
|
52
|
-
end
|
53
|
-
end
|
54
79
|
end
|
55
80
|
end
|
56
81
|
|
@@ -122,6 +147,8 @@ class State
|
|
122
147
|
|
123
148
|
private
|
124
149
|
|
150
|
+
# Return an +Array+ of file paths matching a glob pattern, or a GlobError if
|
151
|
+
# no files were matched.
|
125
152
|
def get_matches(pattern, path, preserve_root)
|
126
153
|
dir = File.dirname(path)
|
127
154
|
matches = contents(dir).select { |entry| File.fnmatch(path, entry) }
|
@@ -135,6 +162,7 @@ class State
|
|
135
162
|
end
|
136
163
|
end
|
137
164
|
|
165
|
+
# Return +true+ if the path's information is cached, +false+ otherwise.
|
138
166
|
def have_all_info_for(path, require_contents=true)
|
139
167
|
@cache.include?(path) && (
|
140
168
|
!require_contents ||
|
data/lib/droxi/text.rb
CHANGED
@@ -33,6 +33,7 @@ module Text
|
|
33
33
|
|
34
34
|
private
|
35
35
|
|
36
|
+
# Return the width of the terminal in columns.
|
36
37
|
def self.get_columns
|
37
38
|
require 'readline'
|
38
39
|
begin
|
@@ -43,6 +44,7 @@ module Text
|
|
43
44
|
end
|
44
45
|
end
|
45
46
|
|
47
|
+
# Return an +Array+ of lines of the given items formatted as a table.
|
46
48
|
def self.format_table(items, item_width, items_per_line, num_lines)
|
47
49
|
num_lines.times.map do |i|
|
48
50
|
items[i * items_per_line, items_per_line].map do |item|
|
@@ -51,6 +53,7 @@ module Text
|
|
51
53
|
end
|
52
54
|
end
|
53
55
|
|
56
|
+
# Return a wrapped line of output from the start of the given text.
|
54
57
|
def self.get_wrap_segment(text, columns)
|
55
58
|
segment, sep, text = text.partition(' ')
|
56
59
|
while !text.empty? && segment.length < columns
|
data/lib/droxi.rb
CHANGED
@@ -28,8 +28,7 @@ module Droxi
|
|
28
28
|
|
29
29
|
private
|
30
30
|
|
31
|
-
# Attempt to authorize the user for app usage.
|
32
|
-
# authorization was successful, +false+ otherwise.
|
31
|
+
# Attempt to authorize the user for app usage.
|
33
32
|
def self.authorize
|
34
33
|
app_key = '5sufyfrvtro9zp7'
|
35
34
|
app_secret = 'h99ihzv86jyypho'
|
@@ -46,8 +45,8 @@ module Droxi
|
|
46
45
|
end
|
47
46
|
end
|
48
47
|
|
49
|
-
#
|
50
|
-
# exists.
|
48
|
+
# Return the access token for the user, requesting authorization if no saved
|
49
|
+
# token exists.
|
51
50
|
def self.get_access_token
|
52
51
|
authorize() until Settings.include?(:access_token)
|
53
52
|
Settings[:access_token]
|
@@ -55,7 +54,7 @@ module Droxi
|
|
55
54
|
|
56
55
|
# Return a prompt message reflecting the current state of the application.
|
57
56
|
def self.prompt(info, state)
|
58
|
-
"
|
57
|
+
"\rdroxi #{info['email']}:#{state.pwd}> "
|
59
58
|
end
|
60
59
|
|
61
60
|
# Run the client in interactive mode.
|
@@ -66,10 +65,11 @@ module Droxi
|
|
66
65
|
init_readline(state)
|
67
66
|
with_interrupt_handling { do_interaction_loop(client, state, info) }
|
68
67
|
|
69
|
-
# Set pwd so that the oldpwd setting is saved to pwd
|
68
|
+
# Set pwd before exiting so that the oldpwd setting is saved to pwd
|
70
69
|
state.pwd = '/'
|
71
70
|
end
|
72
71
|
|
72
|
+
# Set up the Readline library's completion capabilities.
|
73
73
|
def self.init_readline(state)
|
74
74
|
Readline.completion_proc = proc do |word|
|
75
75
|
words = Readline.line_buffer.split
|
@@ -103,12 +103,16 @@ module Droxi
|
|
103
103
|
end
|
104
104
|
end
|
105
105
|
|
106
|
+
# Run the associated block, handling Interrupt errors by printing a blank
|
107
|
+
# line.
|
106
108
|
def self.with_interrupt_handling
|
107
109
|
yield
|
108
110
|
rescue Interrupt
|
109
111
|
puts
|
110
112
|
end
|
111
113
|
|
114
|
+
# Run the main loop of the program, getting user input and executing it as a
|
115
|
+
# command until an getting input fails or an exit is requested.
|
112
116
|
def self.do_interaction_loop(client, state, info)
|
113
117
|
while !state.exit_requested &&
|
114
118
|
line = Readline.readline(prompt(info, state), true)
|
@@ -117,6 +121,8 @@ module Droxi
|
|
117
121
|
puts if !line
|
118
122
|
end
|
119
123
|
|
124
|
+
# Instruct the user to enter an authorization code and return the code. If
|
125
|
+
# the user gives EOF, exit the program.
|
120
126
|
def self.get_auth_code(url)
|
121
127
|
puts '1. Go to: ' + url
|
122
128
|
puts '2. Click "Allow" (you might have to log in first)'
|
data/spec/commands_spec.rb
CHANGED
@@ -90,6 +90,38 @@ describe Commands do
|
|
90
90
|
end
|
91
91
|
end
|
92
92
|
|
93
|
+
describe 'when executing the cp command' do
|
94
|
+
before do
|
95
|
+
state.pwd = '/testing'
|
96
|
+
end
|
97
|
+
|
98
|
+
after do
|
99
|
+
Commands::RM.exec(client, state, '*')
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'must copy source to dest when given 2 args and last arg is non-dir' do
|
103
|
+
Commands::MKDIR.exec(client, state, 'source')
|
104
|
+
Commands::CP.exec(client, state, 'source', 'dest')
|
105
|
+
['source', 'dest'].all? do |dir|
|
106
|
+
client.metadata("/testing/#{dir}")
|
107
|
+
end.must_equal true
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'must copy source into dest when given 2 args and last arg is dir' do
|
111
|
+
Commands::MKDIR.exec(client, state, 'source', 'dest')
|
112
|
+
Commands::CP.exec(client, state, 'source', 'dest')
|
113
|
+
client.metadata('/testing/dest/source').wont_equal nil
|
114
|
+
end
|
115
|
+
|
116
|
+
it 'must copy sources into dest when given 3 or more args' do
|
117
|
+
Commands::MKDIR.exec(client, state, 'source1', 'source2', 'dest')
|
118
|
+
Commands::CP.exec(client, state, 'source1', 'source2', 'dest')
|
119
|
+
['source2', 'source2'].all? do |dir|
|
120
|
+
client.metadata("/testing/dest/#{dir}")
|
121
|
+
end.must_equal true
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
93
125
|
describe 'when executing the forget command' do
|
94
126
|
it 'must clear entire cache when given no arguments' do
|
95
127
|
Commands::LS.exec(client, state, '/')
|
@@ -206,6 +238,39 @@ describe Commands do
|
|
206
238
|
end
|
207
239
|
end
|
208
240
|
|
241
|
+
describe 'when executing the mv command' do
|
242
|
+
before do
|
243
|
+
state.pwd = '/testing'
|
244
|
+
end
|
245
|
+
|
246
|
+
after do
|
247
|
+
Commands::RM.exec(client, state, '*')
|
248
|
+
end
|
249
|
+
|
250
|
+
it 'must move source to dest when given 2 args and last arg is non-dir' do
|
251
|
+
Commands::MKDIR.exec(client, state, 'source')
|
252
|
+
Commands::MV.exec(client, state, 'source', 'dest')
|
253
|
+
client.metadata('/testing/source')['is_deleted'].must_equal true
|
254
|
+
client.metadata('/testing/dest').wont_equal nil
|
255
|
+
end
|
256
|
+
|
257
|
+
it 'must move source into dest when given 2 args and last arg is dir' do
|
258
|
+
Commands::MKDIR.exec(client, state, 'source', 'dest')
|
259
|
+
Commands::MV.exec(client, state, 'source', 'dest')
|
260
|
+
client.metadata('/testing/source')['is_deleted'].must_equal true
|
261
|
+
client.metadata('/testing/dest/source').wont_equal nil
|
262
|
+
end
|
263
|
+
|
264
|
+
it 'must move sources into dest when given 3 or more args' do
|
265
|
+
Commands::MKDIR.exec(client, state, 'source1', 'source2', 'dest')
|
266
|
+
Commands::MV.exec(client, state, 'source1', 'source2', 'dest')
|
267
|
+
['source2', 'source2'].all? do |dir|
|
268
|
+
client.metadata("/testing/#{dir}")['is_deleted'].must_equal true
|
269
|
+
client.metadata("/testing/dest/#{dir}")
|
270
|
+
end.must_equal true
|
271
|
+
end
|
272
|
+
end
|
273
|
+
|
209
274
|
describe 'when executing the put command' do
|
210
275
|
it 'must put a file of the same name when given 1 arg' do
|
211
276
|
state.pwd = '/testing'
|
@@ -239,10 +304,17 @@ describe Commands do
|
|
239
304
|
|
240
305
|
describe 'when executing the rm command' do
|
241
306
|
it 'must remove the remote file when given args' do
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
307
|
+
Commands::MKDIR.exec(client, state, '/testing/test')
|
308
|
+
Commands::RM.exec(client, state, '/testing/test')
|
309
|
+
client.metadata('/testing/test')['is_deleted'].must_equal true
|
310
|
+
end
|
311
|
+
|
312
|
+
it 'must change pwd to existing dir if the current one is removed' do
|
313
|
+
Commands::MKDIR.exec(client, state, '/testing/one')
|
314
|
+
Commands::MKDIR.exec(client, state, '/testing/one/two')
|
315
|
+
Commands::CD.exec(client, state, '/testing/one/two')
|
316
|
+
Commands::RM.exec(client, state, '..')
|
317
|
+
state.pwd.must_equal('/testing')
|
246
318
|
end
|
247
319
|
end
|
248
320
|
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.5
|
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-04 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dropbox-sdk
|