dbox 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.md CHANGED
@@ -62,42 +62,42 @@ $ DROPBOX_AUTH_KEY=v4d7l1rez1czksn DROPBOX_AUTH_SECRET=pqej9rmnj0i1gcxr4 dbox ..
62
62
  * This auth token will last for **10 years**, or when you choose to invalidate it, whichever comes first. So you really only need to do this once, and then keep them around.
63
63
 
64
64
 
65
- Usage
66
- -----
65
+ Using dbox from the Command-Line
66
+ --------------------------------
67
67
 
68
- ### Authorize
68
+ ### Usage
69
+
70
+ #### Authorize
69
71
 
70
72
  ```sh
71
73
  $ dbox authorize
72
74
  ```
73
75
 
74
- ### Create a new Dropbox folder
76
+ #### Create a new Dropbox folder
75
77
 
76
78
  ```sh
77
79
  $ dbox create <remote_path> [<local_path>]
78
80
  ```
79
81
 
80
- ### Clone an existing Dropbox folder
82
+ #### Clone an existing Dropbox folder
81
83
 
82
84
  ```sh
83
85
  $ dbox clone <remote_path> [<local_path>]
84
86
  ```
85
87
 
86
- ### Pull (download changes from Dropbox)
88
+ #### Pull (download changes from Dropbox)
87
89
 
88
90
  ```sh
89
91
  $ dbox pull [<local_path>]
90
92
  ```
91
93
 
92
- ### Push (upload changes to Dropbox)
94
+ #### Push (upload changes to Dropbox)
93
95
 
94
96
  ```sh
95
97
  $ dbox push [<local_path>]
96
98
  ```
97
99
 
98
-
99
- Example
100
- -------
100
+ #### Example
101
101
 
102
102
  ```sh
103
103
  $ export DROPBOX_APP_KEY=cmlrrjd3j0gbend
@@ -136,3 +136,75 @@ $ dbox pull
136
136
  $ cat hello.txt
137
137
  Oh, Hello
138
138
  ```
139
+
140
+ Using dbox from Ruby
141
+ --------------------
142
+
143
+ ### Usage
144
+
145
+ #### Setup
146
+
147
+ * Authorize beforehand with the command-line tool
148
+
149
+ ```ruby
150
+ require "dbox"
151
+ ```
152
+
153
+ #### Create a new Dropbox folder
154
+
155
+ ```ruby
156
+ Dbox.create(remote_path, local_path)
157
+ ```
158
+
159
+ #### Clone an existing Dropbox folder
160
+
161
+ ```ruby
162
+ Dbox.clone(remote_path, local_path)
163
+ ```
164
+
165
+ #### Pull (download changes from Dropbox)
166
+
167
+ ```ruby
168
+ Dbox.pull(local_path)
169
+ ```
170
+
171
+ #### Push (upload changes to Dropbox)
172
+
173
+ ```ruby
174
+ Dbox.push(local_path)
175
+ ```
176
+
177
+ #### Example
178
+
179
+ ```sh
180
+ $ export DROPBOX_APP_KEY=cmlrrjd3j0gbend
181
+ $ export DROPBOX_APP_SECRET=uvuulp75xf9jffl
182
+ ```
183
+
184
+ ```sh
185
+ $ dbox authorize
186
+ ```
187
+
188
+ ```sh
189
+ $ open http://www.dropbox.com/0/oauth/authorize?oauth_token=aaoeuhtns123456
190
+ ```
191
+
192
+ ```sh
193
+ $ export DROPBOX_AUTH_KEY=v4d7l1rez1czksn
194
+ $ export DROPBOX_AUTH_SECRET=pqej9rmnj0i1gcxr4
195
+ ```
196
+
197
+ ```ruby
198
+ > require "dbox"
199
+ > Dbox.clone("/Public", "/tmp/public")
200
+ > File.open("/tmp/public/hello.txt", "w") {|f| f << "Hello World" }
201
+ > Dbox.push("/tmp/public")
202
+
203
+ > File.read("#{ENV['HOME']}/Dropbox/Public/hello.txt")
204
+ => "Hello World"
205
+ > File.open("#{ENV['HOME']}/Dropbox/Public/hello.txt", "w") {|f| f << "Oh, Hello" }
206
+
207
+ > Dbox.pull("/tmp/public")
208
+ > File.read("#{ENV['HOME']}/Dropbox/Public/hello.txt")
209
+ => "Oh, Hello"
210
+ ```
data/TODO.txt CHANGED
@@ -1,7 +1,6 @@
1
- * More helpers in specs (and string style paths instead of join all over the place)
2
- * Have pull, push, etc return a list of changed files
3
1
  * Look down directory tree until you hit a .dropbox.db file
