dbox 0.4.2 → 0.4.3

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.
data/TODO.txt CHANGED
@@ -1,6 +1,3 @@
1
1
  * Look down directory tree until you hit a .dropbox.db file
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)
5
2
  * Add a "sync" command that pushes and pulls in one go
6
3
  * Add support for partial push/pull
data/VERSION CHANGED
@@ -1 +1 @@
1
- 0.4.2
1
+ 0.4.3
data/dbox.gemspec CHANGED
@@ -5,11 +5,11 @@
5
5
 
6
6
  Gem::Specification.new do |s|
7
7
  s.name = %q{dbox}
8
- s.version = "0.4.2"
8
+ s.version = "0.4.3"
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}]
12
- s.date = %q{2011-05-17}
12
+ s.date = %q{2011-05-19}
13
13
  s.description = %q{An easy-to-use Dropbox client with fine-grained control over syncs.}
14
14
  s.email = %q{ken@kenpratt.net}
15
15
  s.executables = [%q{dbox}]
data/lib/dbox/api.rb CHANGED
@@ -71,7 +71,9 @@ module Dbox
71
71
  def metadata(path = "/")
72
72
  log.debug "Fetching metadata for #{path}"
73
73
  run(path) do
74
- @client.metadata(@conf["root"], escape_path(path))
74
+ res = @client.metadata(@conf["root"], escape_path(path))
75
+ log.debug res.inspect
76
+ res
75
77
  end
76
78
  end
77
79
 
data/lib/dbox/db.rb CHANGED
@@ -65,15 +65,11 @@ module Dbox
65
65
  end
66
66
 
67
67
  def pull
68
- res = @root.pull
69
- save
70
- res
68
+ @root.pull
71
69
  end
72
70
 
73
71
  def push
74
- res = @root.push
75
- save
76
- res
72
+ @root.push
77
73
  end
78
74
 
79
75
  def move(new_remote_path)
@@ -148,18 +144,16 @@ module Dbox
148
144
  end
149
145
 
150
146
  def update_modification_info(res)
147
+ raise(BadPath, "Bad path (#{remote_path} != #{res["path"]})") unless remote_path == res["path"]
148
+ raise(RuntimeError, "Mode on #{@path} changed between file and dir -- not supported yet") unless dir? == res["is_dir"]
151
149
  last_modified_at = @modified_at
152
- @modified_at = case t = res["modified"]
153
- when Time
154
- t
155
- when String
156
- Time.parse(t)
157
- end
158
- if res.has_key?("revision")
150
+ @modified_at = parse_time(res["modified"])
151
+ if res["revision"]
159
152
  @revision = res["revision"]
160
153
  else
161
154
  @revision = -1 if @modified_at != last_modified_at
162
155
  end
156
+ log.debug "updated modification info on #{path.inspect}: r#{@revision}, #{@modified_at}"
163
157
  end
164
158
 
165
159
  def smart_new(res)
@@ -170,12 +164,6 @@ module Dbox
170
164
  end
171
165
  end
172
166
 
173
- def update(res)
174
- raise(BadPath, "Bad path (#{remote_path} != #{res["path"]})") unless remote_path == res["path"]
175
- raise(RuntimeError, "Mode on #{@path} changed between file and dir -- not supported yet") unless dir? == res["is_dir"]
176
- update_modification_info(res)
177
- end
178
-
179
167
  def local_path
180
168
  @db.relative_to_local_path(@path)
181
169
  end
@@ -188,6 +176,33 @@ module Dbox
188
176
  raise RuntimeError, "Not implemented"
189
177
  end
190
178
 
179
+ def create(direction)
180
+ case direction
181
+ when :down
182
+ create_local
183
+ when :up
184
+ create_remote
185
+ end
186
+ end
187
+
188
+ def update(direction)
189
+ case direction
190
+ when :down
191
+ update_local
192
+ when :up
193
+ update_remote
194
+ end
195
+ end
196
+
197
+ def delete(direction)
198
+ case direction
199
+ when :down
200
+ delete_local
201
+ when :up
202
+ delete_remote
203
+ end
204
+ end
205
+
191
206
  def create_local; raise RuntimeError, "Not implemented"; end
