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 +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:
|