4
- * Solve upload -> re-download issue
5
- * Add rename_remote command
2
+ * Put pull, push, etc in begin blocks and rescue => save to avoid half-baked repos
3
+ * Detect old db format and migrate
4
+ * Add "move" command (that renames remote)
6
5
  * Add a "sync" command that pushes and pulls in one go
7
- * Add support for partial push/pull? (also, make it defoult behaviour from subdir?)
6
+ * Add support for partial push/pull
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.2.0
1
+ 0.3.0
data/bin/dbox CHANGED
@@ -32,20 +32,30 @@ def print_usage_and_quit; puts usage; exit 1; end
32
32
  print_usage_and_quit unless ARGV.size >= 1
33
33
 
34
34
  command = ARGV[0]
35
- rest = ARGV[1..-1]
35
+ args = ARGV[1..-1]
36
36
 
37
37
  # execute the command
38
38
  case command
39
39
  when "authorize"
40
40
  Dbox.authorize
41
41
  when "create", "clone"
42
- unless rest.size >= 1
42
+ unless args.size >= 1
43
43
  puts "Error: Please provide a remote path to clone"
44
44
  print_usage_and_quit
45
45
  end
46
- Dbox.send(command, *rest)
46
+
47
+ remote_path = args[0]
48
+
49
+ # default to creating a directory inside the current directory with
50
+ # the same name of the directory being created/cloned
51
+ local_path = args[1] || remote_path.split("/").last
52
+
53
+ Dbox.send(command, remote_path, local_path)
47
54
  when "pull", "push"
48
- Dbox.send(command, *rest)
55
+ # default to current directory
56
+ local_path = args[0] || "."
57
+
58
+ Dbox.send(command, local_path)
49
59
  else
50
60
  print_usage_and_quit
51
61
  end
@@ -5,7 +5,7 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{dbox}
8
- s.version = "0.2.0"
8
+ s.version = "0.3.0"
9
9
 
10
10
  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
11
  s.authors = [%q{Ken Pratt}]
@@ -17,36 +17,38 @@ module Dbox
17
17
  Dbox::API.authorize
18
18
  end
19
19
 
20
- def self.create(remote_path, local_path = nil)
20
+ def self.create(remote_path, local_path)
21
21
  remote_path = clean_remote_path(remote_path)
22
- local_path ||= remote_path.split("/").last
22
+ local_path = clean_local_path(local_path)
23
23
  Dbox::DB.create(remote_path, local_path)
24
24
  end
25
25
 
26
- def self.clone(remote_path, local_path = nil)
26
+ def self.clone(remote_path, local_path)
27
27
  remote_path = clean_remote_path(remote_path)
28
- local_path ||= remote_path.split("/").last
28
+ local_path = clean_local_path(local_path)
29
29
  Dbox::DB.clone(remote_path, local_path)
30
30
  end
31
31
 
32
- def self.pull(local_path = nil)
33
- local_path ||= "."
32
+ def self.pull(local_path)
33
+ local_path = clean_local_path(local_path)
34
34
  Dbox::DB.pull(local_path)
35
35
  end
36
36
 
37
- def self.push(local_path = nil)
38
- local_path ||= "."
37
+ def self.push(local_path)
38
+ local_path = clean_local_path(local_path)
39
39
  Dbox::DB.push(local_path)
40
40
  end
41
41
 
42
42
  private
43
43
 
44
44
  def self.clean_remote_path(path)
45
- if path
46
- path.sub(/\/$/,'')
47
- path[0].chr == "/" ? path : "/#{path}"
48
- else
49
- raise "Missing remote path"
50
- end
45
+ raise(ArgumentError, "Missing remote path") unless path
46
+ path.sub(/\/$/,'')
47
+ path[0].chr == "/" ? path : "/#{path}"
48
+ end
49
+
50
+ def self.clean_local_path(path)
51
+ raise(ArgumentError, "Missing local path") unless path
52
+ File.expand_path(path)
51
53
  end
52
54
  end
@@ -1,4 +1,10 @@
1
1
  module Dbox
2
+ class ConfigurationError < RuntimeError; end
3
+ class ServerError < RuntimeError; end
4
+ class RemoteMissing < RuntimeError; end
5
+ class RemoteAlreadyExists < RuntimeError; end
6
+ class RequestDenied < RuntimeError; end
7
+
2
8
  class API
3
9
  include Loggable
4
10
 
@@ -34,60 +40,84 @@ module Dbox
34
40
  auth_key = ENV["DROPBOX_AUTH_KEY"]
35
41
  auth_secret = ENV["DROPBOX_AUTH_SECRET"]
36
42
 
37
- raise("Please set the DROPBOX_AUTH_KEY environment variable to an authenticated Dropbox session key") unless auth_key
38
- raise("Please set the DROPBOX_AUTH_SECRET environment variable to an authenticated Dropbox session secret") unless auth_secret
43
+ raise(ConfigurationError, "Please set the DROPBOX_AUTH_KEY environment variable to an authenticated Dropbox session key") unless auth_key
44
+ raise(ConfigurationError, "Please set the DROPBOX_AUTH_SECRET environment variable to an authenticated Dropbox session secret") unless auth_secret
39
45
 
