droxi 0.2.3 → 0.3.1
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/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:
|