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 +0 -3
- data/VERSION +1 -1
- data/dbox.gemspec +2 -2
- data/lib/dbox/api.rb +3 -1
- data/lib/dbox/db.rb +147 -91
- data/lib/dbox/loggable.rb +1 -1
- metadata +4 -4
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.
|
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.
|
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-
|
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
|
-
|
69
|
-
save
|
70
|
-
res
|
68
|
+
@root.pull
|
71
69
|
end
|
72
70
|
|
73
71
|
def push
|
74
|
-
|
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 =
|
153
|
-
|
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?(
|
200
|
-
!(revision ==
|
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
|
-
|
263
|
-
prev.freeze
|
262
|
+
# calculate changes on this dir
|
264
263
|
res = api.metadata(remote_path)
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
subdirs.inject(
|
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
|
-
|
276
|
-
|
277
|
-
|
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
|
-
|
284
|
-
|
285
|
-
new_paths = contents.keys.sort
|
285
|
+
# execute changes on this dir
|
286
|
+
changelist = execute_changes(changes, :up)
|
286
287
|
|
287
|
-
|
288
|
+
# recur on subdirs, expanding changelist as we go
|
289
|
+
changelist = subdirs.inject(changelist) {|c, d| merge_changelists(c, d.push) }
|
288
290
|
|
289
|
-
|
291
|
+
# only update the modification info on the directory once all descendants are updated
|
292
|
+
update_modification_info(res)
|
290
293
|
|
291
|
-
|
292
|
-
|
294
|
+
# return changes
|
295
|
+
@db.save
|
296
|
+
changelist
|
297
|
+
end
|
293
298
|
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
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(
|
327
|
+
raise(RuntimeError, "Trying to calculate dir changes without any contents")
|
307
328
|
end
|
308
329
|
end
|
309
330
|
|
310
|
-
def
|
311
|
-
|
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
|
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
|
-
|
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
|
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
|
-
|
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:
|
4
|
+
hash: 9
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
8
|
- 4
|
9
|
-
-
|
10
|
-
version: 0.4.
|
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-
|
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.
|