droxi 0.2.3 → 0.3.1

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: 41024246faf2e570c6f96a0c2cfa77af7a871cbb
4
- data.tar.gz: 9a516974414a11c3427e81590aa77d92a43e68d5
3
+ metadata.gz: 1c51dbec9e627b8b8b2e07e4ac8bf4464bee33dd
4
+ data.tar.gz: 480c18b2b62a8ec434cd6c8a8a481a40976f884b
5
5
  SHA512:
6
- metadata.gz: 13d4714c04d56e274c6dc50a7612bb51714c956503faf91a2b83b68d054221ffbb576455d44ad7568ee2a44998bb4f724cbed135108740e303d113ae2490a334
7
- data.tar.gz: a91221075eb67995cb8a38578a4a030531e7028e86ed1346fae2570d28c56cb0b4e153e9cf51c8592c66408dd038bd2963213445bddfd4089d1fd824972a52de
6
+ metadata.gz: 5af1c7d1ab268567fa44508dad484090c6da945f1861e022c025e34dff7527525d81af89d03812b613c1d8aa008e9e33e806656d47832f95fa9ae37654b16655
7
+ data.tar.gz: bd890a7e5aa0c14d912181f7d80643a46c1d41dbf914b4556919f660fa1e67edec98d8e18f48c25d01b9b6d71c4555e3c4e34a18553a6a613838410fba431906
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2014 Brandon Mulcahy
1
+ Copyright (c) 2014-2015 Brandon Mulcahy
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining a copy
4
4
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -1,41 +1,43 @@
1
- droxi
1
+ Droxi
2
2
  =====
