droxi 0.1.3 → 0.2.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: 55c2e238628368cdd91abd0fb76a31cb0b217159
4
- data.tar.gz: bf2bec767b3e0b80bf264a2784fc9b2c3cda15a6
3
+ metadata.gz: fd104f683fa09123fc4146c2536d47c440139454
4
+ data.tar.gz: be57a75ccca1e2c18cca4e9e71df753b5df2f5d7
5
5
  SHA512:
6
- metadata.gz: 18f9cad5986b95260f0c9743231bff2c5a9bc634d7fa61a39ead74664d8c61a2a803b31a91bfd96df2a7acbecf381aa04c8d13c378ad2aaa0d08e94fdbf94f0d
7
- data.tar.gz: 0414164a2564a94de66d758422ef65e213909952a3dc69f4fb1dd10265f01cb000f7d5debb10afd302116d55f47979656720bf4b9866dc5db859d059272a7a2b
6
+ metadata.gz: 45416d90dec9c6ea116b7c360efcdd0b336e5d6b7933cba981173a9d7337f0bac89798f5b8db13332c77e79da385f4b77e1c334c9410892303051f229e8a61ee
7
+ data.tar.gz: d7c3403cf8cdd2684fb5f0f051473802792a9bb1680b28cc5447d1537df63a61befd0b09744854752bc6e1ac1092b725d455211458d298c423ea398f9fc72ede
data/README.md CHANGED
@@ -28,7 +28,8 @@ 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, organize, and share files
31
+ - upload, download, organize, search, and share files
32
+ - file revision control
32
33
  - man page and interactive help
33
34
 
34
35
  developer features
data/Rakefile CHANGED
@@ -80,6 +80,11 @@ task :build do
80
80
  build_page
81
81
  end
82
82
 
83
+ desc 'build html version of man page'
84
+ task :html do
85
+ sh 'groff -man -T html build/droxi.1 > build/droxi.html'
86
+ end
87
+
83
88
  PREFIX = ENV['PREFIX'] || ENV['prefix'] || '/usr/local'
84
89
  BIN_PATH = "#{PREFIX}/bin"
85
90
  MAN_PATH = "#{PREFIX}/share/man/man1"
data/droxi.1.template CHANGED
@@ -18,7 +18,13 @@ 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
22
+ Print help information and exit.
23
+ .TP
21
24
  --version
22
25
  Print version information and exit.
26
+ .SH BUGS
27
+ If you find one, please use https://github.com/jangler/droxi/issues to report
28
+ it, or contact me via email.
23
29
  .SH AUTHOR
24
30
  Written by Brandon Mulcahy (brandon@jangler.info).
data/droxi.gemspec CHANGED
@@ -1,7 +1,7 @@
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-06-08'
4
+ s.date = '2014-06-09'
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 \
@@ -212,6 +212,32 @@ module Commands
212
212
  end
213
213
  )
214
214
 
215
+ # Get remote file revisions.
216
+ HISTORY = Command.new(
217
+ 'history REMOTE_FILE',
218
+ "Print a list of revisions for a remote file. The file can be restored to \
219
+ a previous revision using the 'restore' command and a revision ID given \
220
+ by this command.",
221
+ lambda do |client, state, args|
222
+ extract_flags('history', args, '')
223
+ path = state.resolve_path(args.first)
224
+ if !state.metadata(path) || state.directory?(path)
225
+ warn "history: #{args.first}: no such file"
226
+ else
227
+ try_and_handle(DropboxError) do
228
+ client.revisions(path).each do |rev|
229
+
230
+ size = rev['size'].sub(/ (.)B/, '\1').sub(' bytes', '').rjust(7)
231
+ mtime = Time.parse(rev['modified'])
232
+ current_year = (mtime.year == Time.now.year)
233
+ format_str = current_year ? '%b %e %H:%M' : '%b %e %Y'
234
+ puts "#{size} #{mtime.strftime(format_str)} #{rev['rev']}"
235
+ end
236
+ end
237
+ end
238
+ end
239
+ )
240
+
215
241
  # Change the local working directory.
216
242
  LCD = Command.new(
217
243
  'lcd [LOCAL_DIR]',
@@ -355,6 +381,24 @@ module Commands
355
381
  end
356
382
  )
