droxi 0.1.3 → 0.2.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: 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: