droxi 0.0.0

Sign up to get free protection for your applications and to get access to all the features.
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,4 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'droxi'
4
+ Droxi.run
@@ -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
@@ -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: []