357
383
 
384
+ # Restore a remove file to a previous version.
385
+ RESTORE = Command.new(
386
+ 'restore REMOTE_FILE REVISION_ID',
387
+ "Restore a remote file to a previous version. Use the 'history' command \
388
+ to get a list of IDs for previous revisions of the file.",
389
+ lambda do |client, state, args|
390
+ extract_flags('restore', args, '')
391
+ path = state.resolve_path(args.first)
392
+ if !state.metadata(path) || state.directory?(path)
393
+ warn "restore: #{args.first}: no such file"
394
+ else
395
+ try_and_handle(DropboxError) do
396
+ client.restore(path, args.last)
397
+ end
398
+ end
399
+ end
400
+ )
401
+
358
402
  # Remove remote files.
359
403
  RM = Command.new(
360
404
  'rm [-r] REMOTE_FILE...',
@@ -409,6 +453,25 @@ module Commands
409
453
  end
410
454
  )
411
455
 
456
+ # Search for remote files.
457
+ SEARCH = Command.new(
458
+ 'search REMOTE_DIR SUBSTRING...',
459
+ "List remote files in a directory or its subdirectories with names that \
460
+ contain all given substrings.",
461
+ lambda do |client, state, args|
462
+ extract_flags('search', args, '')
463
+ path = state.resolve_path(args.first)
464
+ unless state.directory?(path)
465
+ warn "search: #{args.first}: no such directory"
466
+ return
467
+ end
468
+ query = args.drop(1).join(' ')
469
+ try_and_handle(DropboxError) do
470
+ client.search(path, query).each { |result| puts result['path'] }
471
+ end
472
+ end
473
+ )
474
+
412
475
  # Get permanent links to remote files.
413
476
  SHARE = Command.new(
414
477
  'share REMOTE_FILE...',
data/lib/droxi.rb CHANGED
@@ -5,11 +5,20 @@ require_relative 'droxi/commands'
5
5
  require_relative 'droxi/complete'
6
6
  require_relative 'droxi/settings'
7
7
  require_relative 'droxi/state'
8
+ require_relative 'droxi/text'
8
9
 
9
10
  # Command-line Dropbox client module.
10
11
  module Droxi
11
12
  # Version number of the program.
12
- VERSION = '0.1.3'
13
+ VERSION = '0.2.0'
14
+
15
+ # Message to display when invoked with the --help option.
16
+ HELP_TEXT =
17
+ "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.'
13
22
 
14
23
  # Run the client.
15
24
  def self.run(args)
@@ -20,7 +29,10 @@ module Droxi
20
29
  args.shift(options.size)
21
30
 
22
31
  args.empty? ? run_interactive(client, state) : invoke(args, client, state)
23
-
32
+ rescue DropboxAuthError => error
33
+ warn error
34
+ Settings.delete(:access_token)
35
+ ensure
24
36
  Settings.save
25
37
  end
26
38
 
@@ -30,10 +42,9 @@ module Droxi
30
42
  # +Array+ of the extracted options.
31
43
  def self.handle_options(args)
32
44
  options = args.take_while { |s| s.start_with?('--') }
33
- if options.include?('--version')
34
- puts "droxi v#{VERSION}"
35
- exit
36
- end
45
+ 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) }
37
48
  options
38
49
  end
39
50
 