3
+ An `ftp`-like command-line [Dropbox](https://www.dropbox.com/) interface in
4
+ [Ruby](https://www.ruby-lang.org/en/).
3
5
 
4
- ftp-like command-line [Dropbox](https://www.dropbox.com/) interface in
5
- [Ruby](https://www.ruby-lang.org/en/)
6
-
7
- installation
6
+ Installation
8
7
  ------------
8
+ Installation as Ruby gem:
9
9
 
10
10
  gem install droxi
11
11
 
12
- or
12
+ Manual installation:
13
13
 
14
14
  git clone https://github.com/jangler/droxi.git
15
15
  cd droxi && rake && sudo rake install
16
16
 
17
- or
18
-
19
- wget https://aur.archlinux.org/packages/dr/droxi/droxi.tar.gz
20
- tar -xzf droxi.tar.gz
21
- cd droxi && makepkg -s && sudo pacman -U droxi-*.pkg.tar.xz
17
+ If you use Arch Linux or a derivative, you may also install via the
18
+ [AUR package](https://aur.archlinux.org/packages/droxi/).
22
19
 
23
- features
20
+ Features
24
21
  --------
25
-
26
- - interface inspired by
22
+ - Interface based on
27
23
  [GNU coreutils](http://www.gnu.org/software/coreutils/),
28
24
  [GNU ftp](http://www.gnu.org/software/inetutils/), and
29
25
  [lftp](http://lftp.yar.ru/)
30
- - context-sensitive tab completion and path globbing
31
- - upload, download, organize, search, and share files
32
- - file revision control
33
- - man page and interactive help
34
-
35
- developer features
36
- ------------------
37
-
38
- - extensive spec-style unit tests using
39
- [MiniTest](https://github.com/seattlerb/minitest)
40
- - [RuboCop](https://github.com/bbatsov/rubocop)-approved
41
- - fully [RDoc](http://rdoc.sourceforge.net/) documented
26
+ - Context-sensitive tab completion and path globbing
27
+ - Upload, download, organize, search, and share files
28
+ - File revision control
29
+ - Man page and interactive help
30
+
31
+ Examples
32
+ --------
33
+ Start interactive session:
34
+
35
+ droxi
36
+
37
+ Invoke single command and exit:
38
+
39
+ droxi share Photos/pic.jpg
40
+
41
+ Scripting:
42
+
43
+ echo -e "cd Photos \n put -f *jpg" | droxi
data/droxi.1.template CHANGED
@@ -18,7 +18,7 @@ Pass a string to be executed by the local shell.
18
18
  --debug
19
19
  Enable the 'debug' command for the session.
20
20
  .TP
21
- --help
21
+ -h, --help
22
22
  Print help information and exit.
23
23
  .TP
24
24
  --version
data/droxi.gemspec CHANGED
@@ -1,11 +1,11 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'droxi'
3
3
  s.version = IO.read('lib/droxi.rb')[/VERSION = '(.+)'/, 1]
4
- s.date = '2014-12-17'
5
- s.summary = 'ftp-like command-line interface to Dropbox'
6
- s.description = "A command-line Dropbox interface inspired by GNU \
7
- coreutils, GNU ftp, and lftp. Features include smart tab \
8
- completion, globbing, and interactive help.".squeeze(' ')
4
+ s.date = '2015-05-09'
5
+ s.summary = 'An ftp-like command-line interface to Dropbox'
6
+ s.description = "A command-line Dropbox interface based on GNU coreutils, \
7
+ GNU ftp, and lftp. Features include smart tab completion, \
8
+ globbing, and interactive help.".squeeze(' ')
9
9
  s.authors = ['Brandon Mulcahy']
10
10
  s.email = 'brandon@jangler.info'
11
11
  s.files = `git ls-files`.split
data/lib/droxi/cache.rb CHANGED
@@ -3,7 +3,7 @@ class Cache < Hash
3
3
  # Add a metadata +Hash+ and its contents to the +Cache+ and return the
4
4
  # +Cache+.
5
5
  def add(metadata)
6
- path = metadata['path']
6
+ path = metadata['path'].downcase
7
7
  store(path, metadata)
8
8
  dirname = File.dirname(path)
9
9
  if dirname != path
@@ -17,14 +17,16 @@ class Cache < Hash
17
17
 
18
18
  # Remove a path's metadata from the +Cache+ and return the +Cache+.
19
19
  def remove(path)
20
+ path = path.downcase
20
21
  recursive_remove(path)
21
22
  contents = fetch(File.dirname(path), {}).fetch('contents', nil)
22
- contents.delete_if { |item| item['path'] == path } if contents
23
+ contents.delete_if { |item| item['path'].downcase == path } if contents
23
24
  self
24
25
  end
25
26
 
26
27
  # Return +true+ if the path's information is cached, +false+ otherwise.
27
28
  def full_info?(path, require_contents = true)
29
+ path = path.downcase
28
30
  info = fetch(path, nil)
29
31
  info && (!require_contents || !info['is_dir'] || info.include?('contents'))
30
32
  end
@@ -33,6 +35,7 @@ class Cache < Hash
33
35
 
34
36
  # Recursively remove a path and its sub-files and directories.
35
37
  def recursive_remove(path)
38
+ path = path.downcase
36
39
  contents = fetch(path, {}).fetch('contents', nil)
37
40
  contents.each { |item| recursive_remove(item['path']) } if contents
38
41
  delete(path)
@@ -165,12 +165,13 @@ module Commands
165
165
 
166
166
  # Download remote files.
167
167
  GET = Command.new(
168
- 'get [-f] REMOTE_FILE...',
168
+ 'get [-E] [-f] REMOTE_FILE...',
169
169
  "Download each specified remote file to a file of the same name in the \
170
170
  local working directory. Will refuse to overwrite existing files unless \
171
- invoked with the -f option.",
171
+ invoked with the -f option. If the -E option is given, remove each \
172
+ remote file after successful download.",
172
173
  lambda do |client, state, args|
173
- flags = extract_flags(GET.usage, args, '-f' => 0)
174
+ flags = extract_flags(GET.usage, args, '-E' => 0, '-f' => 0)
174
175
 
175
176
  state.expand_patterns(args).each do |path|
176
177
  if path.is_a?(GlobError)
@@ -181,6 +182,10 @@ module Commands
181
182
  if flags.include?('-f') || !File.exist?(basename)
182
183
  contents = client.get_file(path)
183
184
  IO.write(basename, contents, mode: 'wb')
185
+ if flags.include?('-E')
186
+ client.file_delete(path)
187
+ state.cache.remove(path)
188
+ end
184
189
  puts "#{basename} <- #{path}"
185
190
  else
186
191
  warn "get: #{basename}: local file already exists"
@@ -199,6 +204,7 @@ module Commands
199
204
  lambda do |_client, _state, args|
200
205
  extract_flags(HELP.usage, args, {})
201
206
  if args.empty?
207
+ puts 'Type "help <command>" for more info about a command:'
202
208
  Text.table(NAMES).each { |line| puts line }
203
209
  else
204
210
  cmd_name = args.first
@@ -263,7 +269,7 @@ module Commands
263
269
  state.local_oldpwd = Dir.pwd
264
270
  Dir.chdir(path)
265
271
  else
266
- warn "lcd: #{args.first}: no such file or directory"
272
+ warn "lcd: #{args.first}: no such directory"
267
273
  end
268
274
  end
269
275
  )
@@ -357,17 +363,18 @@ module Commands
357
363
 
358
364
  # Upload a local file.
359
365
  PUT = Command.new(
360
- 'put [-f] [-q] [-O REMOTE_DIR] [-t COUNT] LOCAL_FILE...',
366
+ 'put [-E] [-f] [-q] [-O REMOTE_DIR] [-t COUNT] LOCAL_FILE...',
361
367
  "Upload local files to the remote working directory. If a remote file of \
362
368
  the same name already exists, Dropbox will rename the upload unless the \
363
369
  the -f option is given, in which case the remote file will be \
364
- overwritten. If the -O option is given, the files will be uploaded to \
365
- the given directory instead of the current directory. The -q option \
366
- prevents progress from being printed. The -t option specifies the \
367
- number of tries in case of error. The default is 5; -t 0 will retry \
368
- infinitely.",
370
+ overwritten. If the -E option is given, delete each local file after \
371
+ successful upload. If the -O option is given, the files will be uploaded \
372
+ to the given directory instead of the current directory. The -q option \
373
+ prevents progress from being printed. The -t option specifies the number \
374
+ of tries in case of error. The default is 5; -t 0 will retry infinitely.",
369
375
  lambda do |client, state, args|
370
376
  flags = extract_flags(PUT.usage, args,
377
+ '-E' => 0,
371
378
  '-f' => 0,
372
379
  '-q' => 0,
373
380
  '-O' => 1,
@@ -388,11 +395,27 @@ module Commands
388
395
  tries_index = flags.find_index('-t')
389
396
  tries = tries_index ? flags[tries_index + 1].to_i : 5
390
397
 
398
+ # Glob arguments.
399
+ args.map! do |arg|
400
+ array = Dir.glob(File.expand_path(arg))
401
+ warn "put: #{arg}: no such file or directory" if array.empty?
402
+ array.map { |path| path.sub(File.dirname(path), File.dirname(arg)) }
403
+ end
404
+ args = args.reduce(:+)
405
+
391
406
  args.each do |arg|
392
407
  to_path = state.resolve_path(File.basename(arg))
393
408
 
394
409
  try_and_handle(StandardError) do
395
- File.open(File.expand_path(arg), 'rb') do |file|
410
+ path = File.expand_path(arg)
411
+ if File.directory?(path)
412
+ warn "put: #{arg}: cannot put directory"
413
+ next
414
+ end
415
+
416
+ success = false
417
+
418
+ File.open(path, 'rb') do |file|
396
419
  if flags.include?('-f') && state.metadata(to_path)
397
420
  client.file_delete(to_path)
398
421
  state.cache.remove(to_path)
@@ -400,15 +423,18 @@ module Commands
400
423
 
401
424
  # Chunked upload if file is more than 1M.
402
425
  if file.size > 1024 * 1024
403
- data = chunked_upload(client, to_path, file,
404
- flags.include?('-q'), tries)
426
+ data, success = chunked_upload(client, to_path, file,
427
+ flags.include?('-q'), tries)
405
428
  else
406
429
  data = client.put_file(to_path, file)
430
+ success = true
407
431
  end
408
432
 
409
433
  state.cache.add(data)
410
434
  puts "#{arg} -> #{data['path']}"
411
435
  end
436
+
437
+ File.delete(path) if flags.include?('-E') && success
412
438
  end
413
439
  end
414
440
 
@@ -706,12 +732,13 @@ module Commands
706
732
  thread = quiet ? nil : Thread.new { monitor_upload(uploader, to_path) }
707
733
  tries = -1 if tries == 0
708
734
  loop_upload(uploader, thread, tries)
735
+ success = (uploader.offset == uploader.total_size)
709
736
  data = uploader.finish(to_path)
710
737
  if thread
711
738
  thread.join
712
739
  print "\r" + (' ' * (18 + to_path.rpartition('/')[2].size)) + "\r"
713
740
  end
714
- data
741
+ [data, success]
715
742
  end
716
743
 
717
744
  # Continuously try to upload until successful or interrupted.
data/lib/droxi/state.rb CHANGED
@@ -35,6 +35,7 @@ class State
35
35
  # Return a +Hash+ of the Dropbox metadata for a file, or +nil+ if the file
36
36
  # does not exist.
37
37
  def metadata(path, require_contents = true)
38
+ path = path.downcase
38
39
  tokens = path.split('/').drop(1)
39
40
 
40
41
  (0..tokens.size).each do |i|
@@ -48,16 +49,18 @@ class State
48
49
 
49
50
  # Return an +Array+ of paths of files in a Dropbox directory.
50
51
  def contents(path)
52
+ path = path.downcase
51
53
  path = resolve_path(path)
52
54
  metadata(path)
53
55
  path = "#{path}/".sub('//', '/')
54
56
  @cache.keys.select do |key|
55
57
  key.start_with?(path) && key != path && !key.sub(path, '').include?('/')
56
- end
58
+ end.map { |key| @cache[key]['path'] }
57
59
  end
58
60
 
59
61
  # Return +true+ if the Dropbox path is a directory, +false+ otherwise.
60
62
  def directory?(path)
63
+ path = path.downcase
61
64
  path = resolve_path(path)
62
65
  metadata(File.dirname(path))
63
66
  @cache.include?(path) && @cache[path]['is_dir']
@@ -99,7 +102,7 @@ class State
99
102
  # Recursively remove directory contents from metadata cache. Yield lines of
100
103
  # (error) output if a block is given.
101
104
  def forget_contents(partial_path)
102
- path = resolve_path(partial_path)
105
+ path = resolve_path(partial_path).downcase
103
106
  if @cache.fetch(path, {}).include?('contents')
104
107
  @cache[path]['contents'].dup.each { |m| @cache.remove(m['path']) }
105
108
  @cache[path].delete('contents')
@@ -113,7 +116,7 @@ class State
113
116
  # Cache metadata for the remote file for a given path. Return +true+ if
114
117
  # successful, +false+ otherwise.
115
118
  def fetch_metadata(path)
116
- data = @client.metadata(path)
119
+ data = @client.metadata(path.downcase)
117
120
  return true if data['is_deleted']
118
121
  @cache.add(data)
119
122
  true
@@ -124,8 +127,11 @@ class State
124
127
  # Return an +Array+ of file paths matching a glob pattern, or a GlobError if
125
128
  # no files were matched.
126
129
  def get_matches(pattern, path, preserve_root)
130
+ path = path.downcase
127
131
  dir = File.dirname(path)
128
- matches = contents(dir).select { |entry| File.fnmatch(path, entry) }
132
+ matches = contents(dir).select do |entry|
133
+ File.fnmatch(path, entry.downcase)
134
+ end
129
135
  return GlobError.new(pattern) if matches.empty?
130
136
  return matches unless preserve_root
131
137
  prefix = pattern.rpartition('/')[0, 2].join
data/lib/droxi.rb CHANGED
@@ -1,4 +1,10 @@
1
- require 'dropbox_sdk'
1
+ begin
2
+ require 'dropbox_sdk'
3
+ rescue LoadError
4
+ puts "droxi requires the dropbox-sdk gem."
5
+ puts "Run `gem install dropbox-sdk` to install it."
6
+ exit
7
+ end
2
8
  require 'readline'
3
9
 
4
10
  require_relative 'droxi/commands'
@@ -10,15 +16,14 @@ require_relative 'droxi/text'
10
16
  # Command-line Dropbox client module.
11
17
  module Droxi
12
18
  # Version number of the program.
13
- VERSION = '0.2.3'
19
+ VERSION = '0.3.1'
14
20
 
15
21
  # Message to display when invoked with the --help option.
16
22
  HELP_TEXT =
17
23
  "If you've installed this program using Rake or the AUR package, you " \
18
- 'should also have the man page installed on your system. `man droxi` ' \
19
- "should do the trick. Otherwise--meaning you've probably installed it " \
20
- "as a Ruby gem--you don't, which is a shame. In that case, you can " \
21
- 'access the man page at http://jangler.info/man/droxi in HTML form.'
24
+ 'should also have the man page installed on your system. If you do not ' \
25
+ 'have the man page, you can access it at http://jangler.info/man/droxi ' \
26
+ 'in HTML form.'
22
27
 
23
28
  # Run the client.
24
29
  def self.run(args)
@@ -41,10 +46,12 @@ module Droxi
41
46
  # Handles command-line options extracted from an +Array+ and returns an
42
47
  # +Array+ of the extracted options.
43
48
  def self.handle_options(args)
44
- options = args.take_while { |s| s.start_with?('--') }
49
+ options = args.take_while { |s| s.start_with?('-') }
45
50
  puts "droxi v#{VERSION}" if options.include?('--version')
46
- Text.wrap(HELP_TEXT).each { |s| puts s } if options.include?('--help')
47
- exit if %w(--help --version).any? { |s| options.include?(s) }
51
+ if options.include?('-h') || options.include?('--help')
52
+ Text.wrap(HELP_TEXT).each { |s| puts s }
53
+ end
54
+ exit if %w(-h --help --version).any? { |s| options.include?(s) }
48
55
  options
49
56
  end
50
57
 
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.2.3
4
+ version: 0.3.1
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-12-17 00:00:00.000000000 Z
11
+ date: 2015-05-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dropbox-sdk
@@ -30,8 +30,8 @@ dependencies:
30
30
  - - ">="
31
31
  - !ruby/object:Gem::Version
32
32
  version: 1.6.4
33
- description: A command-line Dropbox interface inspired by GNU coreutils, GNU ftp,
34
- and lftp. Features include smart tab completion, globbing, and interactive help.
33
+ description: A command-line Dropbox interface based on GNU coreutils, GNU ftp, and
34
+ lftp. Features include smart tab completion, globbing, and interactive help.
35
35
  email: brandon@jangler.info
36
36
  executables:
37
37
  - droxi
@@ -79,9 +79,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
79
79
  version: '0'
80
80
  requirements: []
81
81
  rubyforge_project:
82
- rubygems_version: 2.2.2
82
+ rubygems_version: 2.4.5
83
83
  signing_key:
84
84
  specification_version: 4
85
- summary: ftp-like command-line interface to Dropbox
85
+ summary: An ftp-like command-line interface to Dropbox
86
86
  test_files: []
87
- has_rdoc: