droxi 0.0.0
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 +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: []
|