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 +4 -4
- data/README.md +2 -1
- data/Rakefile +5 -0
- data/droxi.1.template +6 -0
- data/droxi.gemspec +1 -1
- data/lib/droxi/commands.rb +63 -0
- data/lib/droxi.rb +17 -6
- data/spec/cache_spec.rb +88 -0
- data/spec/commands_spec.rb +82 -10
- metadata +4 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fd104f683fa09123fc4146c2536d47c440139454
|
4
|
+
data.tar.gz: be57a75ccca1e2c18cca4e9e71df753b5df2f5d7
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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-
|
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 \
|
data/lib/droxi/commands.rb
CHANGED
@@ -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.
|
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
|
-
|
35
|
-
|
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
|
|
data/spec/cache_spec.rb
ADDED
@@ -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
|
data/spec/commands_spec.rb
CHANGED
@@ -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, '
|
490
|
-
out, _ = @share.call('/testing/
|
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, '
|
515
|
-
@rm.call('/testing/
|
516
|
-
state.metadata('/testing/
|
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
|
-
|
521
|
-
|
522
|
-
|
523
|
-
|
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.
|
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-
|
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:
|