droxi 0.0.4 → 0.0.5
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.
- 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
|