droxi 0.2.3 → 0.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/LICENSE +1 -1
- data/README.md +28 -26
- data/droxi.1.template +1 -1
- data/droxi.gemspec +5 -5
- data/lib/droxi/cache.rb +5 -2
- data/lib/droxi/commands.rb +41 -14
- data/lib/droxi/state.rb +10 -4
- data/lib/droxi.rb +16 -9
- metadata +6 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1c51dbec9e627b8b8b2e07e4ac8bf4464bee33dd
|
4
|
+
data.tar.gz: 480c18b2b62a8ec434cd6c8a8a481a40976f884b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 5af1c7d1ab268567fa44508dad484090c6da945f1861e022c025e34dff7527525d81af89d03812b613c1d8aa008e9e33e806656d47832f95fa9ae37654b16655
|
7
|
+
data.tar.gz: bd890a7e5aa0c14d912181f7d80643a46c1d41dbf914b4556919f660fa1e67edec98d8e18f48c25d01b9b6d71c4555e3c4e34a18553a6a613838410fba431906
|
data/LICENSE
CHANGED
data/README.md
CHANGED
@@ -1,41 +1,43 @@
|
|
1
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
-
|
31
|
-
-
|
32
|
-
-
|
33
|
-
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
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 = '
|
5
|
-
s.summary = 'ftp-like command-line interface to Dropbox'
|
6
|
-
s.description = "A command-line Dropbox interface
|
7
|
-
|
8
|
-
|
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)
|
data/lib/droxi/commands.rb
CHANGED
@@ -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
|
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 -
|
365
|
-
the
|
366
|
-
|
367
|
-
|
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.
|
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
|
-
|
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
|
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
|
-
|
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.
|
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.
|
19
|
-
|
20
|
-
|
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
|
-
|
47
|
-
|
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.
|
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:
|
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
|
34
|
-
|
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.
|
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:
|