192
207
  def delete_local; raise RuntimeError, "Not implemented"; end
193
208
  def update_local; raise RuntimeError, "Not implemented"; end
@@ -196,12 +211,23 @@ module Dbox
196
211
  def delete_remote; raise RuntimeError, "Not implemented"; end
197
212
  def update_remote; raise RuntimeError, "Not implemented"; end
198
213
 
199
- def modified?(last)
200
- !(revision == last.revision && modified_at == last.modified_at)
214
+ def modified?(res)
215
+ out = !(@revision == res["revision"] && @modified_at == parse_time(res["modified"]))
216
+ log.debug "#{path}.modified? r#{@revision} =? r#{res["revision"]}, #{@modified_at} =? #{parse_time(res["modified"])} => #{out}"
217
+ out
218
+ end
219
+
220
+ def parse_time(t)
221
+ case t
222
+ when Time
223
+ t
224
+ when String
225
+ Time.parse(t)
226
+ end
201
227
  end
202
228
 
203
229
  def update_file_timestamp
204
- File.utime(Time.now, modified_at, local_path)
230
+ File.utime(Time.now, @modified_at, local_path)
205
231
  end
206
232
 
207
233
  # this downloads the metadata about this blob from the server and
@@ -232,93 +258,118 @@ module Dbox
232
258
  super(db, res)
233
259
  end
234
260
 
235
- def update(res)
236
- raise(ArgumentError, "Not a directory: #{res.inspect}") unless res["is_dir"]
237
- super(res)
238
- @contents_hash = res["hash"] if res.has_key?("hash")
239
- if res.has_key?("contents")
240
- old_contents = @contents
241
- new_contents_arr = remove_dotfiles(res["contents"]).map do |c|
242
- p = @db.remote_to_relative_path(c["path"])
243
- if last_entry = old_contents[p]
244
- new_entry = last_entry.clone
245
- last_entry.freeze
246
- new_entry.update(c)
247
- [new_entry.path, new_entry]
248
- else
249
- new_entry = smart_new(c)
250
- [new_entry.path, new_entry]
251
- end
252
- end
253
- @contents = Hash[new_contents_arr]
254
- end
255
- end
256
-
257
- def remove_dotfiles(contents)
258
- contents.reject {|c| File.basename(c["path"]).start_with?(".") }
259
- end
260
-
261
261
  def pull
262
- prev = self.clone
263
- prev.freeze
262
+ # calculate changes on this dir
264
263
  res = api.metadata(remote_path)
265
- update(res)
266
- if contents_hash != prev.contents_hash
267
- changes = reconcile(prev, :down)
268
- else
269
- changes = { :created => [], :deleted => [], :updated => [] }
270
- end
271
- subdirs.inject(changes) {|c, d| merge_changes(c, d.pull) }
264
+ changes = calculate_changes(res)
265
+
266
+ # execute changes on this dir
267
+ changelist = execute_changes(changes, :down)
268
+
269
+ # recur on subdirs, expanding changelist as we go
270
+ changelist = subdirs.inject(changelist) {|c, d| merge_changelists(c, d.pull) }
271
+
272
+ # only update the modification info on the directory once all descendants are updated
273
+ update_modification_info(res)
274
+
275
+ # return changes
276
+ @db.save
277
+ changelist
272
278
  end
273
279
 
274
280
  def push
275
- prev = self.clone
276
- prev.freeze
277
- res = gather_info(@path)
278
- update(res)
279
- changes = reconcile(prev, :up)
280
- subdirs.inject(changes) {|c, d| merge_changes(c, d.push) }
281
- end
281
+ # calculate changes on this dir
282
+ res = gather_local_info(@path)
283
+ changes = calculate_changes(res)
282
284
 
