dbox 0.4.2 → 0.4.3

Sign up to get free protection for your applications and to get access to all the features.
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.