droxi 0.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/bin/droxi +4 -0
- data/lib/droxi/commands.rb +329 -0
- data/lib/droxi/settings.rb +70 -0
- data/lib/droxi/state.rb +126 -0
- data/lib/droxi.rb +142 -0
- metadata +63 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c42bb596852aacff40852b70646a1a74c49930ff
|
4
|
+
data.tar.gz: a1cae76af97bc5064f2ee567035d5a50c6e768c9
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 1a4b91bca5b16d9bc98b3ab37fde5e5404ec86eb8ffc0f28426657a6555882efa34849b020c410205665044a4e2abe34bce85928e011d5d13b4417a2debe2486
|
7
|
+
data.tar.gz: b5fa9257afa11b305124d823efa38dc32c3c8fffc577a094ce6f0b249bccdce1d89f8f95cf4e28114da916a074782c94a96503be0b84d8ec4449c44fe42436b5
|
data/bin/droxi
ADDED
@@ -0,0 +1,329 @@
|
|
1
|
+
require 'readline'
|
2
|
+
|
3
|
+
module Commands
|
4
|
+
class UsageError < ArgumentError
|
5
|
+
end
|
6
|
+
|
7
|
+
class Command
|
8
|
+
attr_reader :usage, :description
|
9
|
+
|
10
|
+
def initialize(usage, description, procedure)
|
11
|
+
@usage = usage
|
12
|
+
@description = description.squeeze(' ')
|
13
|
+
@procedure = procedure
|
14
|
+
end
|
15
|
+
|
16
|
+
def exec(client, state, *args)
|
17
|
+
if num_args_ok?(args.length)
|
18
|
+
block = proc { |line| yield line if block_given? }
|
19
|
+
@procedure.yield(client, state, args, block)
|
20
|
+
else
|
21
|
+
raise UsageError.new(@usage)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def num_args_ok?(num_args)
|
26
|
+
args = @usage.split.drop(1)
|
27
|
+
min_args = args.reject { |arg| arg.start_with?('[') }.length
|
28
|
+
if args.last.end_with?('...')
|
29
|
+
max_args = num_args
|
30
|
+
else
|
31
|
+
max_args = args.length
|
32
|
+
end
|
33
|
+
(min_args..max_args).include?(num_args)
|
34
|
+
end
|
35
|
+
|
36
|
+
def type_of_arg(index)
|
37
|
+
args = @usage.split.drop(1)
|
38
|
+
index = [index, args.length - 1].min
|
39
|
+
args[index].tr('[].', '')
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
CD = Command.new(
|
44
|
+
'cd [REMOTE_DIR]',
|
45
|
+
"Change the remote working directory. With no arguments, changes to the \
|
46
|
+
Dropbox root. With a remote directory name as the argument, changes to \
|
47
|
+
that directory. With - as the argument, changes to the previous working \
|
48
|
+
directory.",
|
49
|
+
lambda do |client, state, args, output|
|
50
|
+
if args.empty?
|
51
|
+
state.pwd = '/'
|
52
|
+
elsif args[0] == '-'
|
53
|
+
state.pwd = state.oldpwd
|
54
|
+
else
|
55
|
+
path = state.resolve_path(args[0])
|
56
|
+
if state.is_dir?(client, path)
|
57
|
+
state.pwd = path
|
58
|
+
else
|
59
|
+
output.call('Not a directory')
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
)
|
64
|
+
|
65
|
+
GET = Command.new(
|
66
|
+
'get REMOTE_FILE...',
|
67
|
+
"Download each specified remote file to a file of the same name in the \
|
68
|
+
local working directory.",
|
69
|
+
lambda do |client, state, args, output|
|
70
|
+
state.expand_patterns(client, args).each do |path|
|
71
|
+
begin
|
72
|
+
contents = client.get_file(path)
|
73
|
+
File.open(File.basename(path), 'wb') do |file|
|
74
|
+
file.write(contents)
|
75
|
+
end
|
76
|
+
rescue DropboxError => error
|
77
|
+
output.call(error.to_s)
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
)
|
82
|
+
|
83
|
+
HELP = Command.new(
|
84
|
+
'help [COMMAND]',
|
85
|
+
"Print usage and help information about a command. If no command is \
|
86
|
+
given, print a list of commands instead.",
|
87
|
+
lambda do |client, state, args, output|
|
88
|
+
if args.empty?
|
89
|
+
table_output(NAMES).each { |line| output.call(line) }
|
90
|
+
else
|
91
|
+
cmd_name = args[0]
|
92
|
+
if NAMES.include?(cmd_name)
|
93
|
+
cmd = const_get(cmd_name.upcase.to_s)
|
94
|
+
output.call(cmd.usage)
|
95
|
+
wrap_output(cmd.description).each { |line| output.call(line) }
|
96
|
+
else
|
97
|
+
output.call("Unrecognized command: #{cmd_name}")
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
101
|
+
)
|
102
|
+
|
103
|
+
LCD = Command.new(
|
104
|
+
'lcd [LOCAL_DIR]',
|
105
|
+
"Change the local working directory. With no arguments, changes to the \
|
106
|
+
home directory. With a local directory name as the argument, changes to \
|
107
|
+
that directory. With - as the argument, changes to the previous working \
|
108
|
+
directory.",
|
109
|
+
lambda do |client, state, args, output|
|
110
|
+
path = if args.empty?
|
111
|
+
File.expand_path('~')
|
112
|
+
elsif args[0] == '-'
|
113
|
+
state.local_oldpwd
|
114
|
+
else
|
115
|
+
File.expand_path(args[0])
|
116
|
+
end
|
117
|
+
|
118
|
+
if Dir.exists?(path)
|
119
|
+
state.local_oldpwd = Dir.pwd
|
120
|
+
Dir.chdir(path)
|
121
|
+
else
|
122
|
+
output.call("lcd: #{args[0]}: No such file or directory")
|
123
|
+
end
|
124
|
+
end
|
125
|
+
)
|
126
|
+
|
127
|
+
LS = Command.new(
|
128
|
+
'ls [REMOTE_FILE]...',
|
129
|
+
"List information about remote files. With no arguments, list the \
|
130
|
+
contents of the working directory. When given remote directories as \
|
131
|
+
arguments, list the contents of the directories. When given remote files \
|
132
|
+
as arguments, list the files.",
|
133
|
+
lambda do |client, state, args, output|
|
134
|
+
patterns = if args.empty?
|
135
|
+
["#{state.pwd}/*".sub('//', '/')]
|
136
|
+
else
|
137
|
+
args.map do |path|
|
138
|
+
path = state.resolve_path(path)
|
139
|
+
begin
|
140
|
+
if state.is_dir?(client, path)
|
141
|
+
"#{path}/*".sub('//', '/')
|
142
|
+
else
|
143
|
+
path
|
144
|
+
end
|
145
|
+
rescue DropboxError
|
146
|
+
path
|
147
|
+
end
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
items = []
|
152
|
+
patterns.each do |pattern|
|
153
|
+
begin
|
154
|
+
dir = File.dirname(pattern)
|
155
|
+
state.contents(client, dir).each do |path|
|
156
|
+
items << File.basename(path) if File.fnmatch(pattern, path)
|
157
|
+
end
|
158
|
+
rescue DropboxError => error
|
159
|
+
output.call(error.to_s)
|
160
|
+
end
|
161
|
+
end
|
162
|
+
table_output(items).each { |item| output.call(item) }
|
163
|
+
end
|
164
|
+
)
|
165
|
+
|
166
|
+
MKDIR = Command.new(
|
167
|
+
'mkdir REMOTE_DIR...',
|
168
|
+
"Create remote directories.",
|
169
|
+
lambda do |client, state, args, output|
|
170
|
+
args.each do |arg|
|
171
|
+
begin
|
172
|
+
path = state.resolve_path(arg)
|
173
|
+
state.cache[path] = client.file_create_folder(path)
|
174
|
+
rescue DropboxError => error
|
175
|
+
output.call(error.to_s)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
end
|
179
|
+
)
|
180
|
+
|
181
|
+
PUT = Command.new(
|
182
|
+
'put LOCAL_FILE [REMOTE_FILE]',
|
183
|
+
"Upload a local file to a remote path. If a remote file of the same name \
|
184
|
+
already exists, Dropbox will rename the upload. When given only a local \
|
185
|
+
file path, the remote path defaults to a file of the same name in the \
|
186
|
+
remote working directory.",
|
187
|
+
lambda do |client, state, args, output|
|
188
|
+
from_path = args[0]
|
189
|
+
if args.length == 2
|
190
|
+
to_path = args[1]
|
191
|
+
else
|
192
|
+
to_path = File.basename(from_path)
|
193
|
+
end
|
194
|
+
to_path = state.resolve_path(to_path)
|
195
|
+
|
196
|
+
begin
|
197
|
+
File.open(File.expand_path(from_path), 'rb') do |file|
|
198
|
+
state.cache[to_path] = client.put_file(to_path, file)
|
199
|
+
end
|
200
|
+
rescue Exception => error
|
201
|
+
output.call(error.to_s)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
)
|
205
|
+
|
206
|
+
RM = Command.new(
|
207
|
+
'rm REMOTE_FILE...',
|
208
|
+
"Remove each specified remote file or directory.",
|
209
|
+
lambda do |client, state, args, output|
|
210
|
+
state.expand_patterns(client, args).each do |path|
|
211
|
+
begin
|
212
|
+
client.file_delete(path)
|
213
|
+
state.cache.delete(path)
|
214
|
+
rescue DropboxError => error
|
215
|
+
output.call(error.to_s)
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
)
|
220
|
+
|
221
|
+
SHARE = Command.new(
|
222
|
+
'share REMOTE_FILE...',
|
223
|
+
"Get URLs to share remote files. Shareable links created on Dropbox are \
|
224
|
+
time-limited, but don't require any authentication, so they can be given \
|
225
|
+
out freely. The time limit should allow at least a day of shareability.",
|
226
|
+
lambda do |client, state, args, output|
|
227
|
+
state.expand_patterns(client, args).each do |path|
|
228
|
+
begin
|
229
|
+
output.call("#{path}: #{client.shares(path)['url']}")
|
230
|
+
rescue DropboxError => error
|
231
|
+
output.call(error.to_s)
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
)
|
236
|
+
|
237
|
+
NAMES = constants.select do |sym|
|
238
|
+
const_get(sym).is_a?(Command)
|
239
|
+
end.map { |sym| sym.to_s.downcase }
|
240
|
+
|
241
|
+
def self.exec(input, client, state)
|
242
|
+
if input.start_with?('!')
|
243
|
+
shell(input[1, input.length - 1]) { |line| puts line }
|
244
|
+
elsif not input.empty?
|
245
|
+
tokens = input.split
|
246
|
+
|
247
|
+
# Escape spaces with backslash
|
248
|
+
i = 0
|
249
|
+
while i < tokens.length - 1
|
250
|
+
if tokens[i].end_with?('\\')
|
251
|
+
tokens[i] = "#{tokens[i].chop} #{tokens.delete_at(i + 1)}"
|
252
|
+
else
|
253
|
+
i += 1
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
cmd, args = tokens[0], tokens.drop(1)
|
258
|
+
|
259
|
+
if NAMES.include?(cmd)
|
260
|
+
begin
|
261
|
+
const_get(cmd.upcase.to_sym).exec(client, state, *args) do |line|
|
262
|
+
puts line
|
263
|
+
end
|
264
|
+
rescue UsageError => error
|
265
|
+
puts "Usage: #{error}"
|
266
|
+
end
|
267
|
+
else
|
268
|
+
puts "Unrecognized command: #{cmd}"
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
|
273
|
+
private
|
274
|
+
|
275
|
+
def self.get_screen_size
|
276
|
+
begin
|
277
|
+
Readline.get_screen_size[1]
|
278
|
+
rescue NotImplementedError
|
279
|
+
72
|
280
|
+
end
|
281
|
+
end
|
282
|
+
|
283
|
+
def self.shell(cmd)
|
284
|
+
begin
|
285
|
+
IO.popen(cmd) do |pipe|
|
286
|
+
pipe.each_line { |line| yield line.chomp if block_given? }
|
287
|
+
end
|
288
|
+
rescue Interrupt
|
289
|
+
rescue Exception => error
|
290
|
+
yield error.to_s if block_given?
|
291
|
+
end
|
292
|
+
end
|
293
|
+
|
294
|
+
def self.table_output(items)
|
295
|
+
return [] if items.empty?
|
296
|
+
columns = get_screen_size
|
297
|
+
item_width = items.map { |item| item.length }.max + 2
|
298
|
+
column = 0
|
299
|
+
lines = ['']
|
300
|
+
items.each do |item|
|
301
|
+
if column != 0 && column + item_width >= columns
|
302
|
+
lines << ''
|
303
|
+
column = 0
|
304
|
+
end
|
305
|
+
lines.last << item.ljust(item_width)
|
306
|
+
column += item_width
|
307
|
+
end
|
308
|
+
lines
|
309
|
+
end
|
310
|
+
|
311
|
+
def self.wrap_output(text)
|
312
|
+
columns = get_screen_size
|
313
|
+
column = 0
|
314
|
+
lines = ['']
|
315
|
+
text.split.each do |word|
|
316
|
+
if column != 0 && column + word.length >= columns
|
317
|
+
lines << ''
|
318
|
+
column = 0
|
319
|
+
end
|
320
|
+
if column != 0
|
321
|
+
lines.last << ' '
|
322
|
+
column += 1
|
323
|
+
end
|
324
|
+
lines.last << word
|
325
|
+
column += word.length
|
326
|
+
end
|
327
|
+
lines
|
328
|
+
end
|
329
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
require 'fileutils'
|
2
|
+
|
3
|
+
CONFIG_FILE_PATH = File.expand_path('~/.config/droxi/droxirc')
|
4
|
+
|
5
|
+
class Settings
|
6
|
+
def Settings.[](key)
|
7
|
+
@@settings[key]
|
8
|
+
end
|
9
|
+
|
10
|
+
def Settings.[]=(key, value)
|
11
|
+
if value != @@settings[key]
|
12
|
+
@@dirty = true
|
13
|
+
@@settings[key] = value
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def Settings.include?(key)
|
18
|
+
@@settings.include?(key)
|
19
|
+
end
|
20
|
+
|
21
|
+
def Settings.delete(key)
|
22
|
+
if @@settings.include?(key)
|
23
|
+
@@dirty = true
|
24
|
+
@@settings.delete(key)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def Settings.write
|
29
|
+
if @@dirty
|
30
|
+
@@dirty = false
|
31
|
+
FileUtils.mkdir_p(File.dirname(CONFIG_FILE_PATH))
|
32
|
+
File.open(CONFIG_FILE_PATH, 'w') do |file|
|
33
|
+
@@settings.each_pair { |k, v| file.write("#{k}=#{v}\n") }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def Settings.warn_invalid(line)
|
41
|
+
warn "invalid setting: #{line}"
|
42
|
+
{}
|
43
|
+
end
|
44
|
+
|
45
|
+
def Settings.parse(line)
|
46
|
+
if /^(.+?)=(.+)$/ =~ line
|
47
|
+
key, value = $1.to_sym, $2
|
48
|
+
if [:access_token, :oldpwd].include?(key)
|
49
|
+
{key => value}
|
50
|
+
else
|
51
|
+
warn_invalid(line)
|
52
|
+
end
|
53
|
+
else
|
54
|
+
warn_invalid(line)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def Settings.read
|
59
|
+
if File.exists?(CONFIG_FILE_PATH)
|
60
|
+
File.open(CONFIG_FILE_PATH) do |file|
|
61
|
+
file.each_line.reduce({}) { |a, e| a.merge(parse(e.strip)) }
|
62
|
+
end
|
63
|
+
else
|
64
|
+
{}
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
@@settings = read
|
69
|
+
@@dirty = false
|
70
|
+
end
|
data/lib/droxi/state.rb
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
require_relative 'settings'
|
2
|
+
|
3
|
+
class State
|
4
|
+
attr_reader :oldpwd, :pwd, :cache
|
5
|
+
attr_accessor :local_oldpwd
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@pwd = '/'
|
9
|
+
@oldpwd = Settings[:oldpwd] || '/'
|
10
|
+
@local_oldpwd = Dir.pwd
|
11
|
+
@cache = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def have_all_info_for(path)
|
15
|
+
@cache.include?(path) &&
|
16
|
+
(@cache[path].include?('contents') || !@cache[path]['is_dir'])
|
17
|
+
end
|
18
|
+
|
19
|
+
def metadata(client, path)
|
20
|
+
tokens = path.split('/').drop(1)
|
21
|
+
|
22
|
+
for i in 0..tokens.length
|
23
|
+
partial_path = '/' + tokens.take(i).join('/')
|
24
|
+
unless have_all_info_for(partial_path)
|
25
|
+
begin
|
26
|
+
data = @cache[partial_path] = client.metadata(partial_path)
|
27
|
+
rescue DropboxError
|
28
|
+
return
|
29
|
+
end
|
30
|
+
if data.include?('contents')
|
31
|
+
data['contents'].each do |datum|
|
32
|
+
@cache[datum['path']] = datum
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
@cache[path]
|
39
|
+
end
|
40
|
+
|
41
|
+
def contents(client, path)
|
42
|
+
metadata(client, path)
|
43
|
+
path = "#{path}/".sub('//', '/')
|
44
|
+
@cache.keys.select do |key|
|
45
|
+
key.start_with?(path) && key != path && !key.sub(path, '').include?('/')
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
def is_dir?(client, path)
|
50
|
+
metadata(client, File.dirname(path))
|
51
|
+
@cache.include?(path) && @cache[path]['is_dir']
|
52
|
+
end
|
53
|
+
|
54
|
+
def pwd=(value)
|
55
|
+
@oldpwd, @pwd = @pwd, value
|
56
|
+
Settings[:oldpwd] = @oldpwd
|
57
|
+
end
|
58
|
+
|
59
|
+
def resolve_path(path)
|
60
|
+
path = "#{@pwd}/#{path}" unless path.start_with?('/')
|
61
|
+
path.gsub!('//', '/')
|
62
|
+
while path.sub!(/\/([^\/]+?)\/\.\./, '')
|
63
|
+
end
|
64
|
+
path.chomp!('/')
|
65
|
+
path = '/' if path.empty?
|
66
|
+
path
|
67
|
+
end
|
68
|
+
|
69
|
+
def expand_patterns(client, patterns)
|
70
|
+
patterns.map do |pattern|
|
71
|
+
final_pattern = resolve_path(pattern)
|
72
|
+
|
73
|
+
matches = []
|
74
|
+
client.metadata(File.dirname(final_pattern))['contents'].each do |data|
|
75
|
+
path = data['path']
|
76
|
+
matches << path if File.fnmatch(final_pattern, path)
|
77
|
+
end
|
78
|
+
|
79
|
+
if matches.empty?
|
80
|
+
[final_pattern]
|
81
|
+
else
|
82
|
+
matches
|
83
|
+
end
|
84
|
+
end.flatten
|
85
|
+
end
|
86
|
+
|
87
|
+
def file_complete(client, word)
|
88
|
+
tab_complete(client, word, false)
|
89
|
+
end
|
90
|
+
|
91
|
+
def dir_complete(client, word)
|
92
|
+
tab_complete(client, word, true)
|
93
|
+
end
|
94
|
+
|
95
|
+
private
|
96
|
+
|
97
|
+
def complete(path, prefix_length, dir_only)
|
98
|
+
@cache.keys.select do |key|
|
99
|
+
key.start_with?(path) && key != path &&
|
100
|
+
!(dir_only && !@cache[key]['is_dir'])
|
101
|
+
end.map do |key|
|
102
|
+
if @cache[key]['is_dir']
|
103
|
+
key += '/'
|
104
|
+
else
|
105
|
+
key += ' '
|
106
|
+
end
|
107
|
+
key[prefix_length, key.length]
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
def tab_complete(client, word, dir_only)
|
112
|
+
path = resolve_path(word)
|
113
|
+
prefix_length = path.length - word.length
|
114
|
+
|
115
|
+
if word.end_with?('/')
|
116
|
+
# Treat word as directory
|
117
|
+
metadata(client, path)
|
118
|
+
prefix_length += 1
|
119
|
+
else
|
120
|
+
# Treat word as file
|
121
|
+
metadata(client, File.dirname(path))
|
122
|
+
end
|
123
|
+
|
124
|
+
complete(path, prefix_length, dir_only)
|
125
|
+
end
|
126
|
+
end
|
data/lib/droxi.rb
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
require 'dropbox_sdk'
|
2
|
+
require 'readline'
|
3
|
+
|
4
|
+
require_relative 'droxi/commands'
|
5
|
+
require_relative 'droxi/settings'
|
6
|
+
require_relative 'droxi/state'
|
7
|
+
|
8
|
+
module Droxi
|
9
|
+
APP_KEY = '5sufyfrvtro9zp7'
|
10
|
+
APP_SECRET = 'h99ihzv86jyypho'
|
11
|
+
|
12
|
+
def self.authorize
|
13
|
+
flow = DropboxOAuth2FlowNoRedirect.new(APP_KEY, APP_SECRET)
|
14
|
+
|
15
|
+
authorize_url = flow.start()
|
16
|
+
|
17
|
+
# Have the user sign in and authorize this app
|
18
|
+
puts '1. Go to: ' + authorize_url
|
19
|
+
puts '2. Click "Allow" (you might have to log in first)'
|
20
|
+
puts '3. Copy the authorization code'
|
21
|
+
print 'Enter the authorization code here: '
|
22
|
+
code = gets.strip
|
23
|
+
|
24
|
+
# This will fail if the user gave us an invalid authorization code
|
25
|
+
begin
|
26
|
+
access_token, user_id = flow.finish(code)
|
27
|
+
Settings[:access_token] = access_token
|
28
|
+
rescue DropboxError
|
29
|
+
puts 'Invalid authorization code.'
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def self.get_access_token
|
34
|
+
until Settings.include?(:access_token)
|
35
|
+
authorize()
|
36
|
+
end
|
37
|
+
Settings[:access_token]
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.prompt(info, state)
|
41
|
+
"droxi #{info['email']}:#{state.pwd}> "
|
42
|
+
end
|
43
|
+
|
44
|
+
def self.file_complete(word, dir_only=false)
|
45
|
+
begin
|
46
|
+
path = File.expand_path(word)
|
47
|
+
rescue ArgumentError
|
48
|
+
return []
|
49
|
+
end
|
50
|
+
if word.empty? || (word.length > 1 && word.end_with?('/'))
|
51
|
+
dir = path
|
52
|
+
else
|
53
|
+
dir = File.dirname(path)
|
54
|
+
end
|
55
|
+
Dir.entries(dir).map do |file|
|
56
|
+
(dir + '/').sub('//', '/') + file
|
57
|
+
end.select do |file|
|
58
|
+
file.start_with?(path) && !(dir_only && !File.directory?(file))
|
59
|
+
end.map do |file|
|
60
|
+
if File.directory?(file)
|
61
|
+
file << '/'
|
62
|
+
else
|
63
|
+
file << ' '
|
64
|
+
end
|
65
|
+
if word.start_with?('/')
|
66
|
+
file
|
67
|
+
elsif word.start_with?('~')
|
68
|
+
file.sub(/\/home\/[^\/]+/, '~')
|
69
|
+
else
|
70
|
+
file.sub(Dir.pwd + '/', '')
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def self.dir_complete(word)
|
76
|
+
file_complete(word, true)
|
77
|
+
end
|
78
|
+
|
79
|
+
def self.run
|
80
|
+
client = DropboxClient.new(get_access_token)
|
81
|
+
info = client.account_info
|
82
|
+
puts "Logged in as #{info['display_name']} (#{info['email']})"
|
83
|
+
|
84
|
+
state = State.new
|
85
|
+
|
86
|
+
Readline.completion_proc = proc do |word|
|
87
|
+
words = Readline.line_buffer.split
|
88
|
+
index = words.length
|
89
|
+
index += 1 if Readline.line_buffer.end_with?(' ')
|
90
|
+
if index <= 1
|
91
|
+
type = 'COMMAND'
|
92
|
+
elsif Commands::NAMES.include?(words[0])
|
93
|
+
cmd = Commands.const_get(words[0].upcase.to_sym)
|
94
|
+
type = cmd.type_of_arg(index - 2)
|
95
|
+
end
|
96
|
+
|
97
|
+
options = case type
|
98
|
+
when 'COMMAND'
|
99
|
+
Commands::NAMES.select { |name| name.start_with? word }.map do |name|
|
100
|
+
name + ' '
|
101
|
+
end
|
102
|
+
when 'LOCAL_FILE'
|
103
|
+
file_complete(word)
|
104
|
+
when 'LOCAL_DIR'
|
105
|
+
dir_complete(word)
|
106
|
+
when 'REMOTE_FILE'
|
107
|
+
begin
|
108
|
+
state.file_complete(client, word)
|
109
|
+
rescue DropboxError
|
110
|
+
[]
|
111
|
+
end
|
112
|
+
when 'REMOTE_DIR'
|
113
|
+
begin
|
114
|
+
state.dir_complete(client, word)
|
115
|
+
rescue DropboxError
|
116
|
+
[]
|
117
|
+
end
|
118
|
+
else
|
119
|
+
[]
|
120
|
+
end
|
121
|
+
|
122
|
+
options.map { |option| option.gsub(' ', '\ ').sub(/\\ $/, ' ') }
|
123
|
+
end
|
124
|
+
|
125
|
+
begin
|
126
|
+
Readline.completion_append_character = nil
|
127
|
+
rescue NotImplementedError
|
128
|
+
end
|
129
|
+
|
130
|
+
begin
|
131
|
+
while line = Readline.readline(prompt(info, state), true)
|
132
|
+
Commands.exec(line.chomp, client, state)
|
133
|
+
end
|
134
|
+
puts
|
135
|
+
rescue Interrupt
|
136
|
+
puts
|
137
|
+
end
|
138
|
+
|
139
|
+
state.pwd = '/'
|
140
|
+
Settings.write
|
141
|
+
end
|
142
|
+
end
|
metadata
ADDED
@@ -0,0 +1,63 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: droxi
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Brandon Mulcahy
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2014-06-01 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: dropbox_sdk
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - ">="
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '0'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - ">="
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '0'
|
27
|
+
description: ftp-like command-line interface to Dropbox
|
28
|
+
email: brandon@jangler.info
|
29
|
+
executables:
|
30
|
+
- droxi
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- bin/droxi
|
35
|
+
- lib/droxi.rb
|
36
|
+
- lib/droxi/commands.rb
|
37
|
+
- lib/droxi/settings.rb
|
38
|
+
- lib/droxi/state.rb
|
39
|
+
homepage: https://github.com/jangler/droxi
|
40
|
+
licenses:
|
41
|
+
- MIT
|
42
|
+
metadata: {}
|
43
|
+
post_install_message:
|
44
|
+
rdoc_options: []
|
45
|
+
require_paths:
|
46
|
+
- lib
|
47
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
48
|
+
requirements:
|
49
|
+
- - ">="
|
50
|
+
- !ruby/object:Gem::Version
|
51
|
+
version: '0'
|
52
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
version: '0'
|
57
|
+
requirements: []
|
58
|
+
rubyforge_project:
|
59
|
+
rubygems_version: 2.2.2
|
60
|
+
signing_key:
|
61
|
+
specification_version: 4
|
62
|
+
summary: droxi
|
63
|
+
test_files: []
|