283
- def reconcile(prev, direction)
284
- old_paths = prev.contents.keys.sort
285
- new_paths = contents.keys.sort
285
+ # execute changes on this dir
286
+ changelist = execute_changes(changes, :up)
286
287
 
287
- deleted_paths = old_paths - new_paths
288
+ # recur on subdirs, expanding changelist as we go
289
+ changelist = subdirs.inject(changelist) {|c, d| merge_changelists(c, d.push) }
288
290
 
289
- created_paths = new_paths - old_paths
291
+ # only update the modification info on the directory once all descendants are updated
292
+ update_modification_info(res)
290
293
 
291
- kept_paths = old_paths & new_paths
292
- stale_paths = kept_paths.select {|p| contents[p].modified?(prev.contents[p]) }
294
+ # return changes
295
+ @db.save
296
+ changelist
297
+ end
293
298
 
294
- case direction
295
- when :down
296
- deleted_paths.each {|p| prev.contents[p].delete_local }
297
- created_paths.each {|p| contents[p].create_local }
298
- stale_paths.each {|p| contents[p].update_local }
299
- { :created => created_paths, :deleted => deleted_paths, :updated => stale_paths }
300
- when :up
301
- deleted_paths.each {|p| prev.contents[p].delete_remote }
302
- created_paths.each {|p| contents[p].create_remote }
303
- stale_paths.each {|p| contents[p].update_remote }
304
- { :created => created_paths, :deleted => deleted_paths, :updated => stale_paths }
299
+ def calculate_changes(res)
300
+ raise(ArgumentError, "Not a directory: #{res.inspect}") unless res["is_dir"]
301
+
302
+ if @contents_hash && res["hash"] && @contents_hash == res["hash"]
303
+ # dir hash hasn't changed -- no need to calculate changes
304
+ []
305
+ elsif res["contents"]
306
+ # dir has changed -- calculate changes on contents
307
+ out = []
308
+ got_paths = []
309
+
310
+ remove_dotfiles(res["contents"]).each do |c|
311
+ p = @db.remote_to_relative_path(c["path"])
312
+ c["rel_path"] = p
313
+ got_paths << p
314
+
315
+ if @contents.has_key?(p)
316
+ # only update file if it's been modified
317
+ if @contents[p].modified?(c)
318
+ out << [:update, c]
319
+ end
320
+ else
321
+ out << [:create, c]
322
+ end
323
+ end
324
+ out += (@contents.keys.sort - got_paths.sort).map {|p| [:delete, { "rel_path" => p }] }
325
+ out
305
326
  else
306
- raise(ArgumentError, "Invalid sync direction: #{direction.inspect}")
327
+ raise(RuntimeError, "Trying to calculate dir changes without any contents")
307
328
  end
308
329
  end
309
330
 
310
- def merge_changes(old, new)
311
- old.merge(new) {|k, v1, v2| v1 + v2 }
331
+ def execute_changes(changes, direction)
332
+ log.debug "executing changes: #{changes.inspect}"
333
+ changelist = { :created => [], :deleted => [], :updated => [] }
334
+ changes.each do |op, c|
335
+ case op
336
+ when :create
337
+ e = smart_new(c)
338
+ e.create(direction)
339
+ @contents[e.path] = e
340
+ changelist[:created] << e.path
341
+ when :update
342
+ e = @contents[c["rel_path"]]
343
+ e.update_modification_info(c) if direction == :down
344
+ e.update(direction)
345
+ changelist[:updated] << e.path
346
+ when :delete
347
+ e = @contents[c["rel_path"]]
348
+ e.delete(direction)
349
+ @contents.delete(e.path)
350
+ changelist[:deleted] << e.path
351
+ else
352
+ raise(RuntimeError, "Unknown operation type: #{op}")
353
+ end
354
+ @db.save
355
+ end
356
+ changelist.keys.each {|k| changelist[k].sort! }
357
+ changelist
358
+ end
359
+
360
+ def merge_changelists(old, new)
361
+ old.merge(new) {|k, v1, v2| (v1 + v2).sort }
312
362
  end
