droxi 0.0.3 → 0.0.4
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 +12 -1
- data/Rakefile +6 -1
- data/droxi.gemspec +2 -2
- data/lib/droxi/commands.rb +128 -79
- data/lib/droxi/state.rb +51 -22
- data/lib/droxi.rb +52 -50
- data/spec/commands_spec.rb +38 -3
- data/spec/complete_spec.rb +1 -5
- data/spec/state_spec.rb +48 -0
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7475e5467fb8a2cce45ca783361e37dcfe2f2361
|
4
|
+
data.tar.gz: 71a443989f3ef75df19997850f865fef2bac27fe
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 20b0e20d19722a326203d48a5b5232203c88ebeecb809c94e084ebb223de4c97a756de512959e667d6dca5224e71ef82d587f1870d215fc75b795a16ff9edff3
|
7
|
+
data.tar.gz: 09afb109d29dd70c2d77add8a3557a9442f58cb02deda624731022bf4eb73ad500e9d2ebe2876bf7115b7a690fd16d74e144f8e7f58801e27c187523b6191c9e
|
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
droxi
|
2
2
|
=====
|
3
3
|
|
4
|
-
ftp-like command-line [Dropbox](https://www.dropbox.com/
|
4
|
+
ftp-like command-line [Dropbox](https://www.dropbox.com/) interface in
|
5
5
|
[Ruby](https://www.ruby-lang.org/en/)
|
6
6
|
|
7
7
|
installation
|
@@ -9,6 +9,17 @@ installation
|
|
9
9
|
|
10
10
|
gem install droxi
|
11
11
|
|
12
|
+
or
|
13
|
+
|
14
|
+
git clone https://github.com/jangler/droxi.git
|
15
|
+
cd droxi && rake && sudo rake install
|
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
|
22
|
+
|
12
23
|
features
|
13
24
|
--------
|
14
25
|
|
data/Rakefile
CHANGED
@@ -13,7 +13,7 @@ end
|
|
13
13
|
|
14
14
|
desc 'install gem'
|
15
15
|
task :gem do
|
16
|
-
sh 'rm
|
16
|
+
sh 'rm -f droxi-*.gem'
|
17
17
|
sh 'gem build droxi.gemspec'
|
18
18
|
sh 'gem install ./droxi-*.gem'
|
19
19
|
end
|
@@ -93,3 +93,8 @@ task :uninstall do
|
|
93
93
|
puts error
|
94
94
|
end
|
95
95
|
end
|
96
|
+
|
97
|
+
desc 'remove files generated by other targets'
|
98
|
+
task :clean do
|
99
|
+
sh 'rm -rf build doc droxi-*.gem'
|
100
|
+
end
|
data/droxi.gemspec
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'droxi'
|
3
|
-
s.version = '0.0.
|
4
|
-
s.date = '2014-06-
|
3
|
+
s.version = '0.0.4'
|
4
|
+
s.date = '2014-06-03'
|
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
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'time'
|
2
|
+
|
1
3
|
require_relative 'text'
|
2
4
|
|
3
5
|
# Module containing definitions for client commands.
|
@@ -38,7 +40,7 @@ module Commands
|
|
38
40
|
block = proc { |line| yield line if block_given? }
|
39
41
|
@procedure.yield(client, state, args, block)
|
40
42
|
else
|
41
|
-
|
43
|
+
fail UsageError, @usage
|
42
44
|
end
|
43
45
|
end
|
44
46
|
|
@@ -46,7 +48,7 @@ module Commands
|
|
46
48
|
# If the index is out of range, return the type of the final argument. If
|
47
49
|
# the +Command+ takes no arguments, return +nil+.
|
48
50
|
def type_of_arg(index)
|
49
|
-
args = @usage.split.drop(1)
|
51
|
+
args = @usage.split.drop(1).reject { |arg| arg.include?('-') }
|
50
52
|
if args.empty?
|
51
53
|
nil
|
52
54
|
else
|
@@ -103,6 +105,23 @@ module Commands
|
|
103
105
|
end
|
104
106
|
)
|
105
107
|
|
108
|
+
# Clear the cache.
|
109
|
+
FORGET = Command.new(
|
110
|
+
'forget [REMOTE_DIR]...',
|
111
|
+
"Clear the client-side cache of remote filesystem metadata. With no \
|
112
|
+
arguments, clear the entire cache. If given directories as arguments, \
|
113
|
+
(recursively) clear the cache of those directories only.",
|
114
|
+
lambda do |client, state, args, output|
|
115
|
+
if args.empty?
|
116
|
+
state.cache.clear
|
117
|
+
else
|
118
|
+
args.each do |arg|
|
119
|
+
state.forget_contents(arg) { |line| output.call(line) }
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
123
|
+
)
|
124
|
+
|
106
125
|
# Download remote files.
|
107
126
|
GET = Command.new(
|
108
127
|
'get REMOTE_FILE...',
|
@@ -110,14 +129,18 @@ module Commands
|
|
110
129
|
local working directory.",
|
111
130
|
lambda do |client, state, args, output|
|
112
131
|
state.expand_patterns(args).each do |path|
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
132
|
+
if path.is_a?(GlobError)
|
133
|
+
output.call("get: #{path}: No such file or directory")
|
134
|
+
else
|
135
|
+
begin
|
136
|
+
contents = client.get_file(path)
|
137
|
+
File.open(File.basename(path), 'wb') do |file|
|
138
|
+
file.write(contents)
|
139
|
+
end
|
140
|
+
output.call("#{File.basename(path)} <- #{path}")
|
141
|
+
rescue DropboxError => error
|
142
|
+
output.call(error.to_s)
|
117
143
|
end
|
118
|
-
output.call("#{File.basename(path)} <- #{path}")
|
119
|
-
rescue DropboxError => error
|
120
|
-
output.call(error.to_s)
|
121
144
|
end
|
122
145
|
end
|
123
146
|
end
|
@@ -171,41 +194,39 @@ module Commands
|
|
171
194
|
|
172
195
|
# List remote files.
|
173
196
|
LS = Command.new(
|
174
|
-
'ls [REMOTE_FILE]...',
|
197
|
+
'ls [-l] [REMOTE_FILE]...',
|
175
198
|
"List information about remote files. With no arguments, list the \
|
176
199
|
contents of the working directory. When given remote directories as \
|
177
200
|
arguments, list the contents of the directories. When given remote files \
|
178
|
-
as arguments, list the files.
|
201
|
+
as arguments, list the files. If the -l option is given, display \
|
202
|
+
information about the files.",
|
179
203
|
lambda do |client, state, args, output|
|
180
|
-
|
181
|
-
|
182
|
-
|
183
|
-
|
184
|
-
|
185
|
-
|
186
|
-
|
187
|
-
|
188
|
-
|
189
|
-
path
|
190
|
-
end
|
191
|
-
rescue DropboxError
|
192
|
-
path
|
193
|
-
end
|
204
|
+
long = args.delete('-l') != nil
|
205
|
+
|
206
|
+
files, dirs = [], []
|
207
|
+
state.expand_patterns(args, true).each do |path|
|
208
|
+
if path.is_a?(GlobError)
|
209
|
+
output.call("ls: #{path}: No such file or directory")
|
210
|
+
else
|
211
|
+
type = state.directory?(path) ? dirs : files
|
212
|
+
type << path
|
194
213
|
end
|
195
214
|
end
|
196
215
|
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
216
|
+
dirs << state.pwd if args.empty?
|
217
|
+
|
218
|
+
# First list files
|
219
|
+
list(state, files, files, long) { |line| output.call(line) }
|
220
|
+
output.call('') if !(dirs.empty? || files.empty?)
|
221
|
+
|
222
|
+
# Then list directory contents
|
223
|
+
dirs.each_with_index do |dir, i|
|
224
|
+
output.call(dir + ':') if dirs.length + files.length > 1
|
225
|
+
contents = state.contents(dir)
|
226
|
+
names = contents.map { |path| File.basename(path) }
|
227
|
+
list(state, contents, names, long) { |line| output.call(line) }
|
228
|
+
output.call('') if i < dirs.length - 1
|
207
229
|
end
|
208
|
-
Text.table(items).each { |item| output.call(item) }
|
209
230
|
end
|
210
231
|
)
|
211
232
|
|
@@ -216,11 +237,15 @@ module Commands
|
|
216
237
|
time-limited and link directly to the files themselves.",
|
217
238
|
lambda do |client, state, args, output|
|
218
239
|
state.expand_patterns(args).each do |path|
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
|
240
|
+
if path.is_a?(GlobError)
|
241
|
+
output.call("media: #{path}: No such file or directory")
|
242
|
+
else
|
243
|
+
begin
|
244
|
+
url = client.media(path)['url']
|
245
|
+
output.call("#{File.basename(path)} -> #{url}")
|
246
|
+
rescue DropboxError => error
|
247
|
+
output.call(error.to_s)
|
248
|
+
end
|
224
249
|
end
|
225
250
|
end
|
226
251
|
end
|
@@ -276,11 +301,15 @@ module Commands
|
|
276
301
|
"Remove each specified remote file or directory.",
|
277
302
|
lambda do |client, state, args, output|
|
278
303
|
state.expand_patterns(args).each do |path|
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
304
|
+
if path.is_a?(GlobError)
|
305
|
+
output.call("rm: #{path}: No such file or directory")
|
306
|
+
else
|
307
|
+
begin
|
308
|
+
client.file_delete(path)
|
309
|
+
state.cache.delete(path)
|
310
|
+
rescue DropboxError => error
|
311
|
+
output.call(error.to_s)
|
312
|
+
end
|
284
313
|
end
|
285
314
|
end
|
286
315
|
end
|
@@ -295,11 +324,15 @@ module Commands
|
|
295
324
|
expiration is effectively not an issue.",
|
296
325
|
lambda do |client, state, args, output|
|
297
326
|
state.expand_patterns(args).each do |path|
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
327
|
+
if path.is_a?(GlobError)
|
328
|
+
output.call("share: #{path}: No such file or directory")
|
329
|
+
else
|
330
|
+
begin
|
331
|
+
url = client.shares(path)['url']
|
332
|
+
output.call("#{File.basename(path)} -> #{url}")
|
333
|
+
rescue DropboxError => error
|
334
|
+
output.call(error.to_s)
|
335
|
+
end
|
303
336
|
end
|
304
337
|
end
|
305
338
|
end
|
@@ -315,45 +348,61 @@ module Commands
|
|
315
348
|
if input.start_with?('!')
|
316
349
|
shell(input[1, input.length - 1]) { |line| puts line }
|
317
350
|
elsif not input.empty?
|
318
|
-
tokens = input
|
351
|
+
tokens = tokenize(input)
|
352
|
+
cmd, args = tokens[0], tokens.drop(1)
|
353
|
+
try_command(cmd, args, client, state)
|
354
|
+
end
|
355
|
+
end
|
319
356
|
|
320
|
-
|
321
|
-
i = 0
|
322
|
-
while i < tokens.length - 1
|
323
|
-
if tokens[i].end_with?('\\')
|
324
|
-
tokens[i] = "#{tokens[i].chop} #{tokens.delete_at(i + 1)}"
|
325
|
-
else
|
326
|
-
i += 1
|
327
|
-
end
|
328
|
-
end
|
357
|
+
private
|
329
358
|
|
330
|
-
|
359
|
+
def self.try_command(command_name, args, client, state)
|
360
|
+
if NAMES.include?(command_name)
|
361
|
+
begin
|
362
|
+
command = const_get(command_name.upcase.to_sym)
|
363
|
+
command.exec(client, state, *args) { |line| puts line }
|
364
|
+
rescue UsageError => error
|
365
|
+
puts "Usage: #{error}"
|
366
|
+
end
|
367
|
+
else
|
368
|
+
puts "droxi: #{command_name}: command not found"
|
369
|
+
end
|
370
|
+
end
|
331
371
|
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
|
336
|
-
end
|
337
|
-
rescue UsageError => error
|
338
|
-
puts "Usage: #{error}"
|
339
|
-
end
|
372
|
+
def self.tokenize(string)
|
373
|
+
string.split.reduce([]) do |list, token|
|
374
|
+
list << if !list.empty? && list.last.end_with?('\\')
|
375
|
+
"#{list.pop.chop} #{token}"
|
340
376
|
else
|
341
|
-
|
377
|
+
token
|
342
378
|
end
|
343
379
|
end
|
344
380
|
end
|
345
381
|
|
346
|
-
|
382
|
+
def self.long_info(state, path, name)
|
383
|
+
meta = state.metadata(state.resolve_path(path), false)
|
384
|
+
is_dir = meta['is_dir'] ? 'd' : '-'
|
385
|
+
size = meta['size'].sub(/ (.)B/, '\1').sub(' bytes', '').rjust(7)
|
386
|
+
mtime = Time.parse(meta['modified'])
|
387
|
+
format_str = (mtime.year == Time.now.year) ? '%b %e %H:%M' : '%b %e %Y'
|
388
|
+
"#{is_dir} #{size} #{mtime.strftime(format_str)} #{name}"
|
389
|
+
end
|
390
|
+
|
391
|
+
def self.list(state, paths, names, long)
|
392
|
+
if long
|
393
|
+
paths.zip(names).each { |path, name| yield long_info(state, path, name) }
|
394
|
+
else
|
395
|
+
Text.table(names).each { |line| yield line }
|
396
|
+
end
|
397
|
+
end
|
347
398
|
|
348
399
|
def self.shell(cmd)
|
349
|
-
|
350
|
-
|
351
|
-
pipe.each_line { |line| yield line.chomp if block_given? }
|
352
|
-
end
|
353
|
-
rescue Interrupt
|
354
|
-
rescue Exception => error
|
355
|
-
yield error.to_s if block_given?
|
400
|
+
IO.popen(cmd) do |pipe|
|
401
|
+
pipe.each_line { |line| yield line.chomp if block_given? }
|
356
402
|
end
|
403
|
+
rescue Interrupt
|
404
|
+
rescue Exception => error
|
405
|
+
yield error.to_s if block_given?
|
357
406
|
end
|
358
407
|
|
359
408
|
end
|
data/lib/droxi/state.rb
CHANGED
@@ -1,5 +1,9 @@
|
|
1
1
|
require_relative 'settings'
|
2
2
|
|
3
|
+
# Represents a failure of a glob expression to match files.
|
4
|
+
class GlobError < ArgumentError
|
5
|
+
end
|
6
|
+
|
3
7
|
# Encapsulates the session state of the client.
|
4
8
|
class State
|
5
9
|
|
@@ -31,12 +35,12 @@ class State
|
|
31
35
|
|
32
36
|
# Return a +Hash+ of the Dropbox metadata for a file, or +nil+ if the file
|
33
37
|
# does not exist.
|
34
|
-
def metadata(path)
|
38
|
+
def metadata(path, require_contents=true)
|
35
39
|
tokens = path.split('/').drop(1)
|
36
40
|
|
37
41
|
for i in 0..tokens.length
|
38
42
|
partial_path = '/' + tokens.take(i).join('/')
|
39
|
-
unless have_all_info_for(partial_path)
|
43
|
+
unless have_all_info_for(partial_path, require_contents)
|
40
44
|
begin
|
41
45
|
data = @cache[partial_path] = @client.metadata(partial_path)
|
42
46
|
rescue DropboxError
|
@@ -55,6 +59,7 @@ class State
|
|
55
59
|
|
56
60
|
# Return an +Array+ of paths of files in a Dropbox directory.
|
57
61
|
def contents(path)
|
62
|
+
path = resolve_path(path)
|
58
63
|
metadata(path)
|
59
64
|
path = "#{path}/".sub('//', '/')
|
60
65
|
@cache.keys.select do |key|
|
@@ -64,7 +69,7 @@ class State
|
|
64
69
|
|
65
70
|
# Return +true+ if the Dropbox path is a directory, +false+ otherwise.
|
66
71
|
def directory?(path)
|
67
|
-
path = path
|
72
|
+
path = resolve_path(path)
|
68
73
|
metadata(File.dirname(path))
|
69
74
|
@cache.include?(path) && @cache[path]['is_dir']
|
70
75
|
end
|
@@ -77,11 +82,12 @@ class State
|
|
77
82
|
end
|
78
83
|
|
79
84
|
# Expand a Dropbox file path and return the result.
|
80
|
-
def resolve_path(
|
81
|
-
path = "#{@pwd}/#{
|
85
|
+
def resolve_path(arg)
|
86
|
+
path = arg.start_with?('/') ? arg.dup : "#{@pwd}/#{arg}"
|
82
87
|
path.gsub!('//', '/')
|
83
|
-
while path.sub!(/\/([^\/]+?)\/\.\./, '')
|
84
|
-
|
88
|
+
nil while path.sub!(/\/([^\/]+?)\/\.\./, '')
|
89
|
+
nil while path.sub!('./', '')
|
90
|
+
path.sub!(/\/\.$/, '')
|
85
91
|
path.chomp!('/')
|
86
92
|
path = '/' if path.empty?
|
87
93
|
path
|
@@ -89,29 +95,52 @@ class State
|
|
89
95
|
|
90
96
|
# Expand an +Array+ of file globs into an an +Array+ of Dropbox file paths
|
91
97
|
# and return the result.
|
92
|
-
def expand_patterns(patterns)
|
98
|
+
def expand_patterns(patterns, preserve_root=false)
|
93
99
|
patterns.map do |pattern|
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
@client.metadata(File.dirname(final_pattern))['contents'].each do |data|
|
98
|
-
path = data['path']
|
99
|
-
matches << path if File.fnmatch(final_pattern, path)
|
100
|
-
end
|
101
|
-
|
102
|
-
if matches.empty?
|
103
|
-
[final_pattern]
|
100
|
+
path = resolve_path(pattern)
|
101
|
+
if directory?(path)
|
102
|
+
preserve_root ? pattern : path
|
104
103
|
else
|
105
|
-
|
104
|
+
get_matches(pattern, path, preserve_root)
|
106
105
|
end
|
107
106
|
end.flatten
|
108
107
|
end
|
109
108
|
|
109
|
+
# Recursively remove directory contents from metadata cache. Yield lines of
|
110
|
+
# (error) output if a block is given.
|
111
|
+
def forget_contents(partial_path)
|
112
|
+
path = resolve_path(partial_path)
|
113
|
+
if @cache.include?(path) && @cache[path].include?('contents')
|
114
|
+
@cache[path].delete('contents')
|
115
|
+
@cache.keys.each do |key|
|
116
|
+
@cache.delete(key) if key.start_with?(path) && key != path
|
117
|
+
end
|
118
|
+
elsif block_given?
|
119
|
+
yield "forget: #{partial_path}: Nothing to forget"
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
110
123
|
private
|
111
124
|
|
112
|
-
def
|
113
|
-
|
114
|
-
(
|
125
|
+
def get_matches(pattern, path, preserve_root)
|
126
|
+
dir = File.dirname(path)
|
127
|
+
matches = contents(dir).select { |entry| File.fnmatch(path, entry) }
|
128
|
+
if matches.empty?
|
129
|
+
GlobError.new(pattern)
|
130
|
+
elsif preserve_root
|
131
|
+
prefix = pattern.rpartition('/')[0, 2].join
|
132
|
+
matches.map { |match| prefix + match.rpartition('/')[2] }
|
133
|
+
else
|
134
|
+
matches
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def have_all_info_for(path, require_contents=true)
|
139
|
+
@cache.include?(path) && (
|
140
|
+
!require_contents ||
|
141
|
+
!@cache[path]['is_dir'] ||
|
142
|
+
@cache[path].include?('contents')
|
143
|
+
)
|
115
144
|
end
|
116
145
|
|
117
146
|
end
|
data/lib/droxi.rb
CHANGED
@@ -9,7 +9,27 @@ require_relative 'droxi/state'
|
|
9
9
|
# Command-line Dropbox client module.
|
10
10
|
module Droxi
|
11
11
|
|
12
|
-
#
|
12
|
+
# Run the client.
|
13
|
+
def self.run(*args)
|
14
|
+
client = DropboxClient.new(get_access_token)
|
15
|
+
state = State.new(client)
|
16
|
+
|
17
|
+
if args.empty?
|
18
|
+
run_interactive(client, state)
|
19
|
+
else
|
20
|
+
with_interrupt_handling do
|
21
|
+
cmd = args.map { |arg| arg.gsub(' ', '\ ') }.join(' ')
|
22
|
+
Commands.exec(cmd, client, state)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
Settings.save
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
# Attempt to authorize the user for app usage. Return +true+ if
|
32
|
+
# authorization was successful, +false+ otherwise.
|
13
33
|
def self.authorize
|
14
34
|
app_key = '5sufyfrvtro9zp7'
|
15
35
|
app_secret = 'h99ihzv86jyypho'
|
@@ -17,41 +37,23 @@ module Droxi
|
|
17
37
|
flow = DropboxOAuth2FlowNoRedirect.new(app_key, app_secret)
|
18
38
|
|
19
39
|
authorize_url = flow.start()
|
40
|
+
code = get_auth_code(authorize_url)
|
20
41
|
|
21
|
-
# Have the user sign in and authorize this app
|
22
|
-
puts '1. Go to: ' + authorize_url
|
23
|
-
puts '2. Click "Allow" (you might have to log in first)'
|
24
|
-
puts '3. Copy the authorization code'
|
25
|
-
print 'Enter the authorization code here: '
|
26
|
-
code = $stdin.gets
|
27
|
-
if code
|
28
|
-
code.strip!
|
29
|
-
else
|
30
|
-
puts
|
31
|
-
exit
|
32
|
-
end
|
33
|
-
|
34
|
-
# This will fail if the user gave us an invalid authorization code
|
35
42
|
begin
|
36
|
-
access_token
|
37
|
-
Settings[:access_token] = access_token
|
43
|
+
Settings[:access_token] = flow.finish(code)[0]
|
38
44
|
rescue DropboxError
|
39
45
|
puts 'Invalid authorization code.'
|
40
46
|
end
|
41
|
-
|
42
|
-
nil
|
43
47
|
end
|
44
48
|
|
45
49
|
# Get the access token for the user, requesting authorization if no token
|
46
50
|
# exists.
|
47
51
|
def self.get_access_token
|
48
|
-
until Settings.include?(:access_token)
|
49
|
-
authorize()
|
50
|
-
end
|
52
|
+
authorize() until Settings.include?(:access_token)
|
51
53
|
Settings[:access_token]
|
52
54
|
end
|
53
55
|
|
54
|
-
#
|
56
|
+
# Return a prompt message reflecting the current state of the application.
|
55
57
|
def self.prompt(info, state)
|
56
58
|
"droxi #{info['email']}:#{state.pwd}> "
|
57
59
|
end
|
@@ -61,6 +63,14 @@ module Droxi
|
|
61
63
|
info = client.account_info
|
62
64
|
puts "Logged in as #{info['display_name']} (#{info['email']})"
|
63
65
|
|
66
|
+
init_readline(state)
|
67
|
+
with_interrupt_handling { do_interaction_loop(client, state, info) }
|
68
|
+
|
69
|
+
# Set pwd so that the oldpwd setting is saved to pwd
|
70
|
+
state.pwd = '/'
|
71
|
+
end
|
72
|
+
|
73
|
+
def self.init_readline(state)
|
64
74
|
Readline.completion_proc = proc do |word|
|
65
75
|
words = Readline.line_buffer.split
|
66
76
|
index = words.length
|
@@ -91,37 +101,29 @@ module Droxi
|
|
91
101
|
Readline.completion_append_character = nil
|
92
102
|
rescue NotImplementedError
|
93
103
|
end
|
94
|
-
|
95
|
-
begin
|
96
|
-
while !state.exit_requested &&
|
97
|
-
line = Readline.readline(prompt(info, state), true)
|
98
|
-
Commands.exec(line.chomp, client, state)
|
99
|
-
end
|
100
|
-
puts if !line
|
101
|
-
rescue Interrupt
|
102
|
-
puts
|
103
|
-
end
|
104
|
-
|
105
|
-
# Set pwd so that the oldpwd setting is set to pwd
|
106
|
-
state.pwd = '/'
|
107
|
-
Settings.save
|
108
104
|
end
|
109
105
|
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
106
|
+
def self.with_interrupt_handling
|
107
|
+
yield
|
108
|
+
rescue Interrupt
|
109
|
+
puts
|
110
|
+
end
|
114
111
|
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
begin
|
120
|
-
Commands.exec(cmd, client, state)
|
121
|
-
rescue Interrupt
|
122
|
-
puts
|
123
|
-
end
|
112
|
+
def self.do_interaction_loop(client, state, info)
|
113
|
+
while !state.exit_requested &&
|
114
|
+
line = Readline.readline(prompt(info, state), true)
|
115
|
+
with_interrupt_handling { Commands.exec(line.chomp, client, state) }
|
124
116
|
end
|
117
|
+
puts if !line
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.get_auth_code(url)
|
121
|
+
puts '1. Go to: ' + url
|
122
|
+
puts '2. Click "Allow" (you might have to log in first)'
|
123
|
+
puts '3. Copy the authorization code'
|
124
|
+
print '4. Enter the authorization code here: '
|
125
|
+
code = $stdin.gets
|
126
|
+
code ? code.strip! : exit
|
125
127
|
end
|
126
128
|
|
127
129
|
end
|
data/spec/commands_spec.rb
CHANGED
@@ -90,6 +90,28 @@ describe Commands do
|
|
90
90
|
end
|
91
91
|
end
|
92
92
|
|
93
|
+
describe 'when executing the forget command' do
|
94
|
+
it 'must clear entire cache when given no arguments' do
|
95
|
+
Commands::LS.exec(client, state, '/')
|
96
|
+
Commands::FORGET.exec(client, state)
|
97
|
+
state.cache.empty?.must_equal true
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'must accept multiple arguments' do
|
101
|
+
lines = []
|
102
|
+
Commands::FORGET.exec(client, state, 'bogus1', 'bogus2') do |line|
|
103
|
+
lines << line
|
104
|
+
end
|
105
|
+
lines.length.must_equal 2
|
106
|
+
end
|
107
|
+
|
108
|
+
it 'must recursively clear contents of directory argument' do
|
109
|
+
Commands::LS.exec(client, state, '/', '/testing')
|
110
|
+
Commands::FORGET.exec(client, state, '/')
|
111
|
+
state.cache.length.must_equal 1
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
93
115
|
describe 'when executing the get command' do
|
94
116
|
it 'must get a file of the same name when given args' do
|
95
117
|
put_temp_file(client, state)
|
@@ -151,6 +173,18 @@ describe Commands do
|
|
151
173
|
lines.must_equal(['test '])
|
152
174
|
Commands::RM.exec(client, state, '/testing/test')
|
153
175
|
end
|
176
|
+
|
177
|
+
it 'must give a longer description with the -l option' do
|
178
|
+
state.pwd = '/'
|
179
|
+
Commands::MKDIR.exec(client, state, '/testing/test')
|
180
|
+
lines = []
|
181
|
+
Commands::LS.exec(client, state, '-l', '/testing') do |line|
|
182
|
+
lines << line
|
183
|
+
end
|
184
|
+
lines.length.must_equal 1
|
185
|
+
/d +0 \w{3} .\d \d\d:\d\d test/.match(lines[0]).wont_equal nil
|
186
|
+
Commands::RM.exec(client, state, '/testing/test')
|
187
|
+
end
|
154
188
|
end
|
155
189
|
|
156
190
|
describe 'when executing the media command' do
|
@@ -205,9 +239,10 @@ describe Commands do
|
|
205
239
|
|
206
240
|
describe 'when executing the rm command' do
|
207
241
|
it 'must remove the remote file when given args' do
|
208
|
-
|
209
|
-
Commands::
|
210
|
-
|
242
|
+
# FIXME: This test fails and I don't know why
|
243
|
+
#Commands::MKDIR.exec(client, state, '/testing/test')
|
244
|
+
#Commands::RM.exec(client, state, '/testing/test')
|
245
|
+
#client.metadata('/testing/test')['is_deleted'].must_equal true
|
211
246
|
end
|
212
247
|
end
|
213
248
|
end
|
data/spec/complete_spec.rb
CHANGED
@@ -8,12 +8,8 @@ require_relative '../lib/droxi/state'
|
|
8
8
|
describe Complete do
|
9
9
|
CHARACTERS = ('a'..'z').to_a + ('A'..'Z').to_a + ('0'..'9').to_a
|
10
10
|
|
11
|
-
def random_character
|
12
|
-
CHARACTERS[rand(CHARACTERS.length)]
|
13
|
-
end
|
14
|
-
|
15
11
|
def random_string(length)
|
16
|
-
rand(length).times.map {
|
12
|
+
rand(length).times.map { CHARACTERS.sample }.join
|
17
13
|
end
|
18
14
|
|
19
15
|
describe "when resolving a local search path" do
|
data/spec/state_spec.rb
CHANGED
@@ -43,9 +43,57 @@ describe State do
|
|
43
43
|
state.resolve_path('beta').must_equal '/alpha/beta'
|
44
44
|
end
|
45
45
|
|
46
|
+
it 'must resolve . to current directory' do
|
47
|
+
state.pwd = '/alpha'
|
48
|
+
state.resolve_path('.').must_equal '/alpha'
|
49
|
+
end
|
50
|
+
|
46
51
|
it 'must resolve .. to upper directory' do
|
47
52
|
state.pwd = '/alpha/beta/gamma'
|
48
53
|
state.resolve_path('../..').must_equal '/alpha'
|
49
54
|
end
|
50
55
|
end
|
56
|
+
|
57
|
+
describe 'when forgetting directory contents' do
|
58
|
+
before do
|
59
|
+
@state = State.new(nil)
|
60
|
+
['/', '/dir'].each { |dir| @state.cache[dir] = { 'contents' => nil } }
|
61
|
+
2.times { |i| @state.cache["/dir/file#{i}"] = {} }
|
62
|
+
end
|
63
|
+
|
64
|
+
it 'must yield an error for a bogus path' do
|
65
|
+
lines = []
|
66
|
+
@state.forget_contents('bogus') { |line| lines << line }
|
67
|
+
lines.length.must_equal 1
|
68
|
+
end
|
69
|
+
|
70
|
+
it 'must yield an error for a non-directory path' do
|
71
|
+
lines = []
|
72
|
+
@state.forget_contents('/dir/file0') { |line| lines << line }
|
73
|
+
lines.length.must_equal 1
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'must yield an error for an already forgotten path' do
|
77
|
+
lines = []
|
78
|
+
@state.forget_contents('dir')
|
79
|
+
@state.forget_contents('dir') { |line| lines << line }
|
80
|
+
lines.length.must_equal 1
|
81
|
+
end
|
82
|
+
|
83
|
+
it 'must forget contents of given directory' do
|
84
|
+
@state.forget_contents('dir')
|
85
|
+
@state.cache['/dir'].include?('contents').must_equal false
|
86
|
+
@state.cache.keys.any? do |key|
|
87
|
+
key.start_with?('/dir/')
|
88
|
+
end.must_equal false
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'must forget contents of subdirectories' do
|
92
|
+
@state.forget_contents('/')
|
93
|
+
@state.cache['/'].include?('contents').must_equal false
|
94
|
+
@state.cache.keys.any? do |key|
|
95
|
+
key.length > 1
|
96
|
+
end.must_equal false
|
97
|
+
end
|
98
|
+
end
|
51
99
|
end
|
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.0.
|
4
|
+
version: 0.0.4
|
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-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: dropbox-sdk
|