40
46
  @auth = Authenticator.new(@conf, auth_key, auth_secret)
41
47
  @client = DropboxClient.new(@conf["server"], @conf["content_server"], @conf["port"], @auth)
42
48
  end
43
49
 
44
- def metadata(path = "/")
50
+ def run(path)
45
51
  path = escape_path(path)
46
- log.debug "Fetching metadata for #{path}"
47
52
  begin
48
- case res = @client.metadata(@conf["root"], path)
53
+ res = yield path
54
+ log.debug "Result: #{res.inspect}"
55
+
56
+ case res
49
57
  when Hash
50
58
  res
59
+ when String
60
+ res
51
61
  when Net::HTTPNotFound
52
- raise "Remote path does not exist"
62
+ raise RemoteMissing, "#{path} does not exist on Dropbox"
63
+ when Net::HTTPForbidden
64
+ raise RequestDenied, "Operation on #{path} denied"
53
65
  else
54
- raise "Unexpected result from GET /metadata: #{res.inspect}"
66
+ raise RuntimeError, "Unexpected result: #{res.inspect}"
55
67
  end
56
68
  rescue DropboxError => e
57
- raise "Server error -- might be a hiccup, please try your request again (#{e.message})"
69
+ log.debug e.inspect
70
+ raise ServerError, "Server error -- might be a hiccup, please try your request again (#{e.message})"
71
+ end
72
+ end
73
+
74
+ def metadata(path = "/")
75
+ log.debug "Fetching metadata for #{path}"
76
+ run(path) do |path|
77
+ @client.metadata(@conf["root"], path)
58
78
  end
59
79
  end
60
80
 
61
81
  def create_dir(path)
62
- path = escape_path(path)
63
82
  log.info "Creating #{path}"
64
- @client.file_create_folder(@conf["root"], path)
83
+ run(path) do |path|
84
+ case res = @client.file_create_folder(@conf["root"], path)
85
+ when Net::HTTPForbidden
86
+ raise RemoteAlreadyExists, "The directory at #{path} already exists"
87
+ else
88
+ res
89
+ end
90
+ end
65
91
  end
66
92
 
67
93
  def delete_dir(path)
68
- path = escape_path(path)
69
94
  log.info "Deleting #{path}"
70
- @client.file_delete(@conf["root"], path)
95
+ run(path) do |path|
96
+ @client.file_delete(@conf["root"], path)
97
+ end
71
98
  end
72
99
 
73
100
  def get_file(path)
74
- path = escape_path(path)
75
101
  log.info "Downloading #{path}"
76
- @client.get_file(@conf["root"], path)
102
+ run(path) do |path|
103
+ @client.get_file(@conf["root"], path)
104
+ end
77
105
  end
78
106
 
79
107
  def put_file(path, file_obj)
80
- path = escape_path(path)
81
108
  log.info "Uploading #{path}"
82
- dir = File.dirname(path)
83
- name = File.basename(path)
84
- @client.put_file(@conf["root"], dir, name, file_obj)
109
+ run(path) do |path|
110
+ dir = File.dirname(path)
111
+ name = File.basename(path)
112
+ @client.put_file(@conf["root"], dir, name, file_obj)
113
+ end
85
114
  end
86
115
 
87
116
  def delete_file(path)
88
- path = escape_path(path)
89
117
  log.info "Deleting #{path}"
90
- @client.file_delete(@conf["root"], path)
118
+ run(path) do |path|
119
+ @client.file_delete(@conf["root"], path)
120
+ end
91
121
  end
92
122
 
93
123
  def escape_path(path)
@@ -98,8 +128,8 @@ module Dbox
98
128
  app_key = ENV["DROPBOX_APP_KEY"]
99
129
  app_secret = ENV["DROPBOX_APP_SECRET"]
100
130
 
101
- raise("Please set the DROPBOX_APP_KEY environment variable to a Dropbox application key") unless app_key
102
- raise("Please set the DROPBOX_APP_SECRET environment variable to a Dropbox application secret") unless app_secret
131
+ raise(ConfigurationError, "Please set the DROPBOX_APP_KEY environment variable to a Dropbox application key") unless app_key
132
+ raise(ConfigurationError, "Please set the DROPBOX_APP_SECRET environment variable to a Dropbox application secret") unless app_secret
103
133
 
