droxi 0.0.5 → 0.1.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ed8c5f6d9c0df8aff83fc755d298fda9dda25adf
4
- data.tar.gz: f882a46c0a98a0f4f69cb88f3610fc403abc23e3
3
+ metadata.gz: 244170f34242a9088ac104e483683219845ac012
4
+ data.tar.gz: eb1dab3e763e6749171ae587ef288dd466d74700
5
5
  SHA512:
6
- metadata.gz: c5cbb62624375b1515ff9e7de15167ead211bb535dd427a87954c3a585865e5d25ba90236a0f717c4a491b307d6bbb01fdd430dbee3c4edfaf43e8c49a30b3da
7
- data.tar.gz: 4c9b4bc88008c441cb8f7284be5c90470b147233f134dceb035e9dc681e3ce303168220ecfdea5deb27f2bac94fad43109aeb52b3da18d23bafa730ba8b5580d
6
+ metadata.gz: 92865a868baf0e3efbbf0bf57cb844c12f89c58f500aa2151eea276d3cf3cd899aade5c80e4c10468e6e6c8bc542141bba88bea7786efcf2e924643ac944f426
7
+ data.tar.gz: eeda04ec25143e42c8e185aa2e88b71aee7d7be4facbe49e5bd3b2f2c80256a5e44e82e01d1963e05566498fe73368ea9ed8f09f3b71cb737c722a569cdbc1e2
data/README.md CHANGED
@@ -28,5 +28,13 @@ features
28
28
  [GNU ftp](http://www.gnu.org/software/inetutils/), and
29
29
  [lftp](http://lftp.yar.ru/)
30
30
  - context-sensitive tab completion and path globbing
31
- - upload, download, and share files
31
+ - upload, download, organize, and share files
32
32
  - man page and interactive help
33
+
34
+ developer features
35
+ ------------------
36
+
37
+ - extensive spec-style unit tests using
38
+ [MiniTest](https://github.com/seattlerb/minitest)
39
+ - [RuboCop](https://github.com/bbatsov/rubocop)-approved
40
+ - fully [RDoc](http://rdoc.sourceforge.net/) documented
data/Rakefile CHANGED
@@ -5,10 +5,24 @@ task :test do
5
5
  sh 'ruby spec/all.rb'
6
6
  end
7
7
 
8
+ desc 'run unit tests in verbose mode'
9
+ task :verbose_test do
10
+ sh 'ruby -w spec/all.rb'
11
+ end
12
+
13
+ desc 'check code with rubocop'
14
+ task :cop do
15
+ sh 'rubocop bin lib spec'
16
+ end
17
+
8
18
  desc 'run program'
9
19
  task :run do
10
- require_relative 'lib/droxi'
11
- Droxi.run
20
+ sh 'ruby bin/droxi'
21
+ end
22
+
23
+ desc 'run program in debug mode'
24
+ task :debug do
25
+ sh 'ruby bin/droxi --debug'
12
26
  end
13
27
 
14
28
  desc 'install gem'
@@ -30,8 +44,7 @@ task :build do
30
44
 
31
45
  contents = "#!/usr/bin/env ruby\n\n"
32
46
  contents << `cat -s #{filenames.join(' ')} \
33
- | grep -v require_relative \
34
- | grep -v "require 'droxi'"`
47
+ | grep -v require_relative`
35
48
 
36
49
  IO.write('build/droxi', contents)
37
50
  File.chmod(0755, 'build/droxi')
@@ -96,5 +109,5 @@ end
96
109
 
97
110
  desc 'remove files generated by other targets'
98
111
  task :clean do
99
- sh 'rm -rf build doc droxi-*.gem'
112
+ sh 'rm -rf build coverage doc droxi-*.gem'
100
113
  end
data/bin/droxi CHANGED
@@ -1,4 +1,3 @@
1
1
  #!/usr/bin/env ruby
2
-
3
- require 'droxi'
4
- Droxi.run(*ARGV)
2
+ require_relative '../lib/droxi'
3
+ Droxi.run(*(ARGV.reject { |arg| arg == '--debug' }))
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.5'
4
- s.date = '2014-06-04'
3
+ s.version = '0.1.0'
4
+ s.date = '2014-06-05'
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.rb CHANGED
@@ -8,18 +8,16 @@ require_relative 'droxi/state'
8
8
 
9
9
  # Command-line Dropbox client module.
10
10
  module Droxi
11
-
12
11
  # Run the client.
13
12
  def self.run(*args)
14
- client = DropboxClient.new(get_access_token)
13
+ client = DropboxClient.new(access_token)
15
14
  state = State.new(client)
16
15
 
17
16
  if args.empty?
18
17
  run_interactive(client, state)
19
18
  else
20
19
  with_interrupt_handling do
21
- cmd = args.map { |arg| arg.gsub(' ', '\ ') }.join(' ')
22
- Commands.exec(cmd, client, state)
20
+ Commands.exec(join_cmd(args), client, state)
23
21
  end
24
22
  end
25
23
 
@@ -28,6 +26,12 @@ module Droxi
28
26
 
29
27
  private
30
28
 
29
+ # Return a +String+ of joined command-line args, adding backslash escapes for
30
+ # spaces.
31
+ def self.join_cmd(args)
32
+ args.map { |arg| arg.gsub(' ', '\ ') }.join(' ')
33
+ end
34
+
31
35
  # Attempt to authorize the user for app usage.
32
36
  def self.authorize
33
37
  app_key = '5sufyfrvtro9zp7'
@@ -35,7 +39,7 @@ module Droxi
35
39
 
36
40
  flow = DropboxOAuth2FlowNoRedirect.new(app_key, app_secret)
37
41
 
38
- authorize_url = flow.start()
42
+ authorize_url = flow.start
39
43
  code = get_auth_code(authorize_url)
40
44
 
41
45
  begin
@@ -47,8 +51,8 @@ module Droxi
47
51
 
48
52
  # Return the access token for the user, requesting authorization if no saved
49
53
  # token exists.
50
- def self.get_access_token
51
- authorize() until Settings.include?(:access_token)
54
+ def self.access_token
55
+ authorize until Settings.include?(:access_token)
52
56
  Settings[:access_token]
53
57
  end
54
58
 
@@ -69,37 +73,45 @@ module Droxi
69
73
  state.pwd = '/'
70
74
  end
71
75
 
76
+ # Return an +Array+ of potential tab-completion options for a given
77
+ # completion type, word, and client state.
78
+ def self.completion_options(type, word, state)
79
+ case type
80
+ when 'COMMAND' then Complete.command(word, Commands::NAMES)
81
+ when 'LOCAL_FILE' then Complete.local(word)
82
+ when 'LOCAL_DIR' then Complete.local_dir(word)
83
+ when 'REMOTE_FILE' then Complete.remote(word, state)
84
+ when 'REMOTE_DIR' then Complete.remote_dir(word, state)
85
+ else []
86
+ end
87
+ end
88
+
89
+ # Return a +String+ representing the type of tab-completion that should be
90
+ # performed, given the current line buffer state.
91
+ def self.completion_type
92
+ words = Readline.line_buffer.split
93
+ index = words.length
94
+ index += 1 if Readline.line_buffer.end_with?(' ')
95
+ if index <= 1
96
+ 'COMMAND'
97
+ elsif Commands::NAMES.include?(words[0])
98
+ cmd = Commands.const_get(words[0].upcase.to_sym)
99
+ cmd.type_of_arg(index - 2)
100
+ end
101
+ end
102
+
72
103
  # Set up the Readline library's completion capabilities.
73
104
  def self.init_readline(state)
74
105
  Readline.completion_proc = proc do |word|
75
- words = Readline.line_buffer.split
76
- index = words.length
77
- index += 1 if Readline.line_buffer.end_with?(' ')
78
- if index <= 1
79
- type = 'COMMAND'
80
- elsif Commands::NAMES.include?(words[0])
81
- cmd = Commands.const_get(words[0].upcase.to_sym)
82
- type = cmd.type_of_arg(index - 2)
83
- end
84
-
85
- options = case type
86
- when 'COMMAND'
87
- Commands::NAMES.select { |name| name.start_with? word }.map do |name|
88
- name + ' '
89
- end
90
- when 'LOCAL_FILE' then Complete.local(word)
91
- when 'LOCAL_DIR' then Complete.local_dir(word)
92
- when 'REMOTE_FILE' then Complete.remote(word, state)
93
- when 'REMOTE_DIR' then Complete.remote_dir(word, state)
94
- else []
106
+ completion_options(completion_type, word, state).map do |option|
107
+ option.gsub(' ', '\ ').sub(/\\ $/, ' ')
95
108
  end
96
-
97
- options.map { |option| option.gsub(' ', '\ ').sub(/\\ $/, ' ') }
98
109
  end
99
110
 
100
111
  begin
101
112
  Readline.completion_append_character = nil
102
113
  rescue NotImplementedError
114
+ nil
103
115
  end
104
116
  end
105
117
 
@@ -114,11 +126,12 @@ module Droxi
114
126
  # Run the main loop of the program, getting user input and executing it as a
115
127
  # command until an getting input fails or an exit is requested.
116
128
  def self.do_interaction_loop(client, state, info)
117
- while !state.exit_requested &&
118
- line = Readline.readline(prompt(info, state), true)
129
+ until state.exit_requested
130
+ line = Readline.readline(prompt(info, state), true)
131
+ break unless line
119
132
  with_interrupt_handling { Commands.exec(line.chomp, client, state) }
120
133
  end
121
- puts if !line
134
+ puts unless line
122
135
  end
123
136
 
124
137
  # Instruct the user to enter an authorization code and return the code. If
@@ -131,5 +144,4 @@ module Droxi
131
144
  code = $stdin.gets
132
145
  code ? code.strip! : exit
133
146
  end
134
-
135
147
  end
@@ -4,7 +4,6 @@ require_relative 'text'
4
4
 
5
5
  # Module containing definitions for client commands.
6
6
  module Commands
7
-
8
7
  # Exception indicating that a client command was given the wrong number of
9
8
  # arguments.
10
9
  class UsageError < ArgumentError
@@ -12,7 +11,6 @@ module Commands
12
11
 
13
12
  # A client command. Contains metadata as well as execution procedure.
14
13
  class Command
15
-
16
14
  # A +String+ specifying the usage of the command in the style of a man page
17
15
  # synopsis. Optional arguments are enclosed in brackets; varargs-style
18
16
  # arguments are suffixed with an ellipsis.
@@ -36,12 +34,9 @@ module Commands
36
34
  # given. Raises a +UsageError+ if an invalid number of command-line
37
35
  # arguments is given.
38
36
  def exec(client, state, *args)
39
- if num_args_ok?(args.length)
40
- block = proc { |line| yield line if block_given? }
41
- @procedure.yield(client, state, args, block)
42
- else
43
- fail UsageError, @usage
44
- end
37
+ fail UsageError, @usage unless num_args_ok?(args.length)
38
+ block = proc { |line| yield line if block_given? }
39
+ @procedure.yield(client, state, args, block)
45
40
  end
46
41
 
47
42
  # Return a +String+ describing the type of argument at the given index.
@@ -49,12 +44,9 @@ module Commands
49
44
  # the +Command+ takes no arguments, return +nil+.
50
45
  def type_of_arg(index)
51
46
  args = @usage.split.drop(1).reject { |arg| arg.include?('-') }
52
- if args.empty?
53
- nil
54
- else
55
- index = [index, args.length - 1].min
56
- args[index].tr('[].', '')
57
- end
47
+ return nil if args.empty?
48
+ index = [index, args.length - 1].min
49
+ args[index].tr('[].', '')
58
50
  end
59
51
 
60
52
  private
@@ -64,13 +56,11 @@ module Commands
64
56
  def num_args_ok?(num_args)
65
57
  args = @usage.split.drop(1)
66
58
  min_args = args.reject { |arg| arg.start_with?('[') }.length
67
- if args.empty?
68
- max_args = 0
69
- elsif args.any? { |arg| arg.end_with?('...') }
70
- max_args = num_args
71
- else
72
- max_args = args.length
73
- end
59
+ max_args = if args.any? { |arg| arg.end_with?('...') }
60
+ num_args
61
+ else
62
+ args.length
63
+ end
74
64
  (min_args..max_args).include?(num_args)
75
65
  end
76
66
  end
@@ -82,17 +72,16 @@ module Commands
82
72
  Dropbox root. With a remote directory name as the argument, changes to \
83
73
  that directory. With - as the argument, changes to the previous working \
84
74
  directory.",
85
- lambda do |client, state, args, output|
86
- if args.empty?
87
- state.pwd = '/'
88
- elsif args[0] == '-'
89
- state.pwd = state.oldpwd
75
+ lambda do |_client, state, args, output|
76
+ case
77
+ when args.empty? then state.pwd = '/'
78
+ when args[0] == '-' then state.pwd = state.oldpwd
90
79
  else
91
80
  path = state.resolve_path(args[0])
92
81
  if state.directory?(path)
93
82
  state.pwd = path
94
83
  else
95
- output.call('Not a directory')
84
+ output.call("cd: #{args[0]}: no such directory")
96
85
  end
97
86
  end
98
87
  end
@@ -106,15 +95,35 @@ module Commands
106
95
  final argument is a directory, copies each remote file or folder into \
107
96
  that directory.",
108
97
  lambda do |client, state, args, output|
109
- cp_mv(client, state, args, output, 'cp', :file_copy)
98
+ cp_mv(client, state, args, output, 'cp')
99
+ end
100
+ )
101
+
102
+ # Execute arbitrary code.
103
+ DEBUG = Command.new(
104
+ 'debug STRING...',
105
+ "Evaluates the given string as Ruby code and prints the result. Won't \
106
+ work unless the program was invoked with the --debug flag.",
107
+ # rubocop:disable Lint/UnusedBlockArgument, Lint/Eval
108
+ lambda do |client, state, args, output|
109
+ if ARGV.include?('--debug')
110
+ begin
111
+ output.call(eval(args.join(' ')).inspect)
112
+ # rubocop:enable Lint/UnusedBlockArgument, Lint/Eval
113
+ rescue => error
114
+ output.call(error.inspect)
115
+ end
116
+ else
117
+ output.call('Debug not enabled.')
118
+ end
110
119
  end
111
120
  )
112
121
 
113
122
  # Terminate the session.
114
123
  EXIT = Command.new(
115
124
  'exit',
116
- "Exit the program.",
117
- lambda do |client, state, args, output|
125
+ 'Exit the program.',
126
+ lambda do |_client, state, _args, _output|
118
127
  state.exit_requested = true
119
128
  end
120
129
  )
@@ -125,7 +134,7 @@ module Commands
125
134
  "Clear the client-side cache of remote filesystem metadata. With no \
126
135
  arguments, clear the entire cache. If given directories as arguments, \
127
136
  (recursively) clear the cache of those directories only.",
128
- lambda do |client, state, args, output|
137
+ lambda do |_client, state, args, output|
129
138
  if args.empty?
130
139
  state.cache.clear
131
140
  else
@@ -144,14 +153,13 @@ module Commands
144
153
  lambda do |client, state, args, output|
145
154
  state.expand_patterns(args).each do |path|
146
155
  if path.is_a?(GlobError)
147
- output.call("get: #{path}: No such file or directory")
156
+ output.call("get: #{path}: no such file or directory")
148
157
  else
149
158
  try_and_handle(DropboxError, output) do
150
159
  contents = client.get_file(path)
151
- File.open(File.basename(path), 'wb') do |file|
152
- file.write(contents)
153
- end
154
- output.call("#{File.basename(path)} <- #{path}")
160
+ basename = File.basename(path)
161
+ File.open(basename, 'wb') { |file| file.write(contents) }
162
+ output.call("#{basename} <- #{path}")
155
163
  end
156
164
  end
157
165
  end
@@ -163,7 +171,7 @@ module Commands
163
171
  'help [COMMAND]',
164
172
  "Print usage and help information about a command. If no command is \
165
173
  given, print a list of commands instead.",
166
- lambda do |client, state, args, output|
174
+ lambda do |_client, _state, args, output|
167
175
  if args.empty?
168
176
  Text.table(NAMES).each { |line| output.call(line) }
169
177
  else
@@ -186,20 +194,23 @@ module Commands
186
194
  home directory. With a local directory name as the argument, changes to \
187
195
  that directory. With - as the argument, changes to the previous working \
188
196
  directory.",
189
- lambda do |client, state, args, output|
190
- path = if args.empty?
191
- File.expand_path('~')
192
- elsif args[0] == '-'
193
- state.local_oldpwd
194
- else
195
- File.expand_path(args[0])
196
- end
197
-
198
- if Dir.exists?(path)
197
+ lambda do |_client, state, args, output|
198
+ path = case
199
+ when args.empty? then File.expand_path('~')
200
+ when args[0] == '-' then state.local_oldpwd
201
+ else
202
+ begin
203
+ File.expand_path(args[0])
204
+ rescue ArgumentError
205
+ args[0]
206
+ end
207
+ end
208
+
209
+ if Dir.exist?(path)
199
210
  state.local_oldpwd = Dir.pwd
200
211
  Dir.chdir(path)
201
212
  else
202
- output.call("lcd: #{args[0]}: No such file or directory")
213
+ output.call("lcd: #{args[0]}: no such file or directory")
203
214
  end
204
215
  end
205
216
  )
@@ -212,13 +223,13 @@ module Commands
212
223
  arguments, list the contents of the directories. When given remote files \
213
224
  as arguments, list the files. If the -l option is given, display \
214
225
  information about the files.",
215
- lambda do |client, state, args, output|
216
- long = args.delete('-l') != nil
226
+ lambda do |_client, state, args, output|
227
+ long = args.delete('-l')
217
228
 
218
229
  files, dirs = [], []
219
230
  state.expand_patterns(args, true).each do |path|
220
231
  if path.is_a?(GlobError)
221
- output.call("ls: #{path}: No such file or directory")
232
+ output.call("ls: #{path}: no such file or directory")
222
233
  else
223
234
  type = state.directory?(path) ? dirs : files
224
235
  type << path
@@ -229,7 +240,7 @@ module Commands
229
240
 
230
241
  # First list files
231
242
  list(state, files, files, long) { |line| output.call(line) }
232
- output.call('') if !(dirs.empty? || files.empty?)
243
+ output.call('') unless dirs.empty? || files.empty?
233
244
 
234
245
  # Then list directory contents
235
246
  dirs.each_with_index do |dir, i|
@@ -250,7 +261,7 @@ module Commands
250
261
  lambda do |client, state, args, output|
251
262
  state.expand_patterns(args).each do |path|
252
263
  if path.is_a?(GlobError)
253
- output.call("media: #{path}: No such file or directory")
264
+ output.call("media: #{path}: no such file or directory")
254
265
  else
255
266
  try_and_handle(DropboxError, output) do
256
267
  url = client.media(path)['url']
@@ -264,12 +275,13 @@ module Commands
264
275
  # Create a remote directory.
265
276
  MKDIR = Command.new(
266
277
  'mkdir REMOTE_DIR...',
267
- "Create remote directories.",
278
+ 'Create remote directories.',
268
279
  lambda do |client, state, args, output|
269
280
  args.each do |arg|
270
281
  try_and_handle(DropboxError, output) do
271
282
  path = state.resolve_path(arg)
272
- state.cache[path] = client.file_create_folder(path)
283
+ metadata = client.file_create_folder(path)
284
+ state.cache.add(metadata)
273
285
  end
274
286
  end
275
287
  end
@@ -283,7 +295,7 @@ module Commands
283
295
  final argument is a directory, moves each remote file or folder into \
284
296
  that directory.",
285
297
  lambda do |client, state, args, output|
286
- cp_mv(client, state, args, output, 'mv', :file_move)
298
+ cp_mv(client, state, args, output, 'mv')
287
299
  end
288
300
  )
289
301
 
@@ -296,17 +308,13 @@ module Commands
296
308
  remote working directory.",
297
309
  lambda do |client, state, args, output|
298
310
  from_path = args[0]
299
- if args.length == 2
300
- to_path = args[1]
301
- else
302
- to_path = File.basename(from_path)
303
- end
311
+ to_path = (args.length == 2) ? args[1] : File.basename(from_path)
304
312
  to_path = state.resolve_path(to_path)
305
313
 
306
- try_and_handle(Exception, output) do
314
+ try_and_handle(Exception, output) do
307
315
  File.open(File.expand_path(from_path), 'rb') do |file|
308
316
  data = client.put_file(to_path, file)
309
- state.cache[data['path']] = data
317
+ state.cache.add(data)
310
318
  output.call("#{from_path} -> #{data['path']}")
311
319
  end
312
320
  end
@@ -316,15 +324,15 @@ module Commands
316
324
  # Remove remote files.
317
325
  RM = Command.new(
318
326
  'rm REMOTE_FILE...',
319
- "Remove each specified remote file or directory.",
327
+ 'Remove each specified remote file or directory.',
320
328
  lambda do |client, state, args, output|
321
329
  state.expand_patterns(args).each do |path|
322
330
  if path.is_a?(GlobError)
323
- output.call("rm: #{path}: No such file or directory")
331
+ output.call("rm: #{path}: no such file or directory")
324
332
  else
325
333
  try_and_handle(DropboxError, output) do
326
334
  client.file_delete(path)
327
- state.cache_remove(path)
335
+ state.cache.remove(path)
328
336
  end
329
337
  end
330
338
  end
@@ -342,7 +350,7 @@ module Commands
342
350
  lambda do |client, state, args, output|
343
351
  state.expand_patterns(args).each do |path|
344
352
  if path.is_a?(GlobError)
345
- output.call("share: #{path}: No such file or directory")
353
+ output.call("share: #{path}: no such file or directory")
346
354
  else
347
355
  try_and_handle(DropboxError, output) do
348
356
  url = client.shares(path)['url']
@@ -353,16 +361,20 @@ module Commands
353
361
  end
354
362
  )
355
363
 
364
+ # Return an +Array+ of all command names.
365
+ def self.names
366
+ symbols = constants.select { |sym| const_get(sym).is_a?(Command) }
367
+ symbols.map { |sym| sym.to_s.downcase }
368
+ end
369
+
356
370
  # +Array+ of all command names.
357
- NAMES = constants.select do |sym|
358
- const_get(sym).is_a?(Command)
359
- end.map { |sym| sym.to_s.downcase }
371
+ NAMES = names
360
372
 
361
373
  # Parse and execute a line of user input in the given context.
362
374
  def self.exec(input, client, state)
363
375
  if input.start_with?('!')
364
376
  shell(input[1, input.length - 1]) { |line| puts line }
365
- elsif not input.empty?
377
+ elsif !input.empty?
366
378
  tokens = tokenize(input)
367
379
  cmd, args = tokens[0], tokens.drop(1)
368
380
  try_command(cmd, args, client, state)
@@ -399,10 +411,10 @@ module Commands
399
411
  def self.tokenize(string)
400
412
  string.split.reduce([]) do |list, token|
401
413
  list << if !list.empty? && list.last.end_with?('\\')
402
- "#{list.pop.chop} #{token}"
403
- else
404
- token
405
- end
414
+ "#{list.pop.chop} #{token}"
415
+ else
416
+ token
417
+ end
406
418
  end
407
419
  end
408
420
 
@@ -432,16 +444,17 @@ module Commands
432
444
  pipe.each_line { |line| yield line.chomp if block_given? }
433
445
  end
434
446
  rescue Interrupt
435
- rescue Exception => error
447
+ yield ''
448
+ rescue Errno::ENOENT => error
436
449
  yield error.to_s if block_given?
437
450
  end
438
451
 
439
452
  # Return an +Array+ of paths from an +Array+ of globs, passing error messages
440
453
  # 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|
454
+ def self.expand(state, paths, preserve_root, output, cmd)
455
+ state.expand_patterns(paths, preserve_root).map do |item|
443
456
  if item.is_a?(GlobError)
444
- output.call("#{cmd_name}: #{item}: no such file or directory")
457
+ output.call("#{cmd}: #{item}: no such file or directory") if output
445
458
  nil
446
459
  else
447
460
  item
@@ -449,41 +462,48 @@ module Commands
449
462
  end.compact
450
463
  end
451
464
 
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) }
465
+ # Copies or moves a file and passes a description of the operation to the
466
+ # output +Proc+.
467
+ def self.copy_move(method, args, client, state, output)
468
+ from_path, to_path = args.map { |p| state.resolve_path(p) }
456
469
  try_and_handle(DropboxError, output) do
457
470
  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}")
471
+ state.cache.remove(from_path) if method == :file_move
472
+ state.cache.add(metadata)
473
+ output.call("#{args[0]} -> #{args[1]}")
461
474
  end
462
475
  end
463
476
 
464
477
  # Execute a 'mv' or 'cp' operation depending on arguments given.
465
- def self.cp_mv(client, state, args, output, cmd, method)
478
+ def self.cp_mv(client, state, args, output, cmd)
466
479
  sources = expand(state, args.take(args.length - 1), true, output, cmd)
480
+ method = (cmd == 'cp') ? :file_copy : :file_move
467
481
  dest = state.resolve_path(args.last)
468
482
 
469
483
  if sources.length == 1 && !state.directory?(dest)
470
- copy_move(method, sources[0], args.last, client, state, output)
484
+ copy_move(method, [sources[0], args.last], client, state, output)
471
485
  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")
486
+ cp_mv_to_dir(args, client, state, cmd, output)
487
+ end
488
+ end
489
+
490
+ # Copies or moves files into a directory.
491
+ def self.cp_mv_to_dir(args, client, state, cmd, output)
492
+ sources = expand(state, args.take(args.length - 1), true, nil, cmd)
493
+ method = (cmd == 'cp') ? :file_copy : :file_move
494
+ if state.metadata(state.resolve_path(args.last))
495
+ sources.each do |source|
496
+ to_path = args.last.chomp('/') + '/' + File.basename(source)
497
+ copy_move(method, [source, to_path], client, state, output)
479
498
  end
499
+ else
500
+ output.call("#{cmd}: #{args.last}: no such directory")
480
501
  end
481
502
  end
482
503
 
483
504
  # If the remote working directory does not exist, move up the directory
484
505
  # tree until at a real location.
485
506
  def self.check_pwd(state)
486
- state.pwd = File.dirname(state.pwd) until state.metadata(state.pwd)
507
+ (state.pwd = File.dirname(state.pwd)) until state.metadata(state.pwd)
487
508
  end
488
-
489
509
  end