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 +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.
|