@@ -0,0 +1,88 @@
1
+ require 'minitest/autorun'
2
+
3
+ require_relative '../lib/droxi/cache'
4
+
5
+ describe Cache do
6
+ before do
7
+ @cache = Cache.new
8
+ end
9
+
10
+ def file(path)
11
+ { 'path' => path, 'is_dir' => false }
12
+ end
13
+
14
+ def dir(path)
15
+ { 'path' => path, 'is_dir' => true, 'contents' => [] }
16
+ end
17
+
18
+ describe 'when adding file metadata to the cache' do
19
+ it 'must associate the path with the metadata hash' do
20
+ @cache.add(file('/file'))
21
+ @cache['/file'].wont_be_nil
22
+ end
23
+
24
+ it 'must associate the path with its parent directory' do
25
+ @cache.add(dir('/dir'))
26
+ @cache.add(file('/dir/file'))
27
+ @cache['/dir']['contents'].size.must_equal 1
28
+ end
29
+
30
+ it 'must not duplicate data associated with parent directory' do
31
+ @cache.add(dir('/dir'))
32
+ @cache.add(file('/dir/file'))
33
+ @cache.add(file('/dir/file'))
34
+ @cache['/dir']['contents'].size.must_equal 1
35
+ end
36
+
37
+ it 'must add and associate directory contents' do
38
+ folder = dir('/dir')
39
+ contents = 3.times.map { |i| file("/dir/file#{i}") }
40
+ folder['contents'] = contents
41
+ @cache.add(folder)
42
+ @cache.size.must_equal 4
43
+ end
44
+ end
45
+
46
+ describe 'when removing a path from the cache' do
47
+ it 'must delete the metadata from the hash' do
48
+ @cache.add(file('/file'))
49
+ @cache.remove('/file')
50
+ @cache.must_be :empty?
51
+ end
52
+
53
+ it 'must delete the metadata from the parent directory' do
54
+ @cache.add(dir('/dir'))
55
+ @cache.add(file('/dir/file'))
56
+ @cache.remove('/dir/file')
57
+ @cache['/dir']['contents'].must_be :empty?
58
+ end
59
+
60
+ it 'must delete contents recursively' do
61
+ @cache.add(dir('/dir'))
62
+ @cache.add(file('/dir/file'))
63
+ @cache.remove('/dir')
64
+ @cache.must_be :empty?
65
+ end
66
+ end
67
+
68
+ describe 'when querying whether the cache has full info on a path' do
69
+ it 'must return true for files' do
70
+ @cache.add(file('/file'))
71
+ @cache.full_info?('/file').must_equal true
72
+ end
73
+
74
+ it 'must return false for fictional files' do
75
+ @cache.full_info?('/file').wont_equal true
76
+ end
77
+
78
+ it 'must return false for directories without contents' do
79
+ @cache.add('path' => '/dir', 'is_dir' => true)
80
+ @cache.full_info?('/dir').wont_equal true
81
+ end
82
+
83
+ it 'must return true for directories with contents' do
84
+ @cache.add(dir('/dir'))
85
+ @cache.full_info?('/dir').must_equal true
86
+ end
87
+ end
88
+ end
@@ -257,6 +257,30 @@ describe Commands do
257
257
  end
258
258
  end
259
259
 
260
+ describe 'when executing the history command' do
261
+ before do
262
+ state.pwd = TestUtils::TEST_ROOT
263
+ @history = proc do |*args|
264
+ capture_io { Commands::HISTORY.exec(client, state, *args) }
265
+ end
266
+ end
267
+
268
+ it 'must give the revision history of a given file' do
269
+ TestUtils.structure(client, state, 'test.txt')
270
+ out, _ = @history.call('test.txt')
271
+ out.lines.size.must_be :>=, 1
272
+ end
273
+
274
+ it 'must fail when given a bogus filename or directory name' do
275
+ TestUtils.structure(client, state, 'test')
276
+ %w(bogus test).each do |arg|
277
+ _, err = @history.call(arg)
278
+ err.lines.size.must_equal 1
279
+ err.start_with?('history: ').must_equal true
280
+ end
281
+ end
282
+ end
283
+
260
284
  describe 'when executing the lcd command' do
261
285
  it 'must change to home directory when given no args' do
262
286
  prev_pwd = Dir.pwd
@@ -478,6 +502,32 @@ describe Commands do
478
502
  end
479
503
  end
480
504
 
505
+ describe 'when executing the restore command' do
506
+ before do
507
+ state.pwd = TestUtils::TEST_ROOT
508
+ @restore = proc do |*args|
509
+ capture_io { Commands::RESTORE.exec(client, state, *args) }
510
+ end
511
+ end
512
+
513
+ it 'must set the revision of a file' do
514
+ TestUtils.structure(client, state, 'test.txt')
515
+ out, _ = capture_io { Commands::HISTORY.exec(client, state, 'test.txt') }
516
+ rev = out.split.last
517
+ _, err = @restore.call('test.txt', rev)
518
+ err.must_be :empty?
519
+ end
520
+
521
+ it 'must fail when given a bogus filename or directory name' do
522
+ TestUtils.structure(client, state, 'test')
523
+ %w(bogus test).each do |arg|
524
+ _, err = @restore.call(arg, 'n/a')
525
+ err.lines.size.must_equal 1
526
+ err.start_with?('restore: ').must_equal true
527
+ end
528
+ end
529
+ end
530
+
481
531
  describe 'when executing the share command' do