313
363
 
314
- def gather_info(rel, list_contents=true)
364
+ def gather_local_info(rel, list_contents=true)
315
365
  full = @db.relative_to_local_path(rel)
316
366
  remote = @db.relative_to_remote_path(rel)
317
367
 
318
368
  attrs = {
319
369
  "path" => remote,
320
370
  "is_dir" => File.directory?(full),
321
- "modified" => File.mtime(full)
371
+ "modified" => File.mtime(full),
372
+ "revision" => @contents[rel] ? @contents[rel].revision : nil
322
373
  }
323
374
 
324
375
  if attrs["is_dir"] && list_contents
@@ -326,18 +377,23 @@ module Dbox
326
377
  attrs["contents"] = contents.map do |s|
327
378
  p = File.join(full, s)
328
379
  r = @db.local_to_relative_path(p)
329
- gather_info(r, false)
380
+ gather_local_info(r, false)
330
381
  end
331
382
  end
332
383
 
333
384
  attrs
334
385
  end
335
386
 
387
+ def remove_dotfiles(contents)
388
+ contents.reject {|c| File.basename(c["path"]).start_with?(".") }
389
+ end
390
+
336
391
  def dir?
337
392
  true
338
393
  end
339
394
 
340
395
  def create_local
396
+ log.info "Creating #{local_path}"
341
397
  saving_parent_timestamp do
342
398
  FileUtils.mkdir_p(local_path)
343
399
  update_file_timestamp
@@ -345,7 +401,7 @@ module Dbox
345
401
  end
346
402
 
347
403
  def delete_local
348
- log.info "Deleting dir: #{local_path}"
404
+ log.info "Deleting #{local_path}"
349
405
  saving_parent_timestamp do
350
406
  FileUtils.rm_r(local_path)
351
407
  end
@@ -374,7 +430,7 @@ module Dbox
374
430
 
375
431
  def print
376
432
  puts
377
- puts "#{path} (v#{revision}, #{modified_at})"
433
+ puts "#{path} (v#{@revision}, #{@modified_at})"
378
434
  contents.each do |path, c|
379
435
  puts " #{c.path} (v#{c.revision}, #{c.modified_at})"
380
436
  end
@@ -427,7 +483,7 @@ module Dbox
427
483
 
428
484
  def upload
429
485
  File.open(local_path) do |f|
430
- res = api.put_file(remote_path, f)
486
+ api.put_file(remote_path, f)
431
487
  end
432
488
  force_metadata_update_from_server
433
489
  end
data/lib/dbox/loggable.rb CHANGED
@@ -26,7 +26,7 @@ module Dbox
26
26
  Rails.logger
27
27
  else
28
28
  l = Logger.new(STDOUT)
29
- l.level = Logger::INFO
29
+ l.level = (ENV["DEBUG"] && ENV["DEBUG"] != "false") ? Logger::DEBUG : Logger::INFO
30
30
  l.formatter = proc {|severity, datetime, progname, msg| "[#{severity}] #{msg}\n" }
31
31
  l
32
32
  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: 11
4
+ hash: 9
5
5
  prerelease:
6
6
  segments:
7
7
  - 0
8
8
  - 4
9
- - 2
10
- version: 0.4.2
9
+ - 3
10
+ version: 0.4.3
11
11
  platform: ruby
12
12
  authors:
13
13
  - Ken Pratt
@@ -15,7 +15,7 @@ autorequire:
15
15
  bindir: bin
16
16
  cert_chain: []
17
17
 
18
- date: 2011-05-17 00:00:00 Z
18
+ date: 2011-05-19 00:00:00 Z
19
19
  dependencies: []
20
20
 
21
21
  description: An easy-to-use Dropbox client with fine-grained control over syncs.