104
134
  {
105
135
  "server" => "api.dropbox.com",
@@ -1,4 +1,7 @@
1
1
  module Dbox
2
+ class MissingDatabase < RuntimeError; end
3
+ class BadPath < RuntimeError; end
4
+
2
5
  class DB
3
6
  include Loggable
4
7
 
@@ -7,7 +10,6 @@ module Dbox
7
10
  attr_accessor :local_path
8
11
 
9
12
  def self.create(remote_path, local_path)
10
- log.info "Creating remote folder: #{remote_path}"
11
13
  api.create_dir(remote_path)
12
14
  clone(remote_path, local_path)
13
15
  end
@@ -15,7 +17,7 @@ module Dbox
15
17
  def self.clone(remote_path, local_path)
16
18
  log.info "Cloning #{remote_path} into #{local_path}"
17
19
  res = api.metadata(remote_path)
18
- raise "Remote path error" unless remote_path == res["path"]
20
+ raise(BadPath, "Remote path error") unless remote_path == res["path"]
19
21
  db = new(local_path, res)
20
22
  db.pull
21
23
  end
@@ -24,10 +26,10 @@ module Dbox
24
26
  db_file = db_file(local_path)
25
27
  if File.exists?(db_file)
26
28
  db = File.open(db_file, "r") {|f| YAML::load(f.read) }
27
- db.local_path = File.expand_path(local_path)
29
+ db.local_path = local_path
28
30
  db
29
31
  else
30
- raise "No DB file found in #{local_path}"
32
+ raise MissingDatabase, "No DB file found in #{local_path}"
31
33
  end
32
34
  end
33
35
 
@@ -42,7 +44,7 @@ module Dbox
42
44
  # IMPORTANT: DropboxDb.new is private. Please use DropboxDb.create, DropboxDb.clone, or DropboxDb.load as the entry point.
43
45
  private_class_method :new
44
46
  def initialize(local_path, res)
45
- @local_path = File.expand_path(local_path)
47
+ @local_path = local_path
46
48
  @remote_path = res["path"]
47
49
  FileUtils.mkdir_p(@local_path)
48
50
  @root = DropboxDir.new(self, res)
@@ -56,20 +58,22 @@ module Dbox
56
58
  end
57
59
 
58
60
  def pull
59
- @root.pull
61
+ res = @root.pull
60
62
  save
63
+ res
61
64
  end
62
65
 
63
66
  def push
64
- @root.push
67
+ res = @root.push
65
68
  save
69
+ res
66
70
  end
67
71
 
68
72
  def local_to_relative_path(path)
69
73
  if path.include?(@local_path)
70
74
  path.sub(@local_path, "").sub(/^\//, "")
71
75
  else
72
- raise "Not a local path: #{path}"
76
+ raise BadPath, "Not a local path: #{path}"
73
77
  end
74
78
  end
75
79
 
@@ -77,7 +81,7 @@ module Dbox
77
81
  if path.include?(@remote_path)
78
82
  path.sub(@remote_path, "").sub(/^\//, "")
79
83
  else
80
- raise "Not a remote path: #{path}"
84
+ raise BadPath, "Not a remote path: #{path}"
81
85
  end
82
86
  end
83
87
 
@@ -154,8 +158,8 @@ module Dbox
154
158
  end
155
159
 
156
160
  def update(res)
157
- raise "bad path (#{remote_path} != #{res["path"]})" unless remote_path == res["path"]
158
- raise "mode on #{@path} changed between file and dir -- not supported yet" unless dir? == res["is_dir"] # TODO handle change from dir to file or vice versa?
161
+ raise(BadPath, "Bad path (#{remote_path} != #{res["path"]})") unless remote_path == res["path"]
162
+ raise(RuntimeError, "Mode on #{@path} changed between file and dir -- not supported yet") unless dir? == res["is_dir"]
159
163
  update_modification_info(res)
160
164
  end
161
165
 
@@ -168,16 +172,16 @@ module Dbox
168
172
  end
169
173
 
170
174
  def dir?
171
- raise "not implemented"
175
+ raise RuntimeError, "Not implemented"
172
176
  end
173
177
 
174
- def create_local; raise "not implemented"; end
175
- def delete_local; raise "not implemented"; end
176
- def update_local; raise "not implemented"; end
178
+ def create_local; raise RuntimeError, "Not implemented"; end
179
+ def delete_local; raise RuntimeError, "Not implemented"; end
180
+ def update_local; raise RuntimeError, "Not implemented"; end
177
181
 
178
- def create_remote; raise "not implemented"; end
179
- def delete_remote; raise "not implemented"; end
180
- def update_remote; raise "not implemented"; end
182
+ def create_remote; raise RuntimeError, "Not implemented"; end
183
+ def delete_remote; raise RuntimeError, "Not implemented"; end
184
+ def update_remote; raise RuntimeError, "Not implemented"; end
181
185
 
182
186
  def modified?(last)
183
187
  !(revision == last.revision && modified_at == last.modified_at)
@@ -187,6 +191,15 @@ module Dbox
187
191
  File.utime(Time.now, modified_at, local_path)
188
192
  end
189
193
 
194
+ # this downloads the metadata about this blob from the server and
195
+ # overwrites the metadata & timestamp
196
+ # IMPORTANT: should only be called if you are CERTAIN the file is up to date
197
+ def force_metadata_update_from_server
198
+ res = api.metadata(remote_path)
199
+ update_modification_info(res)
200
+ update_file_timestamp
201
+ end
202
+
190
203
  def saving_parent_timestamp(&proc)
191
204
  parent = File.dirname(local_path)
192
205
  DB.saving_timestamp(parent, &proc)
@@ -207,19 +220,21 @@ module Dbox
207
220
  end
208
221
 
209
222
  def update(res)
210
- raise "not a directory" unless res["is_dir"]
223
+ raise(ArgumentError, "Not a directory: #{res.inspect}") unless res["is_dir"]
211
224
  super(res)
212
225
  @contents_hash = res["hash"] if res.has_key?("hash")
213
226
  if res.has_key?("contents")
214
227
  old_contents = @contents
215
228
  new_contents_arr = remove_dotfiles(res["contents"]).map do |c|
216
- if last_entry = old_contents[c["path"]]
229
+ p = @db.remote_to_relative_path(c["path"])
230
+ if last_entry = old_contents[p]
217
231
  new_entry = last_entry.clone
218
232
  last_entry.freeze
219
233
  new_entry.update(c)
220
- [c["path"], new_entry]
234
+ [new_entry.path, new_entry]
221
235
  else
222
- [c["path"], smart_new(c)]
236
+ new_entry = smart_new(c)
237
+ [new_entry.path, new_entry]
223
238
  end
224
239
  end
225
240
  @contents = Hash[new_contents_arr]
@@ -233,28 +248,28 @@ module Dbox
233
248
  def pull
234
249
  prev = self.clone
235
250
  prev.freeze
236
- log.info "Pulling changes"
237
251
  res = api.metadata(remote_path)
238
252
  update(res)
239
253
  if contents_hash != prev.contents_hash
240
- reconcile(prev, :down)
254
+ changes = reconcile(prev, :down)
255
+ else
256
+ changes = { :created => [], :deleted => [], :updated => [] }
241
257
  end
242
- subdirs.each {|d| d.pull }
258
+ subdirs.inject(changes) {|c, d| merge_changes(c, d.pull) }
243
259
  end
244
260
 
245
261
  def push
246
262
  prev = self.clone
247
263
  prev.freeze
248
- log.info "Pushing changes"
249
264
  res = gather_info(@path)
250
265
  update(res)
251
- reconcile(prev, :up)
252
- subdirs.each {|d| d.push }
266
+ changes = reconcile(prev, :up)
267
+ subdirs.inject(changes) {|c, d| merge_changes(c, d.push) }
253
268
  end
254
269
 
255
270
  def reconcile(prev, direction)
256
- old_paths = prev.contents.keys
257
- new_paths = contents.keys
271
+ old_paths = prev.contents.keys.sort
272
+ new_paths = contents.keys.sort
258
273
 
259
274
  deleted_paths = old_paths - new_paths
260
275
 
@@ -268,15 +283,21 @@ module Dbox
268
283
  deleted_paths.each {|p| prev.contents[p].delete_local }
269
284
  created_paths.each {|p| contents[p].create_local }
270
285
  stale_paths.each {|p| contents[p].update_local }
286
+ { :created => created_paths, :deleted => deleted_paths, :updated => stale_paths }
271
287
  when :up
272
288
  deleted_paths.each {|p| prev.contents[p].delete_remote }
273
289
  created_paths.each {|p| contents[p].create_remote }
274
290
  stale_paths.each {|p| contents[p].update_remote }
291
+ { :created => created_paths, :deleted => deleted_paths, :updated => stale_paths }
275
292
  else
276
- raise "Invalid direction: #{direction.inspect}"
293
+ raise(ArgumentError, "Invalid sync direction: #{direction.inspect}")
277
294
  end
278
295
  end
279
296
 
297
+ def merge_changes(old, new)
298
+ old.merge(new) {|k, v1, v2| v1 + v2 }
299
+ end
300
+
280
301
  def gather_info(rel, list_contents=true)
281
302
  full = @db.relative_to_local_path(rel)
282
303
  remote = @db.relative_to_remote_path(rel)
@@ -303,7 +324,6 @@ module Dbox
303
324
  end
304
325
 
305
326
  def create_local
306
- log.info "Creating dir: #{local_path}"
307
327
  saving_parent_timestamp do
308
328
  FileUtils.mkdir_p(local_path)
309
329
  update_file_timestamp
@@ -318,12 +338,12 @@ module Dbox
318
338
  end
319
339
 
320
340
  def update_local
321
- log.info "Updating dir: #{local_path}"
322
341
  update_file_timestamp
323
342
  end
324
343
 
325
344
  def create_remote
326
345
  api.create_dir(remote_path)
346
+ force_metadata_update_from_server
327
347
  end
328
348
 
329
349
  def delete_remote
@@ -354,10 +374,8 @@ module Dbox
354
374
  end
355
375
 
356
376
  def create_local
357
- log.info "Creating file: #{local_path}"
358
377
  saving_parent_timestamp do
359
378
  download
360
- update_file_timestamp
361
379
  end
362
380
  end
363
381
 
@@ -369,9 +387,7 @@ module Dbox
369
387
  end
370
388
 
371
389
  def update_local
372
- log.info "Updating file: #{local_path}"
373
390
  download
374
- update_file_timestamp
375
391
  end
376
392
 
377
393
  def create_remote
@@ -399,6 +415,7 @@ module Dbox
399
415
  File.open(local_path) do |f|
400
416
  res = api.put_file(remote_path, f)
401
417
  end
418
+ force_metadata_update_from_server
402
419
  end
403
420
  end
404
421
  end
@@ -3,119 +3,183 @@ require File.expand_path(File.dirname(__FILE__) + "/spec_helper")
3
3
  include FileUtils
4
4
 
5
5
  describe Dbox do
6
+ before(:all) do
7
+ clear_test_log
8
+ end
9
+
6
10
  before(:each) do
7
- cd LOCAL_TEST_PATH
11
+ log.info example.full_description
8
12
  @name = randname()
9
13
  @local = File.join(LOCAL_TEST_PATH, @name)
10
14
  @remote = File.join(REMOTE_TEST_PATH, @name)
11
15
  end
12
16
 
17
+ after(:each) do
18
+ log.info ""
19
+ end
20
+
13
21
  describe "#create" do
14
22
  it "creates the local directory" do
15
- Dbox.create(@remote)
16
- File.exists?(@local).should be_true
23
+ Dbox.create(@remote, @local)
24
+ @local.should exist
17
25
  end
18
26
 
19
- xit "should fail if the remote already exists" do
20
- Dbox.create(@remote)
27
+ it "should fail if the remote already exists" do
28
+ Dbox.create(@remote, @local)
21
29
  rm_rf @local
22
- expect { Dbox.create(@remote) }.to raise_error("Remote path already exists")
23
- File.exists?(@local).should be_false
30
+ expect { Dbox.create(@remote, @local) }.to raise_error(Dbox::RemoteAlreadyExists)
31
+ @local.should_not exist
24
32
  end
25
33
  end
26
34
 
27
35
  describe "#clone" do
28
36
  it "creates the local directory" do
29
- Dbox.create(@remote)
37
+ Dbox.create(@remote, @local)
30
38
  rm_rf @local
31
- File.exists?(@local).should be_false
32
- Dbox.clone(@remote)
33
- File.exists?(@local).should be_true
39
+ @local.should_not exist
40
+ Dbox.clone(@remote, @local)
41
+ @local.should exist
34
42
  end
35
43
 
36
44
  it "should fail if the remote does not exist" do
37
- expect { Dbox.clone(@remote) }.to raise_error("Remote path does not exist")
38
- File.exists?(@local).should be_false
45
+ expect { Dbox.clone(@remote, @local) }.to raise_error(Dbox::RemoteMissing)
46
+ @local.should_not exist
39
47
  end
40
48
  end
41
49
 
42
50
  describe "#pull" do
43
51
  it "should fail if the local dir is missing" do
44
- expect { Dbox.pull(@local) }.to raise_error(/No DB file found/)
52
+ expect { Dbox.pull(@local) }.to raise_error(Dbox::MissingDatabase)
45
53
  end
46
54
 
47
55
  it "should fail if the remote dir is missing" do
48
- Dbox.create(@remote)
56
+ Dbox.create(@remote, @local)
49
57
  modify_dbfile {|s| s.sub(/^remote_path: \/.*$/, "remote_path: /#{randname()}") }
50
- expect { Dbox.pull(@local) }.to raise_error("Remote path does not exist")
58
+ expect { Dbox.pull(@local) }.to raise_error(Dbox::RemoteMissing)
51
59
  end
52
60
 
53
61
  it "should be able to pull" do
54
- Dbox.create(@remote)
55
- expect { Dbox.pull(@local) }.to_not raise_error
56
- end
57
-
58
- it "should be able to pull from inside the dir" do
59
- Dbox.create(@remote)
60
- cd @local
61
- expect { Dbox.pull }.to_not raise_error
62
+ Dbox.create(@remote, @local)
63
+ Dbox.pull(@local).should eql(:created => [], :deleted => [], :updated => [])
62
64
  end
63
65
 
64
66
  it "should be able to pull changes" do
65
- Dbox.create(@remote)
66
- File.exists?("#{@local}/hello.txt").should be_false
67
+ Dbox.create(@remote, @local)
68
+ "#{@local}/hello.txt".should_not exist
67
69
 
68
- cd ALTERNATE_LOCAL_TEST_PATH
69
- Dbox.clone(@remote)
70
- cd @name
71
- touch "hello.txt"
72
- Dbox.push
70
+ @alternate = "#{ALTERNATE_LOCAL_TEST_PATH}/#{@name}"
71
+ Dbox.clone(@remote, @alternate)
72
+ touch "#{@alternate}/hello.txt"
73
+ Dbox.push(@alternate).should eql(:created => ["hello.txt"], :deleted => [], :updated => [])
73
74
 
74
- expect { Dbox.pull(@local) }.to_not raise_error
75
- File.exists?("#{@local}/hello.txt").should be_true
75
+ Dbox.pull(@local).should eql(:created => ["hello.txt"], :deleted => [], :updated => [])
76
+ "#{@local}/hello.txt".should exist
76
77
  end
77
78
 
78
79
  it "should be able to pull after deleting a file and not have the file re-created" do
79
- Dbox.create(@remote)
80
- cd @name
81
- touch "hello.txt"
82
- Dbox.push
83
- Dbox.pull
84
- rm "hello.txt"
85
- Dbox.pull
86
- File.exists?("#{@local}/hello.txt").should be_false
80
+ Dbox.create(@remote, @local)
81
+ touch "#{@local}/hello.txt"
82
+ Dbox.push(@local).should eql(:created => ["hello.txt"], :deleted => [], :updated => [])
83
+ rm "#{@local}/hello.txt"
84
+ Dbox.pull(@local).should eql(:created => [], :deleted => [], :updated => [])
85
+ "#{@local}/hello.txt".should_not exist
86
+ end
87
+
88
+ it "should handle a complex set of changes" do
89
+ Dbox.create(@remote, @local)
90
+
91
+ @alternate = "#{ALTERNATE_LOCAL_TEST_PATH}/#{@name}"
92
+ Dbox.clone(@remote, @alternate)
93
+
94
+ Dbox.pull(@local).should eql(:created => [], :deleted => [], :updated => [])
95
+
96
+ touch "#{@alternate}/foo.txt"
97
+ touch "#{@alternate}/bar.txt"
98
+ touch "#{@alternate}/baz.txt"
99
+ Dbox.push(@alternate).should eql(:created => ["bar.txt", "baz.txt", "foo.txt"], :deleted => [], :updated => [])
100
+
101
+ Dbox.pull(@local).should eql(:created => ["bar.txt", "baz.txt", "foo.txt"], :deleted => [], :updated => [])
102
+
103
+ sleep 1
104
+ mkdir "#{@alternate}/subdir"
105
+ touch "#{@alternate}/subdir/one.txt"
106
+ rm "#{@alternate}/foo.txt"
107
+ File.open("#{@alternate}/baz.txt", "w") {|f| f << "baaz" }
108
+ Dbox.push(@alternate).should eql(:created => ["subdir", "subdir/one.txt"], :deleted => ["foo.txt"], :updated => ["baz.txt"])
109
+
110
+ Dbox.pull(@local).should eql(:created => ["subdir", "subdir/one.txt"], :deleted => ["foo.txt"], :updated => ["baz.txt"])
87
111
  end
88
112
  end
89
113
 
90
114
  describe "#push" do
91
115
  it "should fail if the local dir is missing" do
92
- expect { Dbox.push(@local) }.to raise_error(/No DB file found/)
116
+ expect { Dbox.push(@local) }.to raise_error(Dbox::MissingDatabase)
93
117
  end
94
118
 
95
119
  it "should be able to push" do
96
- Dbox.create(@remote)
97
- expect { Dbox.push(@local) }.to_not raise_error
98
- end
99
-
100
- it "should be able to push from inside the dir" do
101
- Dbox.create(@remote)
102
- cd @local
103
- expect { Dbox.push }.to_not raise_error
120
+ Dbox.create(@remote, @local)
121
+ Dbox.push(@local).should eql(:created => [], :deleted => [], :updated => [])
104
122
  end
105
123
 
106
124
  it "should be able to push new file" do
107
- Dbox.create(@remote)
108
- touch File.join(@local, "foo.txt")
109
- expect { Dbox.push(@local) }.to_not raise_error
125
+ Dbox.create(@remote, @local)
126
+ touch "#{@local}/foo.txt"
127
+ Dbox.push(@local).should eql(:created => ["foo.txt"], :deleted => [], :updated => [])
110
128
  end
111
129
 
112
130
  it "should create the remote dir if it is missing" do
113
- Dbox.create(@remote)
114
- touch File.join(@local, "foo.txt")
131
+ Dbox.create(@remote, @local)
132
+ touch "#{@local}/foo.txt"
115
133
  @new_name = randname()
116
134
  @new_remote = File.join(REMOTE_TEST_PATH, @new_name)
117
135
  modify_dbfile {|s| s.sub(/^remote_path: \/.*$/, "remote_path: #{@new_remote}") }
118
- expect { Dbox.push(@local) }.to_not raise_error
136
+ Dbox.push(@local).should eql(:created => ["foo.txt"], :deleted => [], :updated => [])
137
+ end
138
+
139
+ it "should be able to push nested content" do
140
+ Dbox.create(@remote, @local)
141
+ mkdir "#{@local}/subdir"
142
+ touch "#{@local}/subdir/foo.txt"
143
+ Dbox.push(@local).should eql(:created => ["subdir", "subdir/foo.txt"], :deleted => [], :updated => [])
144
+ end
145
+
146
+ it "should not re-download the file after creating" do
147
+ Dbox.create(@remote, @local)
148
+ touch "#{@local}/foo.txt"
149
+ Dbox.push(@local).should eql(:created => ["foo.txt"], :deleted => [], :updated => [])
150
+ Dbox.pull(@local).should eql(:created => [], :deleted => [], :updated => [])
151
+ end
152
+
153
+ it "should not re-download the file after updating" do
154
+ Dbox.create(@remote, @local)
155
+ touch "#{@local}/foo.txt"
156
+ Dbox.push(@local).should eql(:created => ["foo.txt"], :deleted => [], :updated => [])
157
+ sleep 1
158
+ File.open("#{@local}/foo.txt", "w") {|f| f << "fooz" }
159
+ Dbox.push(@local).should eql(:created => [], :deleted => [], :updated => ["foo.txt"])
160
+ Dbox.pull(@local).should eql(:created => [], :deleted => [], :updated => [])
161
+ end
162
+
163
+ it "should not re-download the dir after creating" do
164
+ Dbox.create(@remote, @local)
165
+ mkdir "#{@local}/subdir"
166
+ Dbox.push(@local).should eql(:created => ["subdir"], :deleted => [], :updated => [])
167
+ Dbox.pull(@local).should eql(:created => [], :deleted => [], :updated => [])
168
+ end
169
+
170
+ it "should handle a complex set of changes" do
171
+ Dbox.create(@remote, @local)
172
+ touch "#{@local}/foo.txt"
173
+ touch "#{@local}/bar.txt"
174
+ touch "#{@local}/baz.txt"
175
+ Dbox.push(@local).should eql(:created => ["bar.txt", "baz.txt", "foo.txt"], :deleted => [], :updated => [])
176
+ sleep 1
177
+ mkdir "#{@local}/subdir"
178
+ touch "#{@local}/subdir/one.txt"
179
+ rm "#{@local}/foo.txt"
180
+ touch "#{@local}/baz.txt"
181
+ Dbox.push(@local).should eql(:created => ["subdir", "subdir/one.txt"], :deleted => ["foo.txt"], :updated => ["baz.txt"])
182
+ Dbox.pull(@local).should eql(:created => [], :deleted => [], :updated => [])
119
183
  end
120
184
  end
121
185
  end
@@ -11,7 +11,13 @@ FileUtils.mkdir_p(ALTERNATE_LOCAL_TEST_PATH)
11
11
 
12
12
  REMOTE_TEST_PATH = "/dbox_test_dirs"
13
13
 
14
- LOGGER = Logger.new(File.expand_path(File.join(File.dirname(__FILE__), "..", "tmp", "test.log")))
14
+ $started_at ||= Time.now
15
+
16
+ LOGFILE = File.expand_path(File.join(File.dirname(__FILE__), "..", "tmp", "test.log"))
17
+ LOGGER = Logger.new(LOGFILE)
18
+ LOGGER.formatter = proc do |severity, datetime, progname, msg|
19
+ format "[%4.1fs] [%s] %s\n", (Time.now - $started_at), severity, msg
20
+ end
15
21
 
16
22
  def randname
17
23
  u = `uuidgen`.chomp
@@ -24,3 +30,17 @@ def modify_dbfile
24
30
  s = yield s
25
31
  File.open(dbfile, "w") {|f| f << s }
26
32
  end
33
+
34
+ def clear_test_log
35
+ File.open(LOGFILE, "w") {|f| f << "" }
36
+ end
37
+
38
+ def log
39
+ LOGGER
40
+ end
41
+
42
+ RSpec::Matchers.define :exist do
43
+ match do |actual|
44
+ File.exists?(actual) == true
45
+ end
46
+ end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: dbox
3
3
  version: !ruby/object:Gem::Version
4
- hash: 23
4
+ hash: 19
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
- - 2
8
+ - 3
9
9
  - 0
10
- version: 0.2.0
10
+ version: 0.3.0
11
11
  platform: ruby
12
12
  authors:
13
13
  - Ken Pratt