482
532
  before do
483
533
  @share = proc do |*args|
@@ -486,8 +536,8 @@ describe Commands do
486
536
  end
487
537
 
488
538
  it 'must yield URL when given file path' do
489
- TestUtils.structure(client, state, 'test.txt')
490
- out, _ = @share.call('/testing/test.txt')
539
+ TestUtils.structure(client, state, 'share.txt')
540
+ out, _ = @share.call('/testing/share.txt')
491
541
  out.lines.size.must_equal 1
492
542
  %r{https://.+\..+/}.match(out).wont_be_nil
493
543
  end
@@ -511,17 +561,16 @@ describe Commands do
511
561
  end
512
562
 
513
563
  it 'must remove the remote file when given args' do
514
- TestUtils.structure(client, state, 'test.txt')
515
- @rm.call('/testing/test.txt')
516
- state.metadata('/testing/test.txt').must_be_nil
564
+ TestUtils.structure(client, state, 'file.txt')
565
+ @rm.call('/testing/file.txt')
566
+ state.metadata('/testing/file.txt').must_be_nil
517
567
  end
518
568
 
519
569
  it 'must change pwd to existing dir if the current one is removed' do
520
- # FIXME: I don't know why this test fails. It works in practice.
521
- # TestUtils.structure(client, state, 'one', 'one/two')
522
- # Commands::CD.exec(client, state, '/testing/one/two')
523
- # Commands::RM.exec(client, state, '..')
524
- # state.pwd.must_equal('/testing')
570
+ TestUtils.structure(client, state, 'one', 'one/two')
571
+ Commands::CD.exec(client, state, '/testing/one/two')
572
+ Commands::RM.exec(client, state, '-r', '..')
573
+ state.pwd.must_equal('/testing')
525
574
  end
526
575
 
527
576
  it 'must fail with UsageError when given no args' do
@@ -549,6 +598,29 @@ describe Commands do
549
598
  end
550
599
  end
551
600
 
601
+ describe 'when executing the search command' do
602
+ before do
603
+ state.pwd = TestUtils::TEST_ROOT
604
+ @search = proc do |*args|
605
+ capture_io { Commands::SEARCH.exec(client, state, *args) }
606
+ end
607
+ end
608
+
609
+ it 'must list files iff they match the given substrings' do
610
+ TestUtils.exact_structure(client, state,
611
+ 'hello.txt', 'world.txt',
612
+ 'hi.txt', 'hello.md')
613
+ out, _ = @search.call('.', 'o', 'txt')
614
+ out.split("\n").must_equal ['/testing/hello.txt', '/testing/world.txt']
615
+ end
616
+
617
+ it 'must fail if given a bogus directory name' do
618
+ _, err = @search.call('bogus', 'substring')
619
+ err.lines.size.must_equal 1
620
+ err.start_with?('search: ').must_equal true
621
+ end
622
+ end
623
+
552
624
  describe 'when executing the help command' do
553
625
  before do
554
626
  @help = proc do |*args|
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.1.3
4
+ version: 0.2.0
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-08 00:00:00.000000000 Z
11
+ date: 2014-06-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: dropbox-sdk
@@ -52,6 +52,7 @@ files:
52
52
  - lib/droxi/state.rb
53
53
  - lib/droxi/text.rb
54
54
  - spec/all.rb
55
+ - spec/cache_spec.rb
55
56
  - spec/commands_spec.rb
56
57
  - spec/complete_spec.rb
57
58
  - spec/settings_spec.rb
@@ -83,3 +84,4 @@ signing_key:
83
84
  specification_version: 4
84
85
  summary: ftp-like command-line interface to Dropbox
85
86
  test_files: []
87
+ has_rdoc: