droxi 0.0.5 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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