dbox 0.5.3 → 0.6.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.
@@ -0,0 +1,82 @@
1
+ module Dbox
2
+ module Utils
3
+ def times_equal?(t1, t2)
4
+ time_to_s(t1) == time_to_s(t2)
5
+ end
6
+
7
+ def time_to_s(t)
8
+ case t
9
+ when Time
10
+ # matches dropbox time format
11
+ t.utc.strftime("%a, %d %b %Y %H:%M:%S +0000")
12
+ when String
13
+ t
14
+ end
15
+ end
16
+
17
+ def parse_time(t)
18
+ case t
19
+ when Time
20
+ t
21
+ when String
22
+ Time.parse(t)
23
+ end
24
+ end
25
+
26
+ # assumes local_path is defined
27
+ def local_to_relative_path(path)
28
+ if path.include?(local_path)
29
+ path.sub(local_path, "").sub(/^\//, "")
30
+ else
31
+ raise BadPath, "Not a local path: #{path}"
32
+ end
33
+ end
34
+
35
+ # assumes remote_path is defined
36
+ def remote_to_relative_path(path)
37
+ if path.include?(remote_path)
38
+ path.sub(remote_path, "").sub(/^\//, "")
39
+ else
40
+ raise BadPath, "Not a remote path: #{path}"
41
+ end
42
+ end
43
+
44
+ # assumes local_path is defined
45
+ def relative_to_local_path(path)
46
+ if path && path.length > 0
47
+ File.join(local_path, path)
48
+ else
49
+ local_path
50
+ end
51
+ end
52
+
53
+ # assumes remote_path is defined
54
+ def relative_to_remote_path(path)
55
+ if path && path.length > 0
56
+ File.join(remote_path, path)
57
+ else
58
+ remote_path
59
+ end
60
+ end
61
+
62
+ def calculate_hash(filepath)
63
+ begin
64
+ Digest::MD5.file(filepath).to_s
65
+ rescue Errno::EISDIR
66
+ nil
67
+ rescue Errno::ENOENT
68
+ nil
69
+ end
70
+ end
71
+
72
+ def find_nonconflicting_path(filepath)
73
+ proposed = filepath
74
+ while File.exists?(proposed)
75
+ dir, p = File.split(proposed)
76
+ p = p.sub(/^(.*?)( \((\d+)\))?(\..*?)?$/) { "#{$1} (#{$3 ? $3.to_i + 1 : 1})#{$4}" }
77
+ proposed = File.join(dir, p)
78
+ end
79
+ proposed
80
+ end
81
+ end
82
+ end
@@ -122,6 +122,101 @@ describe Dbox do
122
122
  Dbox.pull(@local).should eql(:created => ["subdir", "subdir/one.txt"], :deleted => ["foo.txt"], :updated => ["", "baz.txt"], :failed => [])
123
123
  Dbox.pull(@local).should eql(:created => [], :deleted => [], :updated => [], :failed => [])
124
124
  end
125
+
126
+ it "should be able to download a bunch of files at the same time" do
127
+ Dbox.create(@remote, @local)
128
+ @alternate = "#{ALTERNATE_LOCAL_TEST_PATH}/#{@name}"
129
+ Dbox.clone(@remote, @alternate)
130
+
131
+ # generate 20 x 100kB files
132
+ 20.times do
133
+ make_file "#{@alternate}/#{randname}.txt", 100
134
+ end
135
+
136
+ Dbox.push(@alternate)
137
+
138
+ res = Dbox.pull(@local)
139
+ res[:deleted].should eql([])
140
+ res[:updated].should eql([""])
141
+ res[:failed].should eql([])
142
+ res[:created].size.should eql(20)
143
+ end
144
+
145
+ it "should be able to pull a series of updates to the same file" do
146
+ Dbox.create(@remote, @local)
147
+ @alternate = "#{ALTERNATE_LOCAL_TEST_PATH}/#{@name}"
148
+ Dbox.clone(@remote, @alternate)
149
+
150
+ make_file "#{@local}/hello.txt"
151
+ Dbox.push(@local)
152
+ Dbox.pull(@alternate).should eql(:created => ["hello.txt"], :deleted => [], :updated => [""], :failed => [])
153
+ make_file "#{@local}/hello.txt"
154
+ Dbox.push(@local)
155
+ Dbox.pull(@alternate).should eql(:created => [], :deleted => [], :updated => ["", "hello.txt"], :failed => [])
156
+ make_file "#{@local}/hello.txt"
157
+ Dbox.push(@local)
158
+ Dbox.pull(@alternate).should eql(:created => [], :deleted => [], :updated => ["", "hello.txt"], :failed => [])
159
+ end
160
+
161
+ it "should handle conflicting pulls of new files gracefully" do
162
+ Dbox.create(@remote, @local)
163
+ @alternate = "#{ALTERNATE_LOCAL_TEST_PATH}/#{@name}"
164
+ Dbox.clone(@remote, @alternate)
165
+
166
+ make_file "#{@local}/hello.txt"
167
+ Dbox.push(@local)
168
+
169
+ make_file "#{@alternate}/hello.txt"
170
+ Dbox.pull(@alternate).should eql(:created => ["hello.txt"], :deleted => [], :updated => [""], :conflicts => [{:original => "hello.txt", :renamed => "hello (1).txt"}], :failed => [])
171
+ end
172
+
173
+ it "should handle conflicting pulls of updated files gracefully" do
174
+ Dbox.create(@remote, @local)
175
+ @alternate = "#{ALTERNATE_LOCAL_TEST_PATH}/#{@name}"
176
+ Dbox.clone(@remote, @alternate)
177
+
178
+ make_file "#{@local}/hello.txt"
179
+ Dbox.push(@local)
180
+ Dbox.pull(@alternate).should eql(:created => ["hello.txt"], :deleted => [], :updated => [""], :failed => [])
181
+
182
+ make_file "#{@local}/hello.txt"
183
+ Dbox.push(@local)
184
+
185
+ make_file "#{@alternate}/hello.txt"
186
+ Dbox.pull(@alternate).should eql(:created => [], :deleted => [], :updated => ["", "hello.txt"], :conflicts => [{:original => "hello.txt", :renamed => "hello (1).txt"}], :failed => [])
187
+ end
188
+
189
+ it "should deal with all sorts of weird filenames when renaming due to conflicts on pull" do
190
+ Dbox.create(@remote, @local)
191
+ @alternate = "#{ALTERNATE_LOCAL_TEST_PATH}/#{@name}"
192
+ Dbox.clone(@remote, @alternate)
193
+
194
+ make_file "#{@local}/hello.txt"
195
+ make_file "#{@local}/hello (1).txt"
196
+ make_file "#{@local}/goodbye.txt"
197
+ Dbox.push(@local)
198
+
199
+ make_file "#{@alternate}/hello.txt"
200
+ make_file "#{@alternate}/hello (1).txt"
201
+ make_file "#{@alternate}/hello (3).txt"
202
+ make_file "#{@alternate}/hello (4).txt"
203
+ make_file "#{@alternate}/hello (test).txt"
204
+ make_file "#{@alternate}/goodbye.txt"
205
+ make_file "#{@alternate}/goodbye (1).txt"
206
+ make_file "#{@alternate}/goodbye (2).txt"
207
+ make_file "#{@alternate}/goodbye (3).txt"
208
+ make_file "#{@alternate}/goodbye ().txt"
209
+
210
+ # there's a race condition, so the output could be one of two things
211
+ res = Dbox.pull(@alternate)
212
+ res[:created].should eql(["goodbye.txt", "hello (1).txt", "hello.txt"])
213
+ res[:updated].should eql([""])
214
+ res[:deleted].should eql([])
215
+ res[:failed].should eql([])
216
+ c = (res[:conflicts] == [{:original => "goodbye.txt", :renamed => "goodbye (4).txt"}, {:original => "hello (1).txt", :renamed => "hello (5).txt"}, {:original => "hello.txt", :renamed => "hello (2).txt"}]) ||
217
+ (res[:conflicts] == [{:original => "goodbye.txt", :renamed => "goodbye (4).txt"}, {:original => "hello (1).txt", :renamed => "hello (2).txt"}, {:original => "hello.txt", :renamed => "hello (5).txt"}])
218
+ c.should be_true
219
+ end
125
220
  end
126
221
 
127
222
  describe "#push" do
@@ -230,6 +325,112 @@ describe Dbox do
230
325
  rm_rf @local
231
326
  Dbox.clone(@remote, @local).should eql(:created => [crazy_name1, File.join(crazy_name1, "foo.txt")], :deleted => [], :updated => [""], :failed => [])
232
327
  end
328
+
329
+ it "should be able to upload a bunch of files at the same time" do
330
+ Dbox.create(@remote, @local)
331
+
332
+ # generate 20 x 100kB files
333
+ 20.times do
334
+ make_file "#{@local}/#{randname}.txt", 100
335
+ end
336
+
337
+ res = Dbox.push(@local)
338
+ res[:deleted].should eql([])
339
+ res[:updated].should eql([])
340
+ res[:failed].should eql([])
341
+ res[:created].size.should eql(20)
342
+ end
343
+
344
+ it "should be able to push a series of updates to the same file" do
345
+ Dbox.create(@remote, @local)
346
+
347
+ make_file "#{@local}/hello.txt"
348
+ Dbox.push(@local).should eql(:created => ["hello.txt"], :deleted => [], :updated => [], :failed => [])
349
+ make_file "#{@local}/hello.txt"
350
+ Dbox.push(@local).should eql(:created => [], :deleted => [], :updated => ["hello.txt"], :failed => [])
351
+ make_file "#{@local}/hello.txt"
352
+ Dbox.push(@local).should eql(:created => [], :deleted => [], :updated => ["hello.txt"], :failed => [])
353
+ end
354
+
355
+ it "should handle conflicting pushes of new files gracefully" do
356
+ Dbox.create(@remote, @local)
357
+
358
+ @alternate = "#{ALTERNATE_LOCAL_TEST_PATH}/#{@name}"
359
+ Dbox.clone(@remote, @alternate)
360
+
361
+ make_file "#{@local}/hello.txt"
362
+ Dbox.push(@local).should eql(:created => ["hello.txt"], :deleted => [], :updated => [], :failed => [])
363
+
364
+ make_file "#{@alternate}/hello.txt"
365
+ Dbox.push(@alternate).should eql(:created => [], :deleted => [], :updated => [], :conflicts => [{:original => "hello.txt", :renamed => "hello (1).txt"}], :failed => [])
366
+ end
367
+
368
+ it "should handle conflicting pushes of updated files gracefully" do
369
+ Dbox.create(@remote, @local)
370
+ make_file "#{@local}/hello.txt"
371
+ Dbox.push(@local)
372
+
373
+ @alternate = "#{ALTERNATE_LOCAL_TEST_PATH}/#{@name}"
374
+ Dbox.clone(@remote, @alternate)
375
+
376
+ make_file "#{@local}/hello.txt"
377
+ Dbox.push(@local).should eql(:created => [], :deleted => [], :updated => ["hello.txt"], :failed => [])
378
+
379
+ make_file "#{@alternate}/hello.txt"
380
+ Dbox.push(@alternate).should eql(:created => [], :deleted => [], :updated => [], :conflicts => [{:original => "hello.txt", :renamed => "hello (1).txt"}], :failed => [])
381
+ end
382
+ end
383
+
384
+ describe "#sync" do
385
+ it "should fail if the local dir is missing" do
386
+ expect { Dbox.sync(@local) }.to raise_error(Dbox::DatabaseError)
387
+ end
388
+
389
+ it "should be able to sync basic changes" do
390
+ Dbox.create(@remote, @local)
391
+ @alternate = "#{ALTERNATE_LOCAL_TEST_PATH}/#{@name}"
392
+ Dbox.clone(@remote, @alternate)
393
+
394
+ Dbox.sync(@local).should eql(:pull => { :created => [], :deleted => [], :updated => [], :failed => [] },
395
+ :push => { :created => [], :deleted => [], :updated => [], :failed => [] })
396
+ Dbox.sync(@alternate).should eql(:pull => { :created => [], :deleted => [], :updated => [], :failed => [] },
397
+ :push => { :created => [], :deleted => [], :updated => [], :failed => [] })
398
+
399
+ make_file "#{@local}/hello.txt"
400
+ Dbox.sync(@local).should eql(:pull => { :created => [], :deleted => [], :updated => [], :failed => [] },
401
+ :push => { :created => ["hello.txt"], :deleted => [], :updated => [], :failed => [] })
402
+ Dbox.sync(@alternate).should eql(:pull => { :created => ["hello.txt"], :deleted => [], :updated => [""], :failed => [] },
403
+ :push => { :created => [], :deleted => [], :updated => [], :failed => [] })
404
+ end
405
+
406
+ it "should be able to sync complex changes" do
407
+ Dbox.create(@remote, @local)
408
+ @alternate = "#{ALTERNATE_LOCAL_TEST_PATH}/#{@name}"
409
+ Dbox.clone(@remote, @alternate)
410
+
411
+ Dbox.sync(@local).should eql(:pull => { :created => [], :deleted => [], :updated => [], :failed => [] },
412
+ :push => { :created => [], :deleted => [], :updated => [], :failed => [] })
413
+ Dbox.sync(@alternate).should eql(:pull => { :created => [], :deleted => [], :updated => [], :failed => [] },
414
+ :push => { :created => [], :deleted => [], :updated => [], :failed => [] })
415
+
416
+ make_file "#{@local}/hello.txt"
417
+ make_file "#{@local}/goodbye.txt"
418
+ make_file "#{@local}/so_long.txt"
419
+ make_file "#{@alternate}/hello.txt"
420
+ make_file "#{@alternate}/farewell.txt"
421
+ Dbox.sync(@local).should eql(:pull => { :created => [], :deleted => [], :updated => [], :failed => [] },
422
+ :push => { :created => ["goodbye.txt", "hello.txt", "so_long.txt"], :deleted => [], :updated => [], :failed => [] })
423
+ Dbox.sync(@alternate).should eql(:pull => { :created => ["goodbye.txt", "hello.txt", "so_long.txt"], :deleted => [], :updated => [""], :failed => [], :conflicts => [{ :renamed => "hello (1).txt", :original => "hello.txt" }] },
424
+ :push => { :created => ["farewell.txt", "hello (1).txt"], :deleted => [], :updated => [], :failed => [] })
425
+
426
+ make_file "#{@alternate}/farewell.txt"
427
+ make_file "#{@alternate}/goodbye.txt"
428
+ make_file "#{@alternate}/au_revoir.txt"
429
+ Dbox.sync(@alternate).should eql(:pull => { :created => [], :deleted => [], :updated => [""], :failed => [] },
430
+ :push => { :created => ["au_revoir.txt"], :deleted => [], :updated => ["farewell.txt", "goodbye.txt"], :failed => [] })
431
+ Dbox.sync(@local).should eql(:pull => { :created => ["au_revoir.txt", "farewell.txt", "hello (1).txt"], :deleted => [], :updated => ["", "goodbye.txt"], :failed => [] },
432
+ :push => { :created => [], :deleted => [], :updated => [], :failed => [] })
433
+ end
233
434
  end
234
435
 
235
436
  describe "#move" do
@@ -36,8 +36,8 @@ def log
36
36
  LOGGER
37
37
  end
38
38
 
39
- def make_file(filepath)
40
- File.open(filepath, "w") {|f| f << randname }
39
+ def make_file(filepath, size_in_kb=1)
40
+ `dd if=/dev/urandom of="#{filepath.gsub('"','\"')}" bs=1024 count=#{size_in_kb} 1>/dev/null 2>/dev/null`
41
41
  end
42
42
 
43
43
  RSpec::Matchers.define :exist do
@@ -0,0 +1,29 @@
1
+ 1.1 (2011-10-17)
2
+ * Various bug fixes found during beta period
3
+ * Improved documentation
4
+ * Improved module structure
5
+ * Removed dependency on the oauth module, using plaintext authentication over https
6
+
7
+ 1.0 (2011-8-16)
8
+ * Backwards compatibility broken
9
+ - Changed interface
10
+ - Change 'sandbox' references to 'app_folder'
11
+ * Updated SDK to Dropbox API Version 1, supporting all calls
12
+ - Added 'rev' parameter to metadata and get_file
13
+ - Added 'parent_rev' parameter to put_file
14
+ - Added search, share, media, revisions, and restore
15
+ - put_file uses /files_put instead of multipart POST
16
+ - Removed methods for calls that were removed from v1 of the REST API
17
+ * Changed return format for calls
18
+ - On error (non-200 response), an exception is raised
19
+ - On success, the JSON is parsed and a Hash is returned
20
+ * Updated examples
21
+ - Improved CLI example
22
+ - Added a Ruby on Rails 3 controller example
23
+ - Added a web based file browser/uploader that uses Sinatra
24
+ * put_file no longer takes a "name" arugment, only takes a full path
25
+ * Removed reliance on config files
26
+ * Assorted bugfixes and improvements
27
+ * All calls are now made over SSL
28
+ * Fully documented code for RDoc generation
29
+ * Added a CHANGELOG
@@ -1,4 +1,4 @@
1
- Copyright (c) 2009 Dropbox (Evenflow, Inc.), http://getdropbox.com/
1
+ Copyright (c) 2009-2011 Dropbox Inc., http://www.dropbox.com/
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
@@ -0,0 +1,7 @@
1
+ Getting started with the Dropbox Ruby SDK:
2
+ 1. Install json and oauth ruby gems with:
3
+ gem install json oauth
4
+ 2. Edit cli_example.rb to to include your APP_KEY and APP_SECRET
5
+ 3. Try running the example app: 'ruby clie_example.rb'.
6
+ 4. See dropbox_controller.rb for an example Ruby on Rails controller. It needs an APP_KEY and APP_SECRET set too.
7
+ 5. Check out the dropbox website for reference and help! http://dropbox.com/developers_beta
@@ -0,0 +1,197 @@
1
+ require './lib/dropbox_sdk'
2
+ require 'pp'
3
+
4
+ ####
5
+ # An example app using the Dropbox API Ruby Client
6
+ # This ruby script sets up a basic command line interface (CLI)
7
+ # that prompts a user to authenticate on the web, then
8
+ # allows them to type commands to manipulate their dropbox.
9
+ ####
10
+
11
+ # You must use your Dropbox App key and secret to use the API.
12
+ # Find this at https://www.dropbox.com/developers
13
+ APP_KEY = ''
14
+ APP_SECRET = ''
15
+ ACCESS_TYPE = :app_folder #The two valid values here are :app_folder and :dropbox
16
+ #The default is :app_folder, but your application might be
17
+ #set to have full :dropbox access. Check your app at
18
+ #https://www.dropbox.com/developers/apps
19
+
20
+ class DropboxCLI
21
+ LOGIN_REQUIRED = %w{put get cp mv rm ls mkdir info logout search}
22
+
23
+ def initialize
24
+ if APP_KEY == '' or APP_SECRET == ''
25
+ puts "You must set your APP_KEY and APP_SECRET in cli_example.rb!"
26
+ puts "Find this in your apps page at https://www.dropbox.com/developers/"
27
+ exit
28
+ end
29
+
30
+ @session = DropboxSession.new(APP_KEY, APP_SECRET)
31
+ @client = nil
32
+ end
33
+
34
+ def login
35
+ ########
36
+ # Instead of going to a authorize URL, you can set a access token key and secret
37
+ # from a previous session
38
+ ########
39
+ # @session.set_access_token('key', 'secret')
40
+
41
+ if @session.authorized?
42
+ puts "already logged in!"
43
+ else
44
+
45
+ # grab the request token for session
46
+ @session.get_request_token
47
+
48
+ authorize_url = @session.get_authorize_url
49
+ puts "Got a request token. Your request token key is #{@session.request_token.key} and your token secret is #{@session.request_token.secret}"
50
+
51
+ # make the user log in and authorize this token
52
+ puts "AUTHORIZING", authorize_url, "Please visit that web page and hit 'Allow', then hit Enter here."
53
+ gets
54
+
55
+ # get the access token from the server. Its then stored in the session.
56
+ @session.get_access_token
57
+
58
+ end
59
+ puts "You are logged in. Your access token key is #{@session.access_token.key} your secret is #{@session.access_token.secret}"
60
+ @client = DropboxClient.new(@session, ACCESS_TYPE)
61
+ end
62
+
63
+ def command_loop
64
+ puts "Enter a command or 'help' or 'exit'"
65
+ command_line = ''
66
+ while command_line.strip != 'exit'
67
+ begin
68
+ execute_dropbox_command(command_line)
69
+ rescue RuntimeError => e
70
+ puts "Command Line Error! #{e.class}: #{e}"
71
+ puts e.backtrace
72
+ end
73
+ print '> '
74
+ command_line = gets.strip
75
+ end
76
+ puts 'goodbye'
77
+ exit(0)
78
+ end
79
+
80
+ def execute_dropbox_command(cmd_line)
81
+ command = cmd_line.split
82
+ method = command.first
83
+ if LOGIN_REQUIRED.include? method
84
+ if @client
85
+ send(method.to_sym, command)
86
+ else
87
+ puts 'must be logged in; type \'login\' to get started.'
88
+ end
89
+ elsif ['login', 'help'].include? method
90
+ send(method.to_sym)
91
+ else
92
+ if command.first && !command.first.strip.empty?
93
+ puts 'invalid command. type \'help\' to see commands.'
94
+ end
95
+ end
96
+ end
97
+
98
+ def logout(command)
99
+ @session.clear_access_token
100
+ puts "You are logged out."
101
+ @client = nil
102
+ end
103
+
104
+ def put(command)
105
+ fname = command[1]
106
+
107
+ #If the user didn't specifiy the file name, just use the name of the file on disk
108
+ if command[2]
109
+ new_name = command[2]
110
+ else
111
+ new_name = File.basename(fname)
112
+ end
113
+
114
+ if fname && !fname.empty? && File.exists?(fname) && (File.ftype(fname) == 'file') && File.stat(fname).readable?
115
+ #This is where we call the the Dropbox Client
116
+ pp @client.put_file(new_name, open(fname))
117
+ else
118
+ puts "couldn't find the file #{ fname }"
119
+ end
120
+ end
121
+
122
+ def get(command)
123
+ dest = command[2]
124
+ if !command[1] || command[1].empty?
125
+ puts "please specify item to get"
126
+ elsif !dest || dest.empty?
127
+ puts "please specify full local path to dest, i.e. the file to write to"
128
+ elsif File.exists?(dest)
129
+ puts "error: File #{dest} already exists."
130
+ else
131
+ src = clean_up(command[1])
132
+ out = @client.get_file('/' + src)
133
+ open(dest, 'w'){|f| f.puts out }
134
+ puts "wrote file #{dest}."
135
+ end
136
+ end
137
+
138
+ def mkdir(command)
139
+ pp @client.file_create_folder(command[1])
140
+ end
141
+
142
+ def thumbnail(command)
143
+ command[2] ||= 'small'
144
+ pp @client.thumbnail(command[1], command[2])
145
+ end
146
+
147
+ def cp(command)
148
+ src = clean_up(command[1])
149
+ dest = clean_up(command[2])
150
+ pp @client.file_copy(src, dest)
151
+ end
152
+
153
+ def mv(command)
154
+ src = clean_up(command[1])
155
+ dest = clean_up(command[2])
156
+ pp @client.file_move(src, dest)
157
+ end
158
+
159
+ def rm(command)
160
+ pp @client.file_delete(clean_up(command[1]))
161
+ end
162
+
163
+ def search(command)
164
+ resp = @client.search('/',clean_up(command[1]))
165
+
166
+ for item in resp
167
+ puts item['path']
168
+ end
169
+ end
170
+
171
+ def info(command)
172
+ pp @client.account_info
173
+ end
174
+
175
+ def ls(command)
176
+ command[1] = '/' + clean_up(command[1] || '')
177
+ resp = @client.metadata(command[1])
178
+
179
+ if resp['contents'].length > 0
180
+ for item in resp['contents']
181
+ puts item['path']
182
+ end
183
+ end
184
+ end
185
+
186
+ def help
187
+ puts "commands are: login #{LOGIN_REQUIRED.join(' ')} help exit"
188
+ end
189
+
190
+ def clean_up(str)
191
+ return str.gsub(/^\/+/, '') if str
192
+ str
193
+ end
194
+ end
195
+
196
+ cli = DropboxCLI.new
197
+ cli.command_loop