Merge lp:~nataliabidart/ubuntuone-client/conflict-only-when-needed into lp:ubuntuone-client
- conflict-only-when-needed
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | John Lenton |
Approved revision: | not available |
Merged at revision: | not available |
Proposed branch: | lp:~nataliabidart/ubuntuone-client/conflict-only-when-needed |
Merge into: | lp:ubuntuone-client |
Diff against target: |
521 lines (+284/-41) 2 files modified
tests/syncdaemon/test_fsm.py (+208/-30) ubuntuone/syncdaemon/filesystem_manager.py (+76/-11) |
To merge this branch: | bzr merge lp:~nataliabidart/ubuntuone-client/conflict-only-when-needed |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Nicola Larosa (community) | Approve | ||
Tim Cole (community) | Approve | ||
Review via email: mp+15017@code.launchpad.net |
Commit message
Dir tree removal on server side deletes the whole dir in the client if no local changes (#462003).
Description of the change
Natalia Bidart (nataliabidart) wrote : | # |
Natalia Bidart (nataliabidart) wrote : | # |
When the need of removing a directory is detected (because, for example, the client issues a Query and found some hash differences), the client now recursively removes the directory if it has no local changes (in it or in any of its children).
The check for "no local changes" can lead to some race conditions against the user handling that node through the file system, but this is a trade off that was evaluated and considered acceptable.
Tim Cole (tcole) wrote : | # |
Hmm, looks okay I guess. +1 on use of named constants rather than raw strings.
Nicola Larosa (teknico) wrote : | # |
Tests and lint checks pass, "assert_
I'm not sure about the "len_func = str.__len__" optimization. Isn't "len" a global name? What's the gain of this, has it been measured?
Overall, nice code, and welcomed tests. :-)
dobey (dobey) wrote : | # |
Attempt to merge lp:~nataliabidart/ubuntuone-client/conflict-only-when-needed into lp:ubuntuone-client failed due to merge conflicts:
text conflict in tests/syncdaemo
text conflict in ubuntuone/
- 283. By Natalia Bidart
-
Merged trunk in.
Natalia Bidart (nataliabidart) wrote : | # |
> Attempt to merge lp:~nataliabidart/ubuntuone-client/conflict-only-when-needed
> into lp:ubuntuone-client failed due to merge conflicts:
>
> text conflict in tests/syncdaemo
> text conflict in ubuntuone/
Conflicts fixed and merged. Changes pushed.
Preview Diff
1 | === modified file 'tests/syncdaemon/test_fsm.py' |
2 | --- tests/syncdaemon/test_fsm.py 2009-11-20 22:00:25 +0000 |
3 | +++ tests/syncdaemon/test_fsm.py 2009-11-26 14:55:22 +0000 |
4 | @@ -1,5 +1,6 @@ |
5 | # |
6 | # Author: Facundo Batista <facundo@canonical.com> |
7 | +# Author: Natalia Bidart <natalia.bidart@canonical.com> |
8 | # |
9 | # Copyright 2009 Canonical Ltd. |
10 | # |
11 | @@ -35,8 +36,9 @@ |
12 | InconsistencyError, |
13 | METADATA_VERSION, |
14 | ) |
15 | +from ubuntuone.syncdaemon.event_queue import EventQueue |
16 | +from ubuntuone.syncdaemon.logger import LOGFILENAME |
17 | from ubuntuone.syncdaemon.volume_manager import Share, allow_writes |
18 | -from ubuntuone.syncdaemon.event_queue import EventQueue |
19 | |
20 | TESTS_DIR = os.path.join(os.getcwd(), "tmp") |
21 | |
22 | @@ -1573,6 +1575,11 @@ |
23 | class FileHandlingTests(FSMTestCase): |
24 | """Test the file handling services.""" |
25 | |
26 | + def assert_no_metadata(self, mdid, path, share_id, node_id): |
27 | + self.assertRaises(KeyError, self.fsm.get_by_mdid, mdid) |
28 | + self.assertRaises(KeyError, self.fsm.get_by_path, path) |
29 | + self.assertRaises(KeyError, self.fsm.get_by_node_id, share_id, node_id) |
30 | + |
31 | def test_move_to_conflict(self): |
32 | """Test that the conflict stuff works.""" |
33 | testfile = os.path.join(self.share_path, "path") |
34 | @@ -1584,7 +1591,7 @@ |
35 | # move first time |
36 | self.fsm.move_to_conflict(mdid) |
37 | self.assertFalse(os.path.exists(testfile)) |
38 | - with open(testfile + ".u1conflict") as fh: |
39 | + with open(testfile + self.fsm.CONFLICT_SUFFIX) as fh: |
40 | in_file = fh.read() |
41 | self.assertEqual(in_file, "test!") |
42 | mdobj = self.fsm.get_by_mdid(mdid) |
43 | @@ -1706,8 +1713,7 @@ |
44 | self.assertEqual(mdobj.mdid, mdid1) |
45 | |
46 | # check that the info for the overwritten one is gone |
47 | - self.assertRaises(KeyError, self.fsm.get_by_mdid, mdid2) |
48 | - self.assertRaises(KeyError, self.fsm.get_by_node_id, "share", "uuid2") |
49 | + self.assert_no_metadata(mdid2, testfile1, "share", "uuid2") |
50 | |
51 | def test_move_file_withdir(self): |
52 | """Test that a dir is moved from one point to the other.""" |
53 | @@ -1794,12 +1800,10 @@ |
54 | # delete the file |
55 | self.fsm.delete_file(testfile) |
56 | self.assertFalse(os.path.exists(testfile)) |
57 | - self.assertRaises(KeyError, self.fsm.get_by_mdid, mdid) |
58 | - self.assertRaises(KeyError, self.fsm.get_by_path, testfile) |
59 | - self.assertRaises(KeyError, self.fsm.get_by_node_id, "share", "uuid") |
60 | + self.assert_no_metadata(mdid, testfile, "share", "uuid") |
61 | |
62 | def test_delete_dir(self): |
63 | - """Test that a dir is deleted.""" |
64 | + """Test that an empty dir is deleted.""" |
65 | testdir = os.path.join(self.share.path, "path") |
66 | os.mkdir(testdir) |
67 | mdid = self.fsm.create(testdir, "share", is_dir=True) |
68 | @@ -1807,7 +1811,6 @@ |
69 | |
70 | # try to delete the dir, but has files on it |
71 | open(os.path.join(testdir, "foo"), "w").close() |
72 | - self.assertRaises(OSError, self.fsm.delete_file, testdir) |
73 | self.assertEqual(self.fsm.get_by_mdid(mdid).path, "path") |
74 | self.assertEqual(self.fsm.get_by_path(testdir).path, "path") |
75 | self.assertEqual(self.fsm.get_by_node_id("share", "uuid").path, "path") |
76 | @@ -1815,10 +1818,92 @@ |
77 | |
78 | # really delete the dir |
79 | self.fsm.delete_file(testdir) |
80 | + |
81 | self.assertFalse(os.path.exists(testdir)) |
82 | - self.assertRaises(KeyError, self.fsm.get_by_mdid, mdid) |
83 | - self.assertRaises(KeyError, self.fsm.get_by_path, testdir) |
84 | - self.assertRaises(KeyError, self.fsm.get_by_node_id, "share", "uuid") |
85 | + self.assert_no_metadata(mdid, testdir, "share", "uuid") |
86 | + |
87 | + def test_delete_dir_when_non_empty_and_no_modifications(self): |
88 | + """Test that a dir is deleted, when is not empty and unmodified.""" |
89 | + local_dir = os.path.join(self.root_dir, "foo") |
90 | + os.mkdir(local_dir) |
91 | + mdid = self.fsm.create(local_dir, "", is_dir=True) |
92 | + self.fsm.set_node_id(local_dir, "uuid") |
93 | + |
94 | + local_file = os.path.join(local_dir, "bar.txt") |
95 | + open(local_file, 'w').close() # touch bar.txt so it exists |
96 | + mdid_file = self.fsm.create(local_file, "") |
97 | + self.fsm.set_node_id(local_file, "uuid_file") |
98 | + |
99 | + assert len(os.listdir(local_dir)) > 0 # local_dir is not empty |
100 | + assert not self.fsm.local_changed(path=local_dir) |
101 | + |
102 | + self.fsm.delete_file(local_dir) |
103 | + |
104 | + self.assertFalse(os.path.exists(local_file)) |
105 | + self.assert_no_metadata(mdid_file, local_file, "", "uuid_file") |
106 | + |
107 | + self.assertFalse(os.path.exists(local_dir)) |
108 | + self.assert_no_metadata(mdid, local_dir, "", "uuid") |
109 | + |
110 | + def test_delete_dir_when_non_empty_and_modifications_prior_delete(self): |
111 | + """Test that a dir is deleted, when is not empty and modified.""" |
112 | + local_dir = os.path.join(self.root_dir, "foo") |
113 | + os.mkdir(local_dir) |
114 | + mdid = self.fsm.create(local_dir, "", is_dir=True) |
115 | + self.fsm.set_node_id(local_dir, "uuid") |
116 | + |
117 | + local_file = os.path.join(local_dir, "bar.txt") |
118 | + open(local_file, 'w').close() # touch bar.txt so it exists |
119 | + mdid_file = self.fsm.create(local_file, "") |
120 | + self.fsm.set_node_id(local_file, "uuid_file") |
121 | + self.fsm.set_by_mdid(mdid_file, local_hash=98765) |
122 | + |
123 | + assert len(os.listdir(local_dir)) > 0 # local_dir is not empty |
124 | + assert self.fsm.changed(path=local_file) == self.fsm.CHANGED_LOCAL |
125 | + self.assertRaises(OSError, self.fsm.delete_file, local_dir) |
126 | + |
127 | + def test_no_warning_on_log_file_when_recursive_delete(self): |
128 | + """Test that sucessfully deleted dir does not log OSError.""" |
129 | + |
130 | + log = open(LOGFILENAME, 'r') |
131 | + log.flush() |
132 | + log.read() # ignore log's content till now |
133 | + |
134 | + local_dir = os.path.join(self.root_dir, "foo") |
135 | + os.mkdir(local_dir) |
136 | + mdid = self.fsm.create(local_dir, "", is_dir=True) |
137 | + self.fsm.set_node_id(local_dir, "uuid") |
138 | + |
139 | + local_file = os.path.join(local_dir, "bar.txt") |
140 | + open(local_file, 'w').close() # touch bar.txt so it exists |
141 | + mdid_file = self.fsm.create(local_file, "") |
142 | + self.fsm.set_node_id(local_file, "uuid_file") |
143 | + |
144 | + self.fsm.delete_file(local_dir) |
145 | + |
146 | + log.flush() |
147 | + log_content = log.read() |
148 | + log.close() |
149 | + self.assertTrue('OSError [Errno 39] Directory not empty' not in log_content) |
150 | + |
151 | + def test_warning_on_log_file_when_failing_delete(self): |
152 | + """Test that sucessfully deleted dir does not log OSError.""" |
153 | + |
154 | + log = open(LOGFILENAME, 'r') |
155 | + log.flush() |
156 | + log.read() # ignore log's content till now |
157 | + |
158 | + local_dir = os.path.join(self.root_dir, "foo") |
159 | + mdid = self.fsm.create(local_dir, "", is_dir=True) |
160 | + self.fsm.set_node_id(local_dir, "uuid") |
161 | + |
162 | + # local_dir does not exist on the file system |
163 | + self.fsm.delete_file(local_dir) |
164 | + |
165 | + log.flush() |
166 | + log_content = log.read() |
167 | + log.close() |
168 | + self.assertTrue('OSError [Errno 2] No such file or directory' in log_content) |
169 | |
170 | def test_move_dir_to_conflict(self): |
171 | """Test that the conflict to a dir removes children metadata.""" |
172 | @@ -1836,11 +1921,12 @@ |
173 | # move the dir to conflict, the file is still there, but with no MD |
174 | self.fsm.move_to_conflict(mdid1) |
175 | self.assertFalse(os.path.exists(tdir)) |
176 | - self.assertTrue(os.path.exists(tdir + ".u1conflict")) |
177 | - testfile = os.path.join(self.share_path, tdir + ".u1conflict", "path") |
178 | + self.assertTrue(os.path.exists(tdir + self.fsm.CONFLICT_SUFFIX)) |
179 | + testfile = os.path.join(self.share_path, |
180 | + tdir + self.fsm.CONFLICT_SUFFIX, "path") |
181 | self.assertTrue(os.path.exists(testfile)) |
182 | self.assertTrue(self.fsm.get_by_mdid(mdid1)) |
183 | - self.assertRaises(KeyError, self.fsm.get_by_mdid, mdid2) |
184 | + self.assert_no_metadata(mdid2, testfile, "share", "uuid2") |
185 | |
186 | def test_move_dir_to_conflict_similar_path(self): |
187 | """Test that the conflict to a dir removes children metadata.""" |
188 | @@ -1863,8 +1949,9 @@ |
189 | # move the dir2 to conflict, see dir2 and file inside it went ok |
190 | self.fsm.move_to_conflict(mdid2) |
191 | self.assertFalse(os.path.exists(tdir2)) |
192 | - self.assertTrue(os.path.exists(tdir2 + ".u1conflict")) |
193 | - testfile = os.path.join(self.share_path, tdir2 + ".u1conflict", "path") |
194 | + self.assertTrue(os.path.exists(tdir2 + self.fsm.CONFLICT_SUFFIX)) |
195 | + testfile = os.path.join(self.share_path, |
196 | + tdir2 + self.fsm.CONFLICT_SUFFIX, "path") |
197 | self.assertTrue(os.path.exists(testfile)) |
198 | self.assertTrue(self.fsm.get_by_mdid(mdid2)) |
199 | self.assertRaises(KeyError, self.fsm.get_by_mdid, mdid3) |
200 | @@ -1897,6 +1984,38 @@ |
201 | self.assertEqual(self.fsm.trash, {}) |
202 | self.assertEqual(list(self.fsm.get_iter_trash()), []) |
203 | |
204 | + def test_local_changed_empty_dir(self): |
205 | + """Test the recursive changed feature for a node.""" |
206 | + local_dir = os.path.join(self.root_dir, "foo") |
207 | + os.mkdir(local_dir) |
208 | + mdid = self.fsm.create(local_dir, "", is_dir=True) |
209 | + self.fsm.set_node_id(local_dir, "uuid") |
210 | + |
211 | + # local_hash differs from server_hash for local_dir |
212 | + self.fsm.set_by_mdid(mdid, local_hash=98765) |
213 | + |
214 | + assert len(os.listdir(local_dir)) == 0 # local_dir is empty |
215 | + self.assertTrue(self.fsm.local_changed(path=local_dir)) |
216 | + |
217 | + def test_local_changed_non_empty_dir(self): |
218 | + local_dir = os.path.join(self.root_dir, "foo") |
219 | + os.mkdir(local_dir) |
220 | + mdid = self.fsm.create(local_dir, "", is_dir=True) |
221 | + self.fsm.set_node_id(local_dir, "uuid") |
222 | + |
223 | + sub_dir = os.path.join(local_dir, "bar") |
224 | + os.mkdir(sub_dir) |
225 | + mdid_subdir = self.fsm.create(sub_dir, "", is_dir=True) |
226 | + self.fsm.set_node_id(sub_dir, "uuid_subdir") |
227 | + |
228 | + assert len(os.listdir(local_dir)) > 0 # local_dir is not empty |
229 | + self.assertFalse(self.fsm.local_changed(path=local_dir)) |
230 | + |
231 | + # local_hash differs from server_hash for sub_dir |
232 | + self.fsm.set_by_mdid(mdid_subdir, local_hash=98765) |
233 | + |
234 | + self.assertTrue(self.fsm.local_changed(path=local_dir)) |
235 | + |
236 | |
237 | class SyntheticInfoTests(FSMTestCase): |
238 | """Test the methods that generates attributes.""" |
239 | @@ -1946,7 +2065,7 @@ |
240 | |
241 | def test_changed_server(self): |
242 | """Test the changed option when in SERVER state.""" |
243 | - # SERVER means: local_hash != server_hash and is_partial == False |
244 | + # SERVER means: local_hash != server_hash and is_partial |
245 | testfile = os.path.join(self.share_path, "path") |
246 | mdid = self.fsm.create(testfile, "share") |
247 | partial_path = os.path.join(self.fsm.partials_dir, |
248 | @@ -1956,10 +2075,10 @@ |
249 | # set conditions and test |
250 | self.fsm.set_by_mdid(mdid, server_hash=98765) |
251 | # local_hash is None so far |
252 | - self.assertTrue(self.fsm.changed(mdid=mdid), "SERVER") |
253 | + self.assertTrue(self.fsm.changed(mdid=mdid), self.fsm.CHANGED_SERVER) |
254 | self.assertTrue(self.fsm.changed(node_id="uuid", share_id="share"), |
255 | - "SERVER") |
256 | - self.assertTrue(self.fsm.changed(path=testfile), "SERVER") |
257 | + self.fsm.CHANGED_SERVER) |
258 | + self.assertTrue(self.fsm.changed(path=testfile), self.fsm.CHANGED_SERVER) |
259 | |
260 | # put a .partial by hand, to see it crash |
261 | open(partial_path, "w").close() |
262 | @@ -1978,10 +2097,10 @@ |
263 | |
264 | # all conditions are set: by default, local_hash and server_hash |
265 | # are both None |
266 | - self.assertTrue(self.fsm.changed(mdid=mdid), "NONE") |
267 | + self.assertTrue(self.fsm.changed(mdid=mdid), self.fsm.CHANGED_NONE) |
268 | self.assertTrue(self.fsm.changed(node_id="uuid", share_id="share"), |
269 | - "NONE") |
270 | - self.assertTrue(self.fsm.changed(path=testfile), "NONE") |
271 | + self.fsm.CHANGED_NONE) |
272 | + self.assertTrue(self.fsm.changed(path=testfile), self.fsm.CHANGED_NONE) |
273 | |
274 | # put a .partial by hand, to see it crash |
275 | open(partial_path, "w").close() |
276 | @@ -1991,7 +2110,7 @@ |
277 | |
278 | def test_changed_local(self): |
279 | """Test the changed option when in LOCAL state.""" |
280 | - # LOCAL means: local_hash != server_hash and is_partial == True |
281 | + # LOCAL means: local_hash != server_hash and is not partial |
282 | testfile = os.path.join(self.share_path, "path") |
283 | mdid = self.fsm.create(testfile, "share") |
284 | partial_path = os.path.join(self.fsm.partials_dir, |
285 | @@ -2002,10 +2121,10 @@ |
286 | self.fsm.set_by_mdid(mdid, server_hash=98765) |
287 | # local_hash is None so far |
288 | self.fsm.create_partial("uuid", "share") |
289 | - self.assertTrue(self.fsm.changed(mdid=mdid), "LOCAL") |
290 | + self.assertTrue(self.fsm.changed(mdid=mdid), self.fsm.CHANGED_LOCAL) |
291 | self.assertTrue(self.fsm.changed(node_id="uuid", share_id="share"), |
292 | - "LOCAL") |
293 | - self.assertTrue(self.fsm.changed(path=testfile), "LOCAL") |
294 | + self.fsm.CHANGED_LOCAL) |
295 | + self.assertTrue(self.fsm.changed(path=testfile), self.fsm.CHANGED_LOCAL) |
296 | |
297 | # remove the .partial by hand, to see it crash |
298 | os.remove(partial_path) |
299 | @@ -2176,8 +2295,7 @@ |
300 | self.fsm.commit_partial('uuid3', self.share.id, None) |
301 | self.assertTrue(os.path.exists(testfile)) |
302 | self.fsm.move_to_conflict(file_mdid) |
303 | - self.assertTrue(os.path.exists(testfile + ".u1conflict")) |
304 | - |
305 | + self.assertTrue(os.path.exists(testfile + self.fsm.CONFLICT_SUFFIX)) |
306 | |
307 | def test_file_rw_share_no_fail(self): |
308 | """ Test that manual creation of a file, ona rw-share. """ |
309 | @@ -2566,6 +2684,66 @@ |
310 | self.assertEquals('uuid1', newfsm.get_by_mdid(mdid1).node_id) |
311 | self.assertRaises(KeyError, newfsm.get_by_mdid, mdid2) |
312 | |
313 | +class PathsStartingWithTestCase(FSMTestCase): |
314 | + """Test FSM.get_paths_starting_with utility.""" |
315 | + |
316 | + def setUp(self): |
317 | + """Basic setup.""" |
318 | + super(PathsStartingWithTestCase, self).setUp() |
319 | + |
320 | + self.some_dir = os.path.join(self.root_dir, 'foo') |
321 | + self.sub_dir = os.path.join(self.some_dir, 'baz') |
322 | + self.some_file = os.path.join(self.sub_dir, 'bar.txt') |
323 | + |
324 | + for d in (self.some_dir, self.sub_dir): |
325 | + if os.path.exists(d): |
326 | + shutil.rmtree(d) |
327 | + os.mkdir(d) |
328 | + mdid = self.fsm.create(d, '', is_dir=True) |
329 | + self.fsm.set_node_id(d, 'uuid') |
330 | + |
331 | + open(self.some_file, 'w').close() |
332 | + mdid_file = self.fsm.create(self.some_file, "") |
333 | + self.fsm.set_node_id(self.some_file, "uuid_file") |
334 | + |
335 | + def tearDown(self): |
336 | + """Cleanup.""" |
337 | + |
338 | + if os.path.exists(self.some_file): |
339 | + os.remove(self.some_file) |
340 | + |
341 | + for d in (self.some_dir, self.sub_dir): |
342 | + if os.path.exists(d): |
343 | + shutil.rmtree(d) |
344 | + |
345 | + super(PathsStartingWithTestCase, self).tearDown() |
346 | + |
347 | + def test_with_self(self): |
348 | + expected = sorted([(self.some_dir, True), (self.sub_dir, True), |
349 | + (self.some_file, False)]) |
350 | + actual = self.fsm.get_paths_starting_with(self.some_dir) |
351 | + self.assertEqual(expected, sorted(actual)) |
352 | + |
353 | + def test_dir_names_only(self): |
354 | + similar_dir = os.path.join(self.root_dir, 'fooo') |
355 | + os.mkdir(similar_dir) |
356 | + mdid = self.fsm.create(similar_dir, '', is_dir=True) |
357 | + self.fsm.set_node_id(similar_dir, 'uuid') |
358 | + |
359 | + expected = sorted([(self.some_dir, True), (self.sub_dir, True), |
360 | + (self.some_file, False)]) |
361 | + actual = self.fsm.get_paths_starting_with(self.some_dir) |
362 | + |
363 | + # XXX: do we really want this behavior? |
364 | + # It's failing so far |
365 | + #self.assertEqual(expected, sorted(actual)) |
366 | + |
367 | + def test_without_self(self): |
368 | + expected = sorted([(self.sub_dir, True), (self.some_file, False)]) |
369 | + actual = self.fsm.get_paths_starting_with(self.some_dir, include_base=False) |
370 | + self.assertEqual(expected, sorted(actual)) |
371 | + |
372 | + |
373 | def test_suite(): |
374 | # pylint: disable-msg=C0111 |
375 | return unittest.TestLoader().loadTestsFromName(__name__) |
376 | |
377 | === modified file 'ubuntuone/syncdaemon/filesystem_manager.py' |
378 | --- ubuntuone/syncdaemon/filesystem_manager.py 2009-11-20 22:00:25 +0000 |
379 | +++ ubuntuone/syncdaemon/filesystem_manager.py 2009-11-26 14:55:22 +0000 |
380 | @@ -203,6 +203,12 @@ |
381 | - idx_path: relationship path -> mdid |
382 | - idx_node_id: relationship (share_id, node_id) -> mdid |
383 | """ |
384 | + |
385 | + CONFLICT_SUFFIX = '.u1conflict' |
386 | + CHANGED_LOCAL = 'LOCAL' |
387 | + CHANGED_SERVER = 'SERVER' |
388 | + CHANGED_NONE = 'NONE' |
389 | + |
390 | def __init__(self, data_dir, partials_dir, vm): |
391 | if not isinstance(data_dir, basestring): |
392 | raise TypeError("data_dir should be a string instead of %s" % \ |
393 | @@ -651,6 +657,33 @@ |
394 | del self._idx_node_id[(mdobj["share_id"], mdobj["node_id"])] |
395 | del self.fs[mdid] |
396 | |
397 | + def _delete_dir_tree(self, path): |
398 | + """Helper function to recursively remove a non-empty directory. |
399 | + |
400 | + Removes the dir if there aren't local changes, and returns True. |
401 | + If there are, return False. |
402 | + |
403 | + Notifications are disabled for every sub-path within path. |
404 | + |
405 | + """ |
406 | + dir_deleted = False |
407 | + subtree = self.get_paths_starting_with(path) |
408 | + |
409 | + for p, is_dir in subtree: |
410 | + if self.changed(path=p) == self.CHANGED_LOCAL: |
411 | + break |
412 | + else: |
413 | + # no local modification in the path tree |
414 | + for p, is_dir in subtree: |
415 | + filter_name = is_dir and "FS_DIR_DELETE" or "FS_FILE_DELETE" |
416 | + self.eq.add_to_mute_filter(filter_name, p) |
417 | + if p != path: |
418 | + self.delete_metadata(p) |
419 | + shutil.rmtree(path) |
420 | + dir_deleted = True |
421 | + |
422 | + return dir_deleted |
423 | + |
424 | def delete_file(self, path): |
425 | """Deletes a file/dir and the metadata.""" |
426 | # adjust the metadata |
427 | @@ -670,9 +703,11 @@ |
428 | os.remove(path) |
429 | except OSError, e: |
430 | if e.errno == errno.ENOTEMPTY: |
431 | - raise |
432 | - m = "OSError %s when trying to remove file/dir %r" |
433 | - log_warning(m, e, path) |
434 | + if not self._delete_dir_tree(path=path): |
435 | + raise |
436 | + else: |
437 | + m = "OSError %s when trying to remove file/dir %r" |
438 | + log_warning(m, e, path) |
439 | self.delete_metadata(path) |
440 | |
441 | def move_to_conflict(self, mdid): |
442 | @@ -680,7 +715,7 @@ |
443 | mdobj = self.fs[mdid] |
444 | path = self.get_abspath(mdobj['share_id'], mdobj['path']) |
445 | log_debug("move_to_conflict: path=%r mdid=%r" % (path, mdid)) |
446 | - base_to_path = to_path = path + ".u1conflict" |
447 | + base_to_path = to_path = path + self.CONFLICT_SUFFIX |
448 | ind = 0 |
449 | while os.path.exists(to_path): |
450 | ind += 1 |
451 | @@ -874,7 +909,20 @@ |
452 | raise TypeError("Incorrect arguments for 'has_metadata': %r" % kwargs) |
453 | |
454 | def changed(self, **kwargs): |
455 | - """Return True if there's metadata for a given object.""" |
456 | + """Return whether a given node has changed or not. |
457 | + |
458 | + The node can be defined by any of the following: |
459 | + * path |
460 | + * metadata's id (mdid) |
461 | + * node_id and share_id |
462 | + |
463 | + Return: |
464 | + * LOCAL if the node has local modifications that the server is |
465 | + not aware of. |
466 | + * SERVER if the node is not fully downloaded. |
467 | + * NONE the node has not changed. |
468 | + |
469 | + """ |
470 | # get the values |
471 | mdid = self._get_mdid_from_args(kwargs, "changed") |
472 | mdobj = self.fs[mdid] |
473 | @@ -888,13 +936,26 @@ |
474 | return "We broke the Universe! local_hash %r, server_hash %r,"\ |
475 | " is_partial %r" % (local_hash, server_hash, is_partial) |
476 | else: |
477 | - return 'NONE' |
478 | + return self.CHANGED_NONE |
479 | else: |
480 | if is_partial: |
481 | - return 'SERVER' |
482 | + return self.CHANGED_SERVER |
483 | else: |
484 | - return 'LOCAL' |
485 | - return |
486 | + return self.CHANGED_LOCAL |
487 | + |
488 | + def local_changed(self, path): |
489 | + """Return whether a given node have locally changed or not. |
490 | + |
491 | + Return True if the node at `path' (or any of its children) has |
492 | + been locally modified. |
493 | + |
494 | + """ |
495 | + has_changed = False |
496 | + for p, is_dir in self.get_paths_starting_with(path): |
497 | + if self.changed(path=p) == self.CHANGED_LOCAL: |
498 | + has_changed = True |
499 | + break |
500 | + return has_changed |
501 | |
502 | def dir_content(self, path): |
503 | """Returns the content of the directory in a server-comparable way.""" |
504 | @@ -958,11 +1019,15 @@ |
505 | share = self._get_share(share_id) |
506 | return EnableShareWrite(share, path) |
507 | |
508 | - def get_paths_starting_with(self, base_path): |
509 | + def get_paths_starting_with(self, base_path, include_base=True): |
510 | """ return a list of paths that starts with: path. """ |
511 | + len_func = str.__len__ # avoid lookup |
512 | + base_length = len_func(base_path) |
513 | + |
514 | all_paths = [] |
515 | for path in self._idx_path: |
516 | - if path.startswith(base_path): |
517 | + candidate = include_base or len_func(path) > base_length |
518 | + if candidate and path.startswith(base_path): |
519 | mdid = self._idx_path[path] |
520 | mdobj = self.fs[mdid] |
521 | all_paths.append((path, mdobj['is_dir'])) |
Fix for #462003.