dbox 0.6.15 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +5 -0
- data/README.md +0 -9
- data/Rakefile +1 -1
- data/TODO.txt +5 -0
- data/VERSION +1 -1
- data/dbox.gemspec +6 -7
- data/lib/dbox.rb +1 -2
- data/lib/dbox/api.rb +1 -1
- data/lib/dbox/database.rb +61 -14
- data/lib/dbox/syncer.rb +116 -138
- data/lib/dbox/utils.rb +38 -7
- data/spec/dbox_spec.rb +128 -0
- data/spec/spec_helper.rb +5 -0
- metadata +10 -11
- data/lib/dbox/parallel_tasks.rb +0 -80
data/History.txt
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
== 0.7.0 / 2012-11-05
|
2
|
+
* Major Enhancements
|
3
|
+
* Changed to be case insensitive, like Dropbox.
|
4
|
+
* Removed support for concurrent uploads/downloads, as it was often encountering throttling from Dropbox APIs.
|
5
|
+
|
1
6
|
== 0.6.15 / 2012-07-26
|
2
7
|
* Bug Fixes
|
3
8
|
* Fixed issue with gem in 0.6.14.
|
data/README.md
CHANGED
@@ -263,12 +263,3 @@ $ export DROPBOX_AUTH_SECRET=pqej9rmnj0i1gcxr4
|
|
263
263
|
> File.read("#{ENV['HOME']}/Dropbox/Public/hello.txt")
|
264
264
|
=> "Oh, Hello"
|
265
265
|
```
|
266
|
-
|
267
|
-
Advanced
|
268
|
-
--------
|
269
|
-
|
270
|
-
To speed up your syncs, you can manually set the number concurrent dropbox operations to execute. (The default is 2.)
|
271
|
-
|
272
|
-
```sh
|
273
|
-
$ export DROPBOX_CONCURRENCY=5
|
274
|
-
```
|
data/Rakefile
CHANGED
@@ -18,7 +18,7 @@ Jeweler::Tasks.new do |gem|
|
|
18
18
|
gem.add_dependency "oauth", ">= 0.4.5"
|
19
19
|
gem.add_dependency "json", ">= 1.5.3"
|
20
20
|
gem.add_dependency "sqlite3", ">= 1.3.3"
|
21
|
-
gem.add_dependency "
|
21
|
+
gem.add_dependency "insensitive_hash", ">= 0.3.0"
|
22
22
|
end
|
23
23
|
Jeweler::RubygemsDotOrgTasks.new
|
24
24
|
|
data/TODO.txt
CHANGED
@@ -1,3 +1,8 @@
|
|
1
|
+
* Upgrade to newest Dropbox Ruby SDK.
|
2
|
+
* Look into using the new /chunked_upload support instead of custom streaming upload.
|
3
|
+
* Look into using /delta api.
|
4
|
+
* Try to get rid of extra updated dirs on pull after push (have push update all the mtimes and such that it affects, so the pull is clean). -- This will clean up the tests more than anything.
|
5
|
+
* Ensure Dropbox API best practices are being followed as closely as possible (https://www.dropbox.com/developers/reference/bestpractice).
|
1
6
|
* Look into occasional hanging on pull operations.
|
2
7
|
* Look into memory leak when syncing a large amount of files.
|
3
8
|
* Look into 10000 file limit on metadata operations.
|
data/VERSION
CHANGED
@@ -1 +1 @@
|
|
1
|
-
0.
|
1
|
+
0.7.0
|
data/dbox.gemspec
CHANGED
@@ -5,11 +5,11 @@
|
|
5
5
|
|
6
6
|
Gem::Specification.new do |s|
|
7
7
|
s.name = "dbox"
|
8
|
-
s.version = "0.
|
8
|
+
s.version = "0.7.0"
|
9
9
|
|
10
10
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
11
11
|
s.authors = ["Ken Pratt"]
|
12
|
-
s.date = "2012-
|
12
|
+
s.date = "2012-11-06"
|
13
13
|
s.description = "An easy-to-use Dropbox client with fine-grained control over syncs."
|
14
14
|
s.email = "ken@kenpratt.net"
|
15
15
|
s.executables = ["dbox"]
|
@@ -32,7 +32,6 @@ Gem::Specification.new do |s|
|
|
32
32
|
"lib/dbox/database.rb",
|
33
33
|
"lib/dbox/db.rb",
|
34
34
|
"lib/dbox/loggable.rb",
|
35
|
-
"lib/dbox/parallel_tasks.rb",
|
36
35
|
"lib/dbox/syncer.rb",
|
37
36
|
"lib/dbox/utils.rb",
|
38
37
|
"sample_polling_script.rb",
|
@@ -54,7 +53,7 @@ Gem::Specification.new do |s|
|
|
54
53
|
s.homepage = "http://github.com/kenpratt/dbox"
|
55
54
|
s.licenses = ["MIT"]
|
56
55
|
s.require_paths = ["lib"]
|
57
|
-
s.rubygems_version = "1.8.
|
56
|
+
s.rubygems_version = "1.8.24"
|
58
57
|
s.summary = "Dropbox made easy."
|
59
58
|
|
60
59
|
if s.respond_to? :specification_version then
|
@@ -65,20 +64,20 @@ Gem::Specification.new do |s|
|
|
65
64
|
s.add_runtime_dependency(%q<oauth>, [">= 0.4.5"])
|
66
65
|
s.add_runtime_dependency(%q<json>, [">= 1.5.3"])
|
67
66
|
s.add_runtime_dependency(%q<sqlite3>, [">= 1.3.3"])
|
68
|
-
s.add_runtime_dependency(%q<
|
67
|
+
s.add_runtime_dependency(%q<insensitive_hash>, [">= 0.3.0"])
|
69
68
|
else
|
70
69
|
s.add_dependency(%q<multipart-post>, [">= 1.1.2"])
|
71
70
|
s.add_dependency(%q<oauth>, [">= 0.4.5"])
|
72
71
|
s.add_dependency(%q<json>, [">= 1.5.3"])
|
73
72
|
s.add_dependency(%q<sqlite3>, [">= 1.3.3"])
|
74
|
-
s.add_dependency(%q<
|
73
|
+
s.add_dependency(%q<insensitive_hash>, [">= 0.3.0"])
|
75
74
|
end
|
76
75
|
else
|
77
76
|
s.add_dependency(%q<multipart-post>, [">= 1.1.2"])
|
78
77
|
s.add_dependency(%q<oauth>, [">= 0.4.5"])
|
79
78
|
s.add_dependency(%q<json>, [">= 1.5.3"])
|
80
79
|
s.add_dependency(%q<sqlite3>, [">= 1.3.3"])
|
81
|
-
s.add_dependency(%q<
|
80
|
+
s.add_dependency(%q<insensitive_hash>, [">= 0.3.0"])
|
82
81
|
end
|
83
82
|
end
|
84
83
|
|
data/lib/dbox.rb
CHANGED
@@ -9,14 +9,13 @@ require "yaml"
|
|
9
9
|
require "logger"
|
10
10
|
require "cgi"
|
11
11
|
require "sqlite3"
|
12
|
-
require "
|
12
|
+
require "insensitive_hash/minimal"
|
13
13
|
|
14
14
|
require "dbox/loggable"
|
15
15
|
require "dbox/utils"
|
16
16
|
require "dbox/api"
|
17
17
|
require "dbox/database"
|
18
18
|
require "dbox/db"
|
19
|
-
require "dbox/parallel_tasks"
|
20
19
|
require "dbox/syncer"
|
21
20
|
|
22
21
|
module Dbox
|
data/lib/dbox/api.rb
CHANGED
data/lib/dbox/database.rb
CHANGED
@@ -55,20 +55,21 @@ module Dbox
|
|
55
55
|
@db.execute_batch(%{
|
56
56
|
CREATE TABLE IF NOT EXISTS metadata (
|
57
57
|
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
58
|
-
remote_path
|
58
|
+
remote_path text COLLATE NOCASE UNIQUE NOT NULL,
|
59
59
|
version integer NOT NULL
|
60
60
|
);
|
61
61
|
CREATE TABLE IF NOT EXISTS entries (
|
62
62
|
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
63
|
-
path
|
63
|
+
path text COLLATE NOCASE UNIQUE NOT NULL,
|
64
64
|
is_dir boolean NOT NULL,
|
65
65
|
parent_id integer REFERENCES entries(id) ON DELETE CASCADE,
|
66
|
-
local_hash
|
67
|
-
remote_hash
|
66
|
+
local_hash text,
|
67
|
+
remote_hash text,
|
68
68
|
modified datetime,
|
69
|
-
revision
|
69
|
+
revision text
|
70
70
|
);
|
71
71
|
CREATE INDEX IF NOT EXISTS entry_parent_ids ON entries(parent_id);
|
72
|
+
CREATE INDEX IF NOT EXISTS entry_path ON entries(path);
|
72
73
|
})
|
73
74
|
end
|
74
75
|
|
@@ -82,7 +83,7 @@ module Dbox
|
|
82
83
|
ALTER TABLE metadata RENAME TO metadata_old;
|
83
84
|
CREATE TABLE metadata (
|
84
85
|
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
85
|
-
remote_path
|
86
|
+
remote_path text NOT NULL,
|
86
87
|
version integer NOT NULL
|
87
88
|
);
|
88
89
|
INSERT INTO metadata SELECT id, remote_path, version FROM metadata_old;
|
@@ -121,12 +122,12 @@ module Dbox
|
|
121
122
|
ALTER TABLE entries RENAME TO entries_old;
|
122
123
|
CREATE TABLE entries (
|
123
124
|
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
124
|
-
path
|
125
|
+
path text UNIQUE NOT NULL,
|
125
126
|
is_dir boolean NOT NULL,
|
126
127
|
parent_id integer REFERENCES entries(id) ON DELETE CASCADE,
|
127
|
-
hash
|
128
|
+
hash text,
|
128
129
|
modified datetime,
|
129
|
-
revision
|
130
|
+
revision text
|
130
131
|
);
|
131
132
|
INSERT INTO entries SELECT id, path, is_dir, parent_id, hash, modified, null FROM entries_old;
|
132
133
|
})
|
@@ -153,13 +154,13 @@ module Dbox
|
|
153
154
|
ALTER TABLE entries RENAME TO entries_old;
|
154
155
|
CREATE TABLE entries (
|
155
156
|
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
156
|
-
path
|
157
|
+
path text UNIQUE NOT NULL,
|
157
158
|
is_dir boolean NOT NULL,
|
158
159
|
parent_id integer REFERENCES entries(id) ON DELETE CASCADE,
|
159
|
-
local_hash
|
160
|
-
remote_hash
|
160
|
+
local_hash text,
|
161
|
+
remote_hash text,
|
161
162
|
modified datetime,
|
162
|
-
revision
|
163
|
+
revision text
|
163
164
|
);
|
164
165
|
INSERT INTO entries SELECT id, path, is_dir, parent_id, null, hash, modified, revision FROM entries_old;
|
165
166
|
})
|
@@ -181,6 +182,50 @@ module Dbox
|
|
181
182
|
COMMIT;
|
182
183
|
})
|
183
184
|
end
|
185
|
+
|
186
|
+
if metadata[:version] < 5
|
187
|
+
log.info "Migrating to database schema v5"
|
188
|
+
|
189
|
+
# make path be case insensitive
|
190
|
+
@db.execute_batch(%{
|
191
|
+
BEGIN TRANSACTION;
|
192
|
+
|
193
|
+
-- migrate metadata table
|
194
|
+
ALTER TABLE metadata RENAME TO metadata_old;
|
195
|
+
CREATE TABLE IF NOT EXISTS metadata (
|
196
|
+
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
197
|
+
remote_path text COLLATE NOCASE UNIQUE NOT NULL,
|
198
|
+
version integer NOT NULL
|
199
|
+
);
|
200
|
+
INSERT INTO metadata SELECT id, remote_path, version FROM metadata_old;
|
201
|
+
DROP TABLE metadata_old;
|
202
|
+
|
203
|
+
-- migrate entries table
|
204
|
+
ALTER TABLE entries RENAME TO entries_old;
|
205
|
+
CREATE TABLE entries (
|
206
|
+
id integer PRIMARY KEY AUTOINCREMENT NOT NULL,
|
207
|
+
path text COLLATE NOCASE UNIQUE NOT NULL,
|
208
|
+
is_dir boolean NOT NULL,
|
209
|
+
parent_id integer REFERENCES entries(id) ON DELETE CASCADE,
|
210
|
+
local_hash text,
|
211
|
+
remote_hash text,
|
212
|
+
modified datetime,
|
213
|
+
revision text
|
214
|
+
);
|
215
|
+
INSERT INTO entries SELECT id, path, is_dir, parent_id, local_hash, remote_hash, modified, revision FROM entries_old;
|
216
|
+
DROP TABLE entries_old;
|
217
|
+
|
218
|
+
-- recreate indexes
|
219
|
+
DROP INDEX IF EXISTS entry_parent_ids;
|
220
|
+
DROP INDEX IF EXISTS entry_path;
|
221
|
+
CREATE INDEX entry_parent_ids ON entries(parent_id);
|
222
|
+
CREATE INDEX entry_path ON entries(path);
|
223
|
+
|
224
|
+
-- update version
|
225
|
+
UPDATE metadata SET version = 5;
|
226
|
+
COMMIT;
|
227
|
+
})
|
228
|
+
end
|
184
229
|
end
|
185
230
|
|
186
231
|
METADATA_COLS = [ :remote_path, :version ] # don't need to return id
|
@@ -189,7 +234,7 @@ module Dbox
|
|
189
234
|
def bootstrap(remote_path)
|
190
235
|
@db.execute(%{
|
191
236
|
INSERT INTO metadata (remote_path, version) VALUES (?, ?);
|
192
|
-
}, remote_path,
|
237
|
+
}, remote_path, 5)
|
193
238
|
@db.execute(%{
|
194
239
|
INSERT INTO entries (path, is_dir) VALUES (?, ?)
|
195
240
|
}, "", 1)
|
@@ -341,6 +386,8 @@ module Dbox
|
|
341
386
|
h = make_fields(entry_cols, res)
|
342
387
|
h[:is_dir] = (h[:is_dir] == 1)
|
343
388
|
h[:modified] = Time.at(h[:modified]) if h[:modified]
|
389
|
+
h[:local_path] = relative_to_local_path(h[:path])
|
390
|
+
h[:remote_path] = relative_to_remote_path(h[:path])
|
344
391
|
h
|
345
392
|
else
|
346
393
|
nil
|
data/lib/dbox/syncer.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
module Dbox
|
2
2
|
class Syncer
|
3
|
-
DEFAULT_CONCURRENCY = 2
|
4
3
|
MIN_BYTES_TO_STREAM_DOWNLOAD = 1024 * 100 # 100kB
|
5
4
|
|
6
5
|
include Loggable
|
@@ -36,11 +35,6 @@ module Dbox
|
|
36
35
|
@@_api ||= API.connect
|
37
36
|
end
|
38
37
|
|
39
|
-
def self.concurrency
|
40
|
-
n = ENV["DROPBOX_CONCURRENCY"].to_i
|
41
|
-
n > 0 ? n : DEFAULT_CONCURRENCY
|
42
|
-
end
|
43
|
-
|
44
38
|
class Operation
|
45
39
|
include Loggable
|
46
40
|
include Utils
|
@@ -53,11 +47,7 @@ module Dbox
|
|
53
47
|
end
|
54
48
|
|
55
49
|
def api
|
56
|
-
|
57
|
-
end
|
58
|
-
|
59
|
-
def clone_api_into_current_thread
|
60
|
-
Thread.current[:api] = api.clone()
|
50
|
+
@api
|
61
51
|
end
|
62
52
|
|
63
53
|
def metadata
|
@@ -78,7 +68,7 @@ module Dbox
|
|
78
68
|
|
79
69
|
def current_dir_entries_as_hash(dir)
|
80
70
|
if dir[:id]
|
81
|
-
out =
|
71
|
+
out = InsensitiveHash.new
|
82
72
|
database.contents(dir[:id]).each {|e| out[e[:path]] = e }
|
83
73
|
out
|
84
74
|
else
|
@@ -99,17 +89,20 @@ module Dbox
|
|
99
89
|
end
|
100
90
|
|
101
91
|
def saving_parent_timestamp(entry, &proc)
|
102
|
-
|
103
|
-
parent = File.dirname(local_path)
|
92
|
+
parent = File.dirname(entry[:local_path])
|
104
93
|
saving_timestamp(parent, &proc)
|
105
94
|
end
|
106
95
|
|
107
96
|
def update_file_timestamp(entry)
|
108
|
-
|
97
|
+
begin
|
98
|
+
File.utime(Time.now, entry[:modified], entry[:local_path])
|
99
|
+
rescue Errno::ENOENT
|
100
|
+
nil
|
101
|
+
end
|
109
102
|
end
|
110
103
|
|
111
104
|
def gather_remote_info(entry)
|
112
|
-
res = api.metadata(
|
105
|
+
res = api.metadata(entry[:remote_path], entry[:remote_hash])
|
113
106
|
case res
|
114
107
|
when Hash
|
115
108
|
out = process_basic_remote_props(res)
|
@@ -133,6 +126,8 @@ module Dbox
|
|
133
126
|
def process_basic_remote_props(res)
|
134
127
|
out = {}
|
135
128
|
out[:path] = remote_to_relative_path(res[:path])
|
129
|
+
out[:local_path] = relative_to_local_path(out[:path])
|
130
|
+
out[:remote_path] = relative_to_remote_path(out[:path])
|
136
131
|
out[:modified] = parse_time(res[:modified])
|
137
132
|
out[:is_dir] = res[:is_dir]
|
138
133
|
out[:remote_hash] = res[:hash] if res[:hash]
|
@@ -188,62 +183,57 @@ module Dbox
|
|
188
183
|
parent_ids_of_failed_entries = []
|
189
184
|
changelist = { :created => [], :deleted => [], :updated => [], :failed => [] }
|
190
185
|
|
191
|
-
# spin up a parallel task queue
|
192
|
-
ptasks = ParallelTasks.new(Syncer.concurrency) { clone_api_into_current_thread() }
|
193
|
-
ptasks.start
|
194
|
-
|
195
186
|
changes.each do |op, c|
|
196
187
|
case op
|
197
188
|
when :create
|
198
189
|
c[:parent_id] ||= lookup_id_by_path(c[:parent_path])
|
199
190
|
if c[:is_dir]
|
200
|
-
#
|
201
|
-
# operations might depend on the directory being there
|
191
|
+
# create the local directory
|
202
192
|
create_dir(c)
|
203
193
|
database.add_entry(c[:path], true, c[:parent_id], c[:modified], c[:revision], c[:remote_hash], nil)
|
204
194
|
changelist[:created] << c[:path]
|
205
195
|
else
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
end
|
216
|
-
rescue Exception => e
|
217
|
-
log.error "Error while downloading #{c[:path]}: #{e.inspect}\n#{e.backtrace.join("\n")}"
|
218
|
-
parent_ids_of_failed_entries << c[:parent_id]
|
219
|
-
changelist[:failed] << { :operation => :create, :path => c[:path], :error => e }
|
196
|
+
# download the new file
|
197
|
+
begin
|
198
|
+
res = create_file(c)
|
199
|
+
local_hash = calculate_hash(c[:local_path])
|
200
|
+
database.add_entry(c[:path], false, c[:parent_id], c[:modified], c[:revision], c[:remote_hash], local_hash)
|
201
|
+
changelist[:created] << c[:path]
|
202
|
+
if res.kind_of?(Array) && res[0] == :conflict
|
203
|
+
changelist[:conflicts] ||= []
|
204
|
+
changelist[:conflicts] << res[1]
|
220
205
|
end
|
206
|
+
rescue Exception => e
|
207
|
+
log.error "Error while downloading #{c[:path]}: #{e.inspect}\n#{e.backtrace.join("\n")}"
|
208
|
+
parent_ids_of_failed_entries << c[:parent_id]
|
209
|
+
changelist[:failed] << { :operation => :create, :path => c[:path], :error => e }
|
221
210
|
end
|
222
211
|
end
|
223
212
|
when :update
|
224
213
|
if c[:is_dir]
|
214
|
+
# update the local directory
|
225
215
|
update_dir(c)
|
226
216
|
database.update_entry_by_path(c[:path], :modified => c[:modified], :revision => c[:revision], :remote_hash => c[:remote_hash])
|
227
217
|
changelist[:updated] << c[:path]
|
228
218
|
else
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
end
|
239
|
-
rescue Exception => e
|
240
|
-
log.error "Error while downloading #{c[:path]}: #{e.inspect}\n#{e.backtrace.join("\n")}"
|
241
|
-
parent_ids_of_failed_entries << c[:parent_id]
|
242
|
-
changelist[:failed] << { :operation => :create, :path => c[:path], :error => e }
|
219
|
+
# download updates to the file
|
220
|
+
begin
|
221
|
+
res = update_file(c)
|
222
|
+
local_hash = calculate_hash(c[:local_path])
|
223
|
+
database.update_entry_by_path(c[:path], :modified => c[:modified], :revision => c[:revision], :remote_hash => c[:remote_hash], :local_hash => local_hash)
|
224
|
+
changelist[:updated] << c[:path]
|
225
|
+
if res.kind_of?(Array) && res[0] == :conflict
|
226
|
+
changelist[:conflicts] ||= []
|
227
|
+
changelist[:conflicts] << res[1]
|
243
228
|
end
|
229
|
+
rescue Exception => e
|
230
|
+
log.error "Error while downloading #{c[:path]}: #{e.inspect}\n#{e.backtrace.join("\n")}"
|
231
|
+
parent_ids_of_failed_entries << c[:parent_id]
|
232
|
+
changelist[:failed] << { :operation => :create, :path => c[:path], :error => e }
|
244
233
|
end
|
245
234
|
end
|
246
235
|
when :delete
|
236
|
+
# delete the local directory/file
|
247
237
|
c[:is_dir] ? delete_dir(c) : delete_file(c)
|
248
238
|
database.delete_entry_by_path(c[:path])
|
249
239
|
changelist[:deleted] << c[:path]
|
@@ -255,9 +245,6 @@ module Dbox
|
|
255
245
|
end
|
256
246
|
end
|
257
247
|
|
258
|
-
# wait for operations to finish
|
259
|
-
ptasks.finish
|
260
|
-
|
261
248
|
# clear hashes on any dirs with children that failed so that
|
262
249
|
# they are processed again on next pull
|
263
250
|
parent_ids_of_failed_entries.uniq.each do |id|
|
@@ -320,24 +307,20 @@ module Dbox
|
|
320
307
|
end
|
321
308
|
|
322
309
|
# add any deletions
|
323
|
-
out += (existing_entries.keys
|
310
|
+
out += case_insensitive_difference(existing_entries.keys, found_paths).map do |p|
|
324
311
|
[:delete, existing_entries[p]]
|
325
312
|
end
|
326
313
|
end
|
327
314
|
|
328
315
|
# recursively process new & existing subdirectories in parallel
|
329
|
-
|
330
|
-
|
331
|
-
|
332
|
-
|
333
|
-
|
334
|
-
|
335
|
-
log.error "Error while caclulating changes for #{operation} on #{dir[:path]}: #{e.inspect}\n#{e.backtrace.join("\n")}"
|
336
|
-
Thread.current[:out] = [[:failed, dir.merge({ :operation => operation, :error => e })]]
|
337
|
-
end
|
316
|
+
recur_dirs.each do |operation, dir|
|
317
|
+
begin
|
318
|
+
out += calculate_changes(dir, operation)
|
319
|
+
rescue Exception => e
|
320
|
+
log.error "Error while caclulating changes for #{operation} on #{dir[:path]}: #{e.inspect}\n#{e.backtrace.join("\n")}"
|
321
|
+
out += [[:failed, dir.merge({ :operation => operation, :error => e })]]
|
338
322
|
end
|
339
323
|
end
|
340
|
-
threads.each {|t| t.join; out += t[:out] }
|
341
324
|
|
342
325
|
out
|
343
326
|
end
|
@@ -351,7 +334,7 @@ module Dbox
|
|
351
334
|
end
|
352
335
|
|
353
336
|
def create_dir(dir)
|
354
|
-
local_path =
|
337
|
+
local_path = dir[:local_path]
|
355
338
|
log.info "Creating #{local_path}"
|
356
339
|
saving_parent_timestamp(dir) do
|
357
340
|
FileUtils.mkdir_p(local_path)
|
@@ -364,7 +347,7 @@ module Dbox
|
|
364
347
|
end
|
365
348
|
|
366
349
|
def delete_dir(dir)
|
367
|
-
local_path =
|
350
|
+
local_path = dir[:local_path]
|
368
351
|
log.info "Deleting #{local_path}"
|
369
352
|
saving_parent_timestamp(dir) do
|
370
353
|
FileUtils.rm_r(local_path)
|
@@ -382,7 +365,7 @@ module Dbox
|
|
382
365
|
end
|
383
366
|
|
384
367
|
def delete_file(file)
|
385
|
-
local_path =
|
368
|
+
local_path = file[:local_path]
|
386
369
|
log.info "Deleting file: #{local_path}"
|
387
370
|
saving_parent_timestamp(file) do
|
388
371
|
FileUtils.rm_rf(local_path)
|
@@ -390,8 +373,8 @@ module Dbox
|
|
390
373
|
end
|
391
374
|
|
392
375
|
def download_file(file)
|
393
|
-
local_path =
|
394
|
-
remote_path =
|
376
|
+
local_path = file[:local_path]
|
377
|
+
remote_path = file[:remote_path]
|
395
378
|
|
396
379
|
# check to ensure we aren't overwriting an untracked file or a
|
397
380
|
# file with local modifications
|
@@ -449,41 +432,34 @@ module Dbox
|
|
449
432
|
log.debug "Executing changes:\n" + changes.map {|c| c.inspect }.join("\n")
|
450
433
|
changelist = { :created => [], :deleted => [], :updated => [], :failed => [] }
|
451
434
|
|
452
|
-
# spin up a parallel task queue
|
453
|
-
ptasks = ParallelTasks.new(Syncer.concurrency) { clone_api_into_current_thread() }
|
454
|
-
ptasks.start
|
455
|
-
|
456
435
|
changes.each do |op, c|
|
457
436
|
case op
|
458
437
|
when :create
|
459
438
|
c[:parent_id] ||= lookup_id_by_path(c[:parent_path])
|
460
439
|
|
461
440
|
if c[:is_dir]
|
462
|
-
#
|
463
|
-
# operations might depend on the directory being there
|
441
|
+
# create the remote directiory
|
464
442
|
create_dir(c)
|
465
443
|
database.add_entry(c[:path], true, c[:parent_id], nil, nil, nil, nil)
|
466
444
|
force_metadata_update_from_server(c)
|
467
445
|
changelist[:created] << c[:path]
|
468
446
|
else
|
469
|
-
#
|
470
|
-
|
471
|
-
|
472
|
-
|
473
|
-
|
474
|
-
|
475
|
-
|
476
|
-
|
477
|
-
|
478
|
-
|
479
|
-
|
480
|
-
|
481
|
-
changelist[:conflicts] << { :original => c[:path], :renamed => res[:path] }
|
482
|
-
end
|
483
|
-
rescue Exception => e
|
484
|
-
log.error "Error while uploading #{c[:path]}: #{e.inspect}\n#{e.backtrace.join("\n")}"
|
485
|
-
changelist[:failed] << { :operation => :create, :path => c[:path], :error => e }
|
447
|
+
# upload a new file
|
448
|
+
begin
|
449
|
+
local_hash = calculate_hash(c[:local_path])
|
450
|
+
res = upload_file(c)
|
451
|
+
database.add_entry(c[:path], false, c[:parent_id], nil, nil, nil, local_hash)
|
452
|
+
if case_insensitive_equal(c[:path], res[:path])
|
453
|
+
force_metadata_update_from_server(c)
|
454
|
+
changelist[:created] << c[:path]
|
455
|
+
else
|
456
|
+
log.warn "#{c[:path]} had a conflict and was renamed to #{res[:path]} on the server"
|
457
|
+
changelist[:conflicts] ||= []
|
458
|
+
changelist[:conflicts] << { :original => c[:path], :renamed => res[:path] }
|
486
459
|
end
|
460
|
+
rescue Exception => e
|
461
|
+
log.error "Error while uploading #{c[:path]}: #{e.inspect}\n#{e.backtrace.join("\n")}"
|
462
|
+
changelist[:failed] << { :operation => :create, :path => c[:path], :error => e }
|
487
463
|
end
|
488
464
|
end
|
489
465
|
when :update
|
@@ -494,46 +470,41 @@ module Dbox
|
|
494
470
|
|
495
471
|
# only update files -- nothing to do to update a dir
|
496
472
|
if !c[:is_dir]
|
497
|
-
|
498
|
-
|
499
|
-
|
500
|
-
|
501
|
-
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
506
|
-
|
507
|
-
|
508
|
-
|
509
|
-
changelist[:conflicts] ||= []
|
510
|
-
changelist[:conflicts] << { :original => c[:path], :renamed => res[:path] }
|
511
|
-
end
|
512
|
-
rescue Exception => e
|
513
|
-
log.error "Error while uploading #{c[:path]}: #{e.inspect}\n#{e.backtrace.join("\n")}"
|
514
|
-
changelist[:failed] << { :operation => :update, :path => c[:path], :error => e }
|
473
|
+
# upload changes to a file
|
474
|
+
begin
|
475
|
+
local_hash = calculate_hash(c[:local_path])
|
476
|
+
res = upload_file(c)
|
477
|
+
database.update_entry_by_path(c[:path], :local_hash => local_hash)
|
478
|
+
if case_insensitive_equal(c[:path], res[:path])
|
479
|
+
force_metadata_update_from_server(c)
|
480
|
+
changelist[:updated] << c[:path]
|
481
|
+
else
|
482
|
+
log.warn "#{c[:path]} had a conflict and was renamed to #{res[:path]} on the server"
|
483
|
+
changelist[:conflicts] ||= []
|
484
|
+
changelist[:conflicts] << { :original => c[:path], :renamed => res[:path] }
|
515
485
|
end
|
486
|
+
rescue Exception => e
|
487
|
+
log.error "Error while uploading #{c[:path]}: #{e.inspect}\n#{e.backtrace.join("\n")}"
|
488
|
+
changelist[:failed] << { :operation => :update, :path => c[:path], :error => e }
|
516
489
|
end
|
517
490
|
end
|
518
491
|
when :delete
|
519
|
-
#
|
520
|
-
|
492
|
+
# delete a remote file/directory
|
493
|
+
begin
|
521
494
|
begin
|
522
|
-
|
523
|
-
|
524
|
-
|
525
|
-
|
526
|
-
delete_file(c)
|
527
|
-
end
|
528
|
-
rescue Dbox::RemoteMissing
|
529
|
-
# safe to delete even if remote is already gone
|
495
|
+
if c[:is_dir]
|
496
|
+
delete_dir(c)
|
497
|
+
else
|
498
|
+
delete_file(c)
|
530
499
|
end
|
531
|
-
|
532
|
-
|
533
|
-
rescue Exception => e
|
534
|
-
log.error "Error while deleting #{c[:path]}: #{e.inspect}\n#{e.backtrace.join("\n")}"
|
535
|
-
changelist[:failed] << { :operation => :delete, :path => c[:path], :error => e }
|
500
|
+
rescue Dbox::RemoteMissing
|
501
|
+
# safe to delete even if remote is already gone
|
536
502
|
end
|
503
|
+
database.delete_entry_by_path(c[:path])
|
504
|
+
changelist[:deleted] << c[:path]
|
505
|
+
rescue Exception => e
|
506
|
+
log.error "Error while deleting #{c[:path]}: #{e.inspect}\n#{e.backtrace.join("\n")}"
|
507
|
+
changelist[:failed] << { :operation => :delete, :path => c[:path], :error => e }
|
537
508
|
end
|
538
509
|
when :failed
|
539
510
|
changelist[:failed] << { :operation => c[:operation], :path => c[:path], :error => c[:error] }
|
@@ -542,9 +513,6 @@ module Dbox
|
|
542
513
|
end
|
543
514
|
end
|
544
515
|
|
545
|
-
# wait for operations to finish
|
546
|
-
ptasks.finish
|
547
|
-
|
548
516
|
# sort & return output
|
549
517
|
sort_changelist(changelist)
|
550
518
|
end
|
@@ -559,7 +527,17 @@ module Dbox
|
|
559
527
|
child_paths = list_contents(dir).sort
|
560
528
|
|
561
529
|
child_paths.each do |p|
|
562
|
-
|
530
|
+
local_path = relative_to_local_path(p)
|
531
|
+
remote_path = relative_to_remote_path(p)
|
532
|
+
c = {
|
533
|
+
:path => p,
|
534
|
+
:local_path => local_path,
|
535
|
+
:remote_path => remote_path,
|
536
|
+
:modified => mtime(local_path),
|
537
|
+
:is_dir => is_dir(local_path),
|
538
|
+
:parent_path => dir[:path],
|
539
|
+
:local_hash => calculate_hash(local_path)
|
540
|
+
}
|
563
541
|
if entry = existing_entries[p]
|
564
542
|
c[:id] = entry[:id]
|
565
543
|
recur_dirs << c if c[:is_dir] # queue dir for later
|
@@ -572,7 +550,7 @@ module Dbox
|
|
572
550
|
end
|
573
551
|
|
574
552
|
# add any deletions
|
575
|
-
out += (existing_entries.keys
|
553
|
+
out += case_insensitive_difference(existing_entries.keys, child_paths).map do |p|
|
576
554
|
[:delete, existing_entries[p]]
|
577
555
|
end
|
578
556
|
|
@@ -585,11 +563,11 @@ module Dbox
|
|
585
563
|
end
|
586
564
|
|
587
565
|
def mtime(path)
|
588
|
-
File.mtime(
|
566
|
+
File.mtime(path)
|
589
567
|
end
|
590
568
|
|
591
569
|
def is_dir(path)
|
592
|
-
File.directory?(
|
570
|
+
File.directory?(path)
|
593
571
|
end
|
594
572
|
|
595
573
|
def modified?(entry, res)
|
@@ -607,30 +585,30 @@ module Dbox
|
|
607
585
|
end
|
608
586
|
|
609
587
|
def list_contents(dir)
|
610
|
-
local_path =
|
588
|
+
local_path = dir[:local_path]
|
611
589
|
paths = Dir.entries(local_path).reject {|s| s == "." || s == ".." || s.start_with?(".") }
|
612
590
|
paths.map {|p| local_to_relative_path(File.join(local_path, p)) }
|
613
591
|
end
|
614
592
|
|
615
593
|
def create_dir(dir)
|
616
|
-
remote_path =
|
594
|
+
remote_path = dir[:remote_path]
|
617
595
|
log.info "Creating #{remote_path}"
|
618
596
|
api.create_dir(remote_path)
|
619
597
|
end
|
620
598
|
|
621
599
|
def delete_dir(dir)
|
622
|
-
remote_path =
|
600
|
+
remote_path = dir[:remote_path]
|
623
601
|
api.delete_dir(remote_path)
|
624
602
|
end
|
625
603
|
|
626
604
|
def delete_file(file)
|
627
|
-
remote_path =
|
605
|
+
remote_path = file[:remote_path]
|
628
606
|
api.delete_file(remote_path)
|
629
607
|
end
|
630
608
|
|
631
609
|
def upload_file(file)
|
632
|
-
local_path =
|
633
|
-
remote_path =
|
610
|
+
local_path = file[:local_path]
|
611
|
+
remote_path = file[:remote_path]
|
634
612
|
db_entry = database.find_by_path(file[:path])
|
635
613
|
last_revision = db_entry ? db_entry[:revision] : nil
|
636
614
|
res = api.put_file(remote_path, local_path, last_revision)
|
data/lib/dbox/utils.rb
CHANGED
@@ -25,8 +25,8 @@ module Dbox
|
|
25
25
|
|
26
26
|
# assumes local_path is defined
|
27
27
|
def local_to_relative_path(path)
|
28
|
-
if path
|
29
|
-
|
28
|
+
if path =~ /^#{local_path}\/?(.*)$/i
|
29
|
+
$1
|
30
30
|
else
|
31
31
|
raise BadPath, "Not a local path: #{path}"
|
32
32
|
end
|
@@ -34,8 +34,8 @@ module Dbox
|
|
34
34
|
|
35
35
|
# assumes remote_path is defined
|
36
36
|
def remote_to_relative_path(path)
|
37
|
-
if path
|
38
|
-
|
37
|
+
if path =~ /^#{remote_path}\/?(.*)$/i
|
38
|
+
$1
|
39
39
|
else
|
40
40
|
raise BadPath, "Not a remote path: #{path}"
|
41
41
|
end
|
@@ -44,9 +44,9 @@ module Dbox
|
|
44
44
|
# assumes local_path is defined
|
45
45
|
def relative_to_local_path(path)
|
46
46
|
if path && path.length > 0
|
47
|
-
|
47
|
+
case_insensitive_join(local_path, path)
|
48
48
|
else
|
49
|
-
local_path
|
49
|
+
case_insensitive_resolve(local_path)
|
50
50
|
end
|
51
51
|
end
|
52
52
|
|
@@ -59,6 +59,37 @@ module Dbox
|
|
59
59
|
end
|
60
60
|
end
|
61
61
|
|
62
|
+
def case_insensitive_resolve(path)
|
63
|
+
if File.exists?(path)
|
64
|
+
path
|
65
|
+
else
|
66
|
+
matches = Dir.glob(path, File::FNM_CASEFOLD)
|
67
|
+
case matches.size
|
68
|
+
when 0 then path
|
69
|
+
when 1 then matches.first
|
70
|
+
else raise(RuntimeError, "Oops, you have multiple files with the same case. Please delete one of them, as Dropbox is case insensitive. (#{matches.join(', ')})")
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
def case_insensitive_join(path, *rest)
|
76
|
+
if rest.length == 0
|
77
|
+
case_insensitive_resolve(path)
|
78
|
+
else
|
79
|
+
rest = rest.map {|s| s.split(File::SEPARATOR) }.flatten
|
80
|
+
case_insensitive_join(File.join(case_insensitive_resolve(path), rest[0]), *rest[1..-1])
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def case_insensitive_difference(a, b)
|
85
|
+
b = b.map(&:downcase).sort
|
86
|
+
a.reject {|s| b.include?(s.downcase) }
|
87
|
+
end
|
88
|
+
|
89
|
+
def case_insensitive_equal(a, b)
|
90
|
+
a && b && a.downcase == b.downcase
|
91
|
+
end
|
92
|
+
|
62
93
|
def calculate_hash(filepath)
|
63
94
|
begin
|
64
95
|
Digest::MD5.file(filepath).to_s
|
@@ -71,7 +102,7 @@ module Dbox
|
|
71
102
|
|
72
103
|
def find_nonconflicting_path(filepath)
|
73
104
|
proposed = filepath
|
74
|
-
while File.exists?(proposed)
|
105
|
+
while File.exists?(case_insensitive_resolve(proposed))
|
75
106
|
dir, p = File.split(proposed)
|
76
107
|
p = p.sub(/^(.*?)( \((\d+)\))?(\..*?)?$/) { "#{$1} (#{$3 ? $3.to_i + 1 : 1})#{$4}" }
|
77
108
|
proposed = File.join(dir, p)
|
data/spec/dbox_spec.rb
CHANGED
@@ -460,6 +460,134 @@ describe Dbox do
|
|
460
460
|
Dbox.sync(@local).should eql(:pull => { :created => ["au_revoir.txt", "farewell.txt", "hello (1).txt"], :deleted => [], :updated => ["", "goodbye.txt"], :failed => [] },
|
461
461
|
:push => { :created => [], :deleted => [], :updated => [], :failed => [] })
|
462
462
|
end
|
463
|
+
|
464
|
+
it "should be able to handle a file that has changed case" do
|
465
|
+
Dbox.create(@remote, @local)
|
466
|
+
make_file "#{@local}/hello.txt"
|
467
|
+
Dbox.sync(@local).should eql(:pull => { :created => [], :deleted => [], :updated => [], :failed => [] },
|
468
|
+
:push => { :created => ["hello.txt"], :deleted => [], :updated => [], :failed => [] })
|
469
|
+
rename_file "#{@local}/hello.txt", "#{@local}/HELLO.txt"
|
470
|
+
Dbox.sync(@local).should eql(:pull => { :created => [], :deleted => [], :updated => [""], :failed => [] },
|
471
|
+
:push => { :created => [], :deleted => [], :updated => [], :failed => [] })
|
472
|
+
end
|
473
|
+
|
474
|
+
it "should be able to handle a file that has changed case remotely" do
|
475
|
+
Dbox.create(@remote, @local)
|
476
|
+
@alternate = "#{ALTERNATE_LOCAL_TEST_PATH}/#{@name}"
|
477
|
+
Dbox.clone(@remote, @alternate)
|
478
|
+
make_file "#{@local}/hello.txt"
|
479
|
+
Dbox.sync(@local).should eql(:pull => { :created => [], :deleted => [], :updated => [], :failed => [] },
|
480
|
+
:push => { :created => ["hello.txt"], :deleted => [], :updated => [], :failed => [] })
|
481
|
+
Dbox.sync(@alternate).should eql(:pull => { :created => ["hello.txt"], :deleted => [], :updated => [""], :failed => [] },
|
482
|
+
:push => { :created => [], :deleted => [], :updated => [], :failed => [] })
|
483
|
+
rename_file "#{@local}/hello.txt", "#{@local}/HELLO.txt"
|
484
|
+
Dbox.sync(@local).should eql(:pull => { :created => [], :deleted => [], :updated => [""], :failed => [] },
|
485
|
+
:push => { :created => [], :deleted => [], :updated => [], :failed => [] })
|
486
|
+
Dbox.sync(@alternate).should eql(:pull => { :created => [], :deleted => [], :updated => [], :failed => [] },
|
487
|
+
:push => { :created => [], :deleted => [], :updated => [], :failed => [] })
|
488
|
+
end
|
489
|
+
|
490
|
+
it "should be able to handle a folder that has changed case" do
|
491
|
+
Dbox.create(@remote, @local)
|
492
|
+
mkdir "#{@local}/foo"
|
493
|
+
make_file "#{@local}/foo/hello.txt"
|
494
|
+
Dbox.sync(@local).should eql(:pull => { :created => [], :deleted => [], :updated => [], :failed => [] },
|
495
|
+
:push => { :created => ["foo", "foo/hello.txt"], :deleted => [], :updated => [], :failed => [] })
|
496
|
+
rename_file "#{@local}/foo", "#{@local}/FOO"
|
497
|
+
Dbox.sync(@local).should eql(:pull => { :created => [], :deleted => [], :updated => ["", "foo"], :failed => [] },
|
498
|
+
:push => { :created => [], :deleted => [], :updated => [], :failed => [] })
|
499
|
+
make_file "#{@local}/FOO/hello2.txt"
|
500
|
+
Dbox.sync(@local).should eql(:pull => { :created => [], :deleted => [], :updated => [], :failed => [] },
|
501
|
+
:push => { :created => ["FOO/hello2.txt"], :deleted => [], :updated => [], :failed => [] })
|
502
|
+
end
|
503
|
+
|
504
|
+
it "should be able to handle a folder that has changed case remotely" do
|
505
|
+
Dbox.create(@remote, @local)
|
506
|
+
@alternate = "#{ALTERNATE_LOCAL_TEST_PATH}/#{@name}"
|
507
|
+
Dbox.clone(@remote, @alternate)
|
508
|
+
mkdir "#{@local}/foo"
|
509
|
+
make_file "#{@local}/foo/hello.txt"
|
510
|
+
Dbox.sync(@local).should eql(:pull => { :created => [], :deleted => [], :updated => [], :failed => [] },
|
511
|
+
:push => { :created => ["foo", "foo/hello.txt"], :deleted => [], :updated => [], :failed => [] })
|
512
|
+
Dbox.sync(@alternate).should eql(:pull => { :created => ["foo", "foo/hello.txt"], :deleted => [], :updated => [""], :failed => [] },
|
513
|
+
:push => { :created => [], :deleted => [], :updated => [], :failed => [] })
|
514
|
+
rename_file "#{@local}/foo", "#{@local}/FOO"
|
515
|
+
make_file "#{@local}/FOO/hello2.txt"
|
516
|
+
make_file "#{@alternate}/foo/hello3.txt"
|
517
|
+
Dbox.sync(@local).should eql(:pull => { :created => [], :deleted => [], :updated => ["", "foo"], :failed => [] },
|
518
|
+
:push => { :created => ["FOO/hello2.txt"], :deleted => [], :updated => [], :failed => [] })
|
519
|
+
Dbox.sync(@alternate).should eql(:pull => { :created => ["foo/hello2.txt"], :deleted => [], :updated => ["foo"], :failed => [] },
|
520
|
+
:push => { :created => ["foo/hello3.txt"], :deleted => [], :updated => [], :failed => [] })
|
521
|
+
Dbox.sync(@local).should eql(:pull => { :created => ["foo/hello3.txt"], :deleted => [], :updated => ["foo"], :failed => [] },
|
522
|
+
:push => { :created => [], :deleted => [], :updated => [], :failed => [] })
|
523
|
+
end
|
524
|
+
|
525
|
+
it "should be able to handle creating a new file of a different case from a deleted file" do
|
526
|
+
Dbox.create(@remote, @local)
|
527
|
+
mkdir "#{@local}/foo"
|
528
|
+
make_file "#{@local}/foo/hello.txt"
|
529
|
+
Dbox.sync(@local).should eql(:pull => { :created => [], :deleted => [], :updated => [], :failed => [] },
|
530
|
+
:push => { :created => ["foo", "foo/hello.txt"], :deleted => [], :updated => [], :failed => [] })
|
531
|
+
rm_rf "#{@local}/foo"
|
532
|
+
Dbox.sync(@local).should eql(:pull => { :created => [], :deleted => [], :updated => ["", "foo"], :failed => [] },
|
533
|
+
:push => { :created => [], :deleted => ["foo"], :updated => [], :failed => [] })
|
534
|
+
mkdir "#{@local}/FOO"
|
535
|
+
make_file "#{@local}/FOO/HELLO.txt"
|
536
|
+
Dbox.sync(@local).should eql(:pull => { :created => [], :deleted => [], :updated => [""], :failed => [] },
|
537
|
+
:push => { :created => ["FOO", "FOO/HELLO.txt"], :deleted => [], :updated => [], :failed => [] })
|
538
|
+
Dbox.sync(@local).should eql(:pull => { :created => [], :deleted => [], :updated => ["", "FOO"], :failed => [] },
|
539
|
+
:push => { :created => [], :deleted => [], :updated => [], :failed => [] })
|
540
|
+
end
|
541
|
+
|
542
|
+
it "should be able to handle creating a new file of a different case from a deleted file remotely" do
|
543
|
+
Dbox.create(@remote, @local)
|
544
|
+
@alternate = "#{ALTERNATE_LOCAL_TEST_PATH}/#{@name}"
|
545
|
+
Dbox.clone(@remote, @alternate)
|
546
|
+
|
547
|
+
mkdir "#{@local}/foo"
|
548
|
+
make_file "#{@local}/foo/hello.txt"
|
549
|
+
Dbox.sync(@local).should eql(:pull => { :created => [], :deleted => [], :updated => [], :failed => [] },
|
550
|
+
:push => { :created => ["foo", "foo/hello.txt"], :deleted => [], :updated => [], :failed => [] })
|
551
|
+
Dbox.sync(@alternate).should eql(:pull => { :created => ["foo", "foo/hello.txt"], :deleted => [], :updated => [""], :failed => [] },
|
552
|
+
:push => { :created => [], :deleted => [], :updated => [], :failed => [] })
|
553
|
+
rm_rf "#{@alternate}/foo"
|
554
|
+
Dbox.sync(@alternate).should eql(:pull => { :created => [], :deleted => [], :updated => [], :failed => [] },
|
555
|
+
:push => { :created => [], :deleted => ["foo"], :updated => [], :failed => [] })
|
556
|
+
mkdir "#{@alternate}/FOO"
|
557
|
+
make_file "#{@alternate}/FOO/HELLO.txt"
|
558
|
+
make_file "#{@alternate}/FOO/HELLO2.txt"
|
559
|
+
Dbox.sync(@alternate).should eql(:pull => { :created => [], :deleted => [], :updated => [""], :failed => [] },
|
560
|
+
:push => { :created => ["FOO", "FOO/HELLO.txt", "FOO/HELLO2.txt"], :deleted => [], :updated => [], :failed => [] })
|
561
|
+
|
562
|
+
rename_file "#{@alternate}/FOO", "#{@alternate}/Foo"
|
563
|
+
make_file "#{@alternate}/Foo/Hello3.txt"
|
564
|
+
Dbox.sync(@alternate).should eql(:pull => { :created => [], :deleted => [], :updated => ["", "FOO"], :failed => [] },
|
565
|
+
:push => { :created => ["Foo/Hello3.txt"], :deleted => [], :updated => [], :failed => [] })
|
566
|
+
|
567
|
+
Dbox.sync(@local).should eql(:pull => { :created => ["foo/HELLO2.txt", "foo/Hello3.txt"], :deleted => [], :updated => ["", "FOO", "foo/HELLO.txt"], :failed => [] },
|
568
|
+
:push => { :created => [], :deleted => [], :updated => [], :failed => [] })
|
569
|
+
end
|
570
|
+
|
571
|
+
it "should be able to handle nested directories with case changes" do
|
572
|
+
Dbox.create(@remote, @local)
|
573
|
+
@alternate = "#{ALTERNATE_LOCAL_TEST_PATH}/#{@name}"
|
574
|
+
Dbox.clone(@remote, @alternate)
|
575
|
+
|
576
|
+
mkdir "#{@local}/foo"
|
577
|
+
make_file "#{@local}/foo/hello.txt"
|
578
|
+
Dbox.sync(@local).should eql(:pull => { :created => [], :deleted => [], :updated => [], :failed => [] },
|
579
|
+
:push => { :created => ["foo", "foo/hello.txt"], :deleted => [], :updated => [], :failed => [] })
|
580
|
+
Dbox.sync(@alternate).should eql(:pull => { :created => ["foo", "foo/hello.txt"], :deleted => [], :updated => [""], :failed => [] },
|
581
|
+
:push => { :created => [], :deleted => [], :updated => [], :failed => [] })
|
582
|
+
|
583
|
+
rename_file "#{@local}/foo", "#{@local}/FOO"
|
584
|
+
mkdir "#{@local}/FOO/BAR"
|
585
|
+
make_file "#{@local}/FOO/BAR/hello2.txt"
|
586
|
+
Dbox.sync(@local).should eql(:pull => { :created => [], :deleted => [], :updated => ["", "foo"], :failed => [] },
|
587
|
+
:push => { :created => ["FOO/BAR", "FOO/BAR/hello2.txt"], :deleted => [], :updated => [], :failed => [] })
|
588
|
+
Dbox.sync(@alternate).should eql(:pull => { :created => ["FOO/BAR/hello2.txt", "foo/BAR"], :deleted => [], :updated => ["foo"], :failed => [] },
|
589
|
+
:push => { :created => [], :deleted => [], :updated => [], :failed => [] })
|
590
|
+
end
|
463
591
|
end
|
464
592
|
|
465
593
|
describe "#move" do
|
data/spec/spec_helper.rb
CHANGED
@@ -40,6 +40,11 @@ def make_file(filepath, size_in_kb=1)
|
|
40
40
|
`dd if=/dev/urandom of="#{filepath.gsub('"','\"')}" bs=1024 count=#{size_in_kb} 1>/dev/null 2>/dev/null`
|
41
41
|
end
|
42
42
|
|
43
|
+
def rename_file(oldpath, newpath)
|
44
|
+
FileUtils.mv oldpath, "#{oldpath}-tmp"
|
45
|
+
FileUtils.mv "#{oldpath}-tmp", newpath
|
46
|
+
end
|
47
|
+
|
43
48
|
RSpec::Matchers.define :exist do
|
44
49
|
match do |actual|
|
45
50
|
File.exists?(actual) == true
|
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: 3
|
5
5
|
prerelease:
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
9
|
-
-
|
10
|
-
version: 0.
|
8
|
+
- 7
|
9
|
+
- 0
|
10
|
+
version: 0.7.0
|
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: 2012-
|
18
|
+
date: 2012-11-06 00:00:00 Z
|
19
19
|
dependencies:
|
20
20
|
- !ruby/object:Gem::Dependency
|
21
21
|
name: multipart-post
|
@@ -82,19 +82,19 @@ dependencies:
|
|
82
82
|
type: :runtime
|
83
83
|
version_requirements: *id004
|
84
84
|
- !ruby/object:Gem::Dependency
|
85
|
-
name:
|
85
|
+
name: insensitive_hash
|
86
86
|
prerelease: false
|
87
87
|
requirement: &id005 !ruby/object:Gem::Requirement
|
88
88
|
none: false
|
89
89
|
requirements:
|
90
90
|
- - ">="
|
91
91
|
- !ruby/object:Gem::Version
|
92
|
-
hash:
|
92
|
+
hash: 19
|
93
93
|
segments:
|
94
|
+
- 0
|
94
95
|
- 3
|
95
96
|
- 0
|
96
|
-
|
97
|
-
version: 3.0.1
|
97
|
+
version: 0.3.0
|
98
98
|
type: :runtime
|
99
99
|
version_requirements: *id005
|
100
100
|
description: An easy-to-use Dropbox client with fine-grained control over syncs.
|
@@ -121,7 +121,6 @@ files:
|
|
121
121
|
- lib/dbox/database.rb
|
122
122
|
- lib/dbox/db.rb
|
123
123
|
- lib/dbox/loggable.rb
|
124
|
-
- lib/dbox/parallel_tasks.rb
|
125
124
|
- lib/dbox/syncer.rb
|
126
125
|
- lib/dbox/utils.rb
|
127
126
|
- sample_polling_script.rb
|
@@ -168,7 +167,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
168
167
|
requirements: []
|
169
168
|
|
170
169
|
rubyforge_project:
|
171
|
-
rubygems_version: 1.8.
|
170
|
+
rubygems_version: 1.8.24
|
172
171
|
signing_key:
|
173
172
|
specification_version: 3
|
174
173
|
summary: Dropbox made easy.
|
data/lib/dbox/parallel_tasks.rb
DELETED
@@ -1,80 +0,0 @@
|
|
1
|
-
require "thread"
|
2
|
-
|
3
|
-
#
|
4
|
-
# Usage:
|
5
|
-
#
|
6
|
-
# puts "Creating task queue with 5 concurrent workers"
|
7
|
-
# tasks = ParallelTasks.new(5) { puts "Worker thread starting up" }
|
8
|
-
#
|
9
|
-
# puts "Starting workers"
|
10
|
-
# tasks.start
|
11
|
-
#
|
12
|
-
# puts "Making some work"
|
13
|
-
# 20.times do
|
14
|
-
# tasks.add do
|
15
|
-
# x = rand(5)
|
16
|
-
# puts "Sleeping for #{x}s"
|
17
|
-
# sleep x
|
18
|
-
# end
|
19
|
-
# end
|
20
|
-
#
|
21
|
-
# puts "Waiting for workers to finish"
|
22
|
-
# tasks.finish
|
23
|
-
#
|
24
|
-
# puts "Done"
|
25
|
-
#
|
26
|
-
class ParallelTasks
|
27
|
-
def initialize(num_workers, &initialization_proc)
|
28
|
-
@num_workers = num_workers
|
29
|
-
@initialization_proc = initialization_proc
|
30
|
-
@workers = []
|
31
|
-
@work_queue = Queue.new
|
32
|
-
@semaphore = Mutex.new
|
33
|
-
@done_making_tasks = false
|
34
|
-
end
|
35
|
-
|
36
|
-
def start
|
37
|
-
@num_workers.times do
|
38
|
-
@workers << Thread.new do
|
39
|
-
@initialization_proc.call if @initialization_proc
|
40
|
-
done = false
|
41
|
-
while !done
|
42
|
-
task = nil
|
43
|
-
@semaphore.synchronize do
|
44
|
-
unless @work_queue.empty?
|
45
|
-
task = @work_queue.pop()
|
46
|
-
else
|
47
|
-
if @done_making_tasks
|
48
|
-
done = true
|
49
|
-
else
|
50
|
-
sleep 0.1
|
51
|
-
end
|
52
|
-
end
|
53
|
-
end
|
54
|
-
if task
|
55
|
-
begin
|
56
|
-
task.call
|
57
|
-
rescue Exception => e
|
58
|
-
log.error e.inspect
|
59
|
-
end
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|
65
|
-
|
66
|
-
def add(&proc)
|
67
|
-
@work_queue << proc
|
68
|
-
end
|
69
|
-
|
70
|
-
def finish
|
71
|
-
@done_making_tasks = true
|
72
|
-
begin
|
73
|
-
@workers.each {|t| t.join }
|
74
|
-
rescue Exception => e
|
75
|
-
log.error "Error waiting for workers to complete tasks: #{e.inspect}"
|
76
|
-
@workers.each {|t| t.kill }
|
77
|
-
raise e
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|