Merge lp:~facundo/ubuntuone-client/ignore-nonutf8-filenames into lp:ubuntuone-client

Proposed by Facundo Batista
Status: Merged
Approved by: Facundo Batista
Approved revision: 450
Merged at revision: not available
Proposed branch: lp:~facundo/ubuntuone-client/ignore-nonutf8-filenames
Merge into: lp:ubuntuone-client
Diff against target: 511 lines (+225/-35)
6 files modified
data/source_ubuntuone-client.py (+2/-0)
tests/syncdaemon/test_dbus.py (+17/-0)
tests/syncdaemon/test_eq_inotify.py (+158/-35)
ubuntuone/syncdaemon/dbus_interface.py (+14/-0)
ubuntuone/syncdaemon/event_queue.py (+25/-0)
ubuntuone/syncdaemon/logger.py (+9/-0)
To merge this branch: bzr merge lp:~facundo/ubuntuone-client/ignore-nonutf8-filenames
Reviewer Review Type Date Requested Status
dobey (community) Approve
Natalia Bidart (community) Approve
Review via email: mp+22238@code.launchpad.net

Commit message

Ignore all the events from disk when file/dir name is not valid.

Description of the change

Ignore all the events from disk when file/dir name is not valid.

This introduces a new event, FS_INVALID_NAME, and a new specific log to include only alerts when this happens (which are also logged in main log file).

Also, when this event happens, the DBus interface generates a signal to make the system aware of this.

Tests included for everything.

To post a comment you must log in.
450. By Facundo Batista

Docstring

Revision history for this message
Natalia Bidart (nataliabidart) wrote :

Nice!

review: Approve
Revision history for this message
dobey (dobey) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'data/source_ubuntuone-client.py'
2--- data/source_ubuntuone-client.py 2009-10-06 13:50:37 +0000
3+++ data/source_ubuntuone-client.py 2010-03-26 18:31:32 +0000
4@@ -27,6 +27,7 @@
5 # things we may want to collect for the report
6 u1_client_log = os.path.join(u1_log_path, "syncdaemon.log")
7 u1_except_log = os.path.join(u1_log_path, "syncdaemon-exceptions.log")
8+u1_invalidnames_log = os.path.join(u1_log_path, "syncdaemon-invalid-names.log")
9 u1_oauth_log = os.path.join(u1_log_path, "oauth-login.log")
10 u1_u1sync_log = os.path.join(u1_log_path, "u1sync.log")
11 u1_sd_conf = os.path.join("etc", "xdg", "ubuntuone", "syncdaemon.conf")
12@@ -37,6 +38,7 @@
13 def add_info(report):
14 """add report info"""
15 attach_file_if_exists(report, u1_except_log)
16+ attach_file_if_exists(report, u1_invalidnames_log)
17 attach_file_if_exists(report, u1_oauth_log)
18 attach_file_if_exists(report, u1_usersd_conf)
19 attach_file_if_exists(report, u1_sd_conf)
20
21=== modified file 'tests/syncdaemon/test_dbus.py'
22--- tests/syncdaemon/test_dbus.py 2010-03-10 22:12:06 +0000
23+++ tests/syncdaemon/test_dbus.py 2010-03-26 18:31:32 +0000
24@@ -1133,6 +1133,23 @@
25 self.event_q.push(event_name, *args)
26 return d
27
28+ def test_invalid_filename(self):
29+ """Test the FS_INVALID_NAME signal."""
30+ d = defer.Deferred()
31+
32+ def handler(dirname, filename):
33+ """Handler for InvalidName signal."""
34+ self.assertTrue(isinstance(dirname, unicode))
35+ self.assertTrue(isinstance(filename, str))
36+ d.callback(True)
37+
38+ match = self.bus.add_signal_receiver(handler,
39+ signal_name='InvalidName',
40+ byte_arrays=True)
41+ self.signal_receivers.add(match)
42+ self.main.event_q.push('FS_INVALID_NAME', u'test/dir', 'testpath')
43+ return d
44+
45 def test_share_changed(self):
46 """ Test the ShareChanged signal. """
47 share_path = os.path.join(self.main.shares_dir, 'share')
48
49=== modified file 'tests/syncdaemon/test_eq_inotify.py'
50--- tests/syncdaemon/test_eq_inotify.py 2010-03-15 17:05:49 +0000
51+++ tests/syncdaemon/test_eq_inotify.py 2010-03-26 18:31:32 +0000
52@@ -31,6 +31,18 @@
53 from tests.syncdaemon.test_eventqueue import BaseEQTestCase
54
55
56+class DontHitMe(object):
57+ """We shouldn't be called."""
58+
59+ def __init__(self, test_instance):
60+ self.test_instance = test_instance
61+
62+ def handle_default(self, *a, **k):
63+ """Something here? Error!"""
64+ self.test_inst.finished_error("Don't hit me! Received %s %s" % (a, k))
65+
66+
67+
68 class WatchTests(BaseEQTestCase):
69 """Test the EQ API to add and remove watchs."""
70
71@@ -393,20 +405,13 @@
72 open(fromfile, "w").close()
73 symlpath = os.path.join(testdir, "syml")
74
75- class DontHitMe(object):
76- """we shouldn't be called"""
77- # class-closure, cannot use self, pylint: disable-msg=E0213
78- def handle_default(innerself, *a):
79- """Something here? Error!"""
80- self.finished_error("don't hit me! received %s" % (a,))
81-
82 def confirm():
83 """check result."""
84 self.finished_ok()
85
86 # set up everything and freeze
87 self.eq.inotify_add_watch(testdir)
88- self.eq.subscribe(DontHitMe())
89+ self.eq.subscribe(DontHitMe(self))
90
91 os.symlink(fromfile, symlpath)
92 reactor.callLater(.1, confirm)
93@@ -1026,13 +1031,6 @@
94 testfile = os.path.join(testdir, "bar")
95 os.mkdir(testdir)
96
97- class DontHitMe(object):
98- """we shouldn't be called"""
99- # class-closure, cannot use self, pylint: disable-msg=E0213
100- def handle_default(innerself, *a):
101- """Something here? Error!"""
102- self.finished_error("don't hit me! received %s" % (a,))
103-
104 def freeze_commit():
105 """release and check result."""
106 d = self.eq.freeze_commit([("FS_DIR_DELETE", "foobar")])
107@@ -1046,7 +1044,7 @@
108
109 # set up everything and freeze
110 self.eq.inotify_add_watch(testdir)
111- self.eq.subscribe(DontHitMe())
112+ self.eq.subscribe(DontHitMe(self))
113 self.eq.freeze_begin(testdir)
114
115 open(testfile, "w").close()
116@@ -1132,15 +1130,6 @@
117 class MutedSignalsTests(BaseTwisted):
118 """Test that EQ filter some signals on demand."""
119
120- class DontHitMe(object):
121- """we shouldn't be called"""
122- # class-closure, cannot use self, pylint: disable-msg=E0213
123- def __init__(innerself, obj):
124- innerself.obj = obj
125- def handle_default(innerself, *a):
126- """Something here? Error!"""
127- innerself.obj.finished_error("don't hit me! received %s" % (a,))
128-
129 def check_filter(self, _=None):
130 self.assertFalse(self.eq._processor._to_mute._cnt)
131 self.finished_ok()
132@@ -1179,7 +1168,7 @@
133 self.eq.add_to_mute_filter("FS_FILE_CLOSE_NOWRITE", testfile)
134
135 self.eq.inotify_add_watch(self.root_dir)
136- self.eq.subscribe(self.DontHitMe(self))
137+ self.eq.subscribe(DontHitMe(self))
138
139 # generate the event
140 open(testfile)
141@@ -1194,7 +1183,7 @@
142 self.eq.add_to_mute_filter("FS_FILE_CLOSE_NOWRITE", testfile)
143
144 self.eq.inotify_add_watch(self.root_dir)
145- self.eq.subscribe(self.DontHitMe(self))
146+ self.eq.subscribe(DontHitMe(self))
147
148 # generate the event
149 fh.close()
150@@ -1209,7 +1198,7 @@
151 self.eq.add_to_mute_filter("FS_FILE_CLOSE_WRITE", testfile)
152
153 self.eq.inotify_add_watch(self.root_dir)
154- self.eq.subscribe(self.DontHitMe(self))
155+ self.eq.subscribe(DontHitMe(self))
156
157 # generate the event
158 open(testfile, "w").close()
159@@ -1222,7 +1211,7 @@
160 self.eq.add_to_mute_filter("FS_DIR_CREATE", testdir)
161
162 self.eq.inotify_add_watch(self.root_dir)
163- self.eq.subscribe(self.DontHitMe(self))
164+ self.eq.subscribe(DontHitMe(self))
165
166 # generate the event
167 os.mkdir(testdir)
168@@ -1236,7 +1225,7 @@
169 self.eq.add_to_mute_filter("FS_FILE_DELETE", testfile)
170
171 self.eq.inotify_add_watch(self.root_dir)
172- self.eq.subscribe(self.DontHitMe(self))
173+ self.eq.subscribe(DontHitMe(self))
174
175 # generate the event
176 os.remove(testfile)
177@@ -1250,7 +1239,7 @@
178 self.eq.add_to_mute_filter("FS_DIR_DELETE", testdir)
179
180 self.eq.inotify_add_watch(self.root_dir)
181- self.eq.subscribe(self.DontHitMe(self))
182+ self.eq.subscribe(DontHitMe(self))
183
184 # generate the event
185 os.rmdir(testdir)
186@@ -1269,7 +1258,7 @@
187 self.eq.add_to_mute_filter("FS_FILE_MOVE", fromfile, tofile)
188
189 self.eq.inotify_add_watch(self.root_dir)
190- self.eq.subscribe(self.DontHitMe(self))
191+ self.eq.subscribe(DontHitMe(self))
192
193 # generate the event
194 os.rename(fromfile, tofile)
195@@ -1288,7 +1277,7 @@
196 self.eq.add_to_mute_filter("FS_DIR_MOVE", fromdir, todir)
197
198 self.eq.inotify_add_watch(self.root_dir)
199- self.eq.subscribe(self.DontHitMe(self))
200+ self.eq.subscribe(DontHitMe(self))
201
202 # generate the event
203 os.rename(fromdir, todir)
204@@ -1307,7 +1296,7 @@
205 self.eq.add_to_mute_filter("FS_FILE_MOVE", fromfile, tofile)
206
207 self.eq.inotify_add_watch(self.root_dir)
208- self.eq.subscribe(self.DontHitMe(self))
209+ self.eq.subscribe(DontHitMe(self))
210
211 # generate the event
212 os.rename(fromfile, tofile)
213@@ -1325,7 +1314,7 @@
214 self.eq.add_to_mute_filter("FS_FILE_CLOSE_WRITE", tofile)
215
216 self.eq.inotify_add_watch(root_dir)
217- self.eq.subscribe(self.DontHitMe(self))
218+ self.eq.subscribe(DontHitMe(self))
219
220 # generate the event
221 os.rename(fromfile, tofile)
222@@ -1727,6 +1716,140 @@
223 return self._deferred
224
225
226+class NonUTF8NamesTests(BaseTwisted):
227+ """Test the non-utf8 name handling."""
228+
229+ invalid_name = "invalid \xff\xff name"
230+
231+ def test_file_open(self):
232+ """Test invalid_filename after open a file."""
233+ testfile = os.path.join(self.root_dir, self.invalid_name)
234+ open(testfile, "w").close()
235+
236+ self.eq.inotify_add_watch(self.root_dir)
237+ should_events = [
238+ ('FS_INVALID_NAME', self.root_dir, self.invalid_name), # open
239+ ('FS_INVALID_NAME', self.root_dir, self.invalid_name), # close no w
240+ ]
241+ self.eq.subscribe(DynamicHitMe(should_events, self))
242+
243+ # generate the event
244+ open(testfile)
245+ return self._deferred
246+
247+ def test_file_close_nowrite(self):
248+ """Test invalid_filename after a close no write."""
249+ testfile = os.path.join(self.root_dir, self.invalid_name)
250+ open(testfile, "w").close()
251+ fh = open(testfile)
252+
253+ self.eq.inotify_add_watch(self.root_dir)
254+ should_events = [
255+ ('FS_INVALID_NAME', self.root_dir, self.invalid_name), # close no w
256+ ]
257+ self.eq.subscribe(DynamicHitMe(should_events, self))
258+
259+ # generate the event
260+ fh.close()
261+ return self._deferred
262+
263+ def test_file_create_close_write(self):
264+ """Test invalid_filename after a create, open and close write."""
265+ testfile = os.path.join(self.root_dir, self.invalid_name)
266+
267+ self.eq.inotify_add_watch(self.root_dir)
268+ should_events = [
269+ ('FS_INVALID_NAME', self.root_dir, self.invalid_name), # create
270+ ('FS_INVALID_NAME', self.root_dir, self.invalid_name), # open
271+ ('FS_INVALID_NAME', self.root_dir, self.invalid_name), # close w
272+ ]
273+ self.eq.subscribe(DynamicHitMe(should_events, self))
274+
275+ # generate the event
276+ open(testfile, "w").close()
277+ return self._deferred
278+
279+ def test_dir_create(self):
280+ """Test invalid_filename after a dir create."""
281+ testdir = os.path.join(self.root_dir, self.invalid_name)
282+
283+ self.eq.inotify_add_watch(self.root_dir)
284+ should_events = [
285+ ('FS_INVALID_NAME', self.root_dir, self.invalid_name), # create
286+ ]
287+ self.eq.subscribe(DynamicHitMe(should_events, self))
288+
289+ # generate the event
290+ os.mkdir(testdir)
291+ return self._deferred
292+
293+ def test_file_delete(self):
294+ """Test invalid_filename after a file delete."""
295+ testfile = os.path.join(self.root_dir, self.invalid_name)
296+ open(testfile, "w").close()
297+
298+ self.eq.inotify_add_watch(self.root_dir)
299+ should_events = [
300+ ('FS_INVALID_NAME', self.root_dir, self.invalid_name), # delete
301+ ]
302+ self.eq.subscribe(DynamicHitMe(should_events, self))
303+
304+ # generate the event
305+ os.remove(testfile)
306+ return self._deferred
307+
308+ def test_dir_delete(self):
309+ """Test invalid_filename after a dir delete."""
310+ testdir = os.path.join(self.root_dir, self.invalid_name)
311+ os.mkdir(testdir)
312+
313+ self.eq.inotify_add_watch(self.root_dir)
314+ should_events = [
315+ ('FS_INVALID_NAME', self.root_dir, self.invalid_name), # delete
316+ ]
317+ self.eq.subscribe(DynamicHitMe(should_events, self))
318+
319+ # generate the event
320+ os.rmdir(testdir)
321+ return self._deferred
322+
323+ def test_file_move_to(self):
324+ """Test invalid_filename after moving a file into a watched dir."""
325+ fromfile = os.path.join(self.root_dir, self.invalid_name)
326+ open(fromfile, "w").close()
327+ destdir = os.path.join(self.root_dir, "watched_dir")
328+ os.mkdir(destdir)
329+ destfile = os.path.join(destdir, self.invalid_name)
330+
331+ self.eq.inotify_add_watch(destdir)
332+ should_events = [
333+ ('FS_INVALID_NAME', destdir, self.invalid_name), # move to
334+ ]
335+ self.eq.subscribe(DynamicHitMe(should_events, self))
336+
337+ # generate the event
338+ os.rename(fromfile, destfile)
339+ return self._deferred
340+
341+ def test_file_move_from(self):
342+ """Test invalid_filename after moving a file from a watched dir."""
343+ fromdir = os.path.join(self.root_dir, "watched_dir")
344+ os.mkdir(fromdir)
345+ fromfile = os.path.join(fromdir, self.invalid_name)
346+ open(fromfile, "w").close()
347+ destfile = os.path.join(self.root_dir, self.invalid_name)
348+
349+ self.eq.inotify_add_watch(fromdir)
350+ should_events = [
351+ ('FS_INVALID_NAME', fromdir, self.invalid_name), # move from
352+ ]
353+ self.eq.subscribe(DynamicHitMe(should_events, self))
354+
355+ # generate the event
356+ os.rename(fromfile, destfile)
357+ return self._deferred
358+
359+
360 def test_suite():
361 # pylint: disable-msg=C0111
362 return unittest.TestLoader().loadTestsFromName(__name__)
363
364=== modified file 'ubuntuone/syncdaemon/dbus_interface.py'
365--- ubuntuone/syncdaemon/dbus_interface.py 2010-03-23 22:48:19 +0000
366+++ ubuntuone/syncdaemon/dbus_interface.py 2010-03-26 18:31:32 +0000
367@@ -285,6 +285,11 @@
368 """ Fire a D-BUS signal, notifying an upload has finished. """
369 pass
370
371+ @dbus.service.signal(DBUS_IFACE_STATUS_NAME, signature='say')
372+ def InvalidName(self, dirname, filename):
373+ """Fire a D-BUS signal, notifying an invalid file or dir name."""
374+ pass
375+
376 @dbus.service.signal(DBUS_IFACE_STATUS_NAME)
377 def StatusChanged(self, status):
378 """ Fire a D-BUS signal, notifying that the status of the
379@@ -333,6 +338,10 @@
380 'size': '0'}
381 self.ContentQueueChanged(message)
382
383+ def emit_invalid_name(self, dirname, filename):
384+ """Emits the signal."""
385+ self.InvalidName(unicode(dirname), str(filename))
386+
387 def emit_status_changed(self, state):
388 """Emits the signal."""
389 state_dict = {'name':state.name,
390@@ -551,6 +560,11 @@
391 upload_error=str(error))
392 self.dbus_iface.status.emit_signal_error('UploadFinished', args)
393
394+ def handle_FS_INVALID_NAME(self, dirname, filename):
395+ """Handle FS_INVALID_NAME."""
396+ self.handle_default('FS_INVALID_NAME', dirname, filename)
397+ self.dbus_iface.status.emit_invalid_name(dirname, filename)
398+
399 def handle_SYS_STATE_CHANGED(self, state):
400 """ handle SYS_STATE_CHANGED """
401 self.handle_default('SYS_STATE_CHANGED', state)
402
403=== modified file 'ubuntuone/syncdaemon/event_queue.py'
404--- ubuntuone/syncdaemon/event_queue.py 2010-03-24 12:44:39 +0000
405+++ ubuntuone/syncdaemon/event_queue.py 2010-03-26 18:31:32 +0000
406@@ -41,6 +41,7 @@
407 'FS_DIR_DELETE': ('path',),
408 'FS_FILE_MOVE': ('path_from', 'path_to',),
409 'FS_DIR_MOVE': ('path_from', 'path_to',),
410+ 'FS_INVALID_NAME': ('dirname', 'filename',),
411
412 'AQ_FILE_NEW_OK': ('marker', 'new_id'),
413 'AQ_FILE_NEW_ERROR': ('marker', 'error'),
414@@ -184,6 +185,23 @@
415 DEFAULT_HANDLER = "handle_default" # receives (event_name, *args, **kwargs)
416
417
418+def validate_filename(real_func):
419+ """Decorator that validates the filename."""
420+ def func(self, event):
421+ """If valid, executes original function."""
422+ try:
423+ # validate UTF-8
424+ event.name.decode("utf8")
425+ except UnicodeDecodeError:
426+ dirname = event.path.decode("utf8")
427+ self.invnames_log.info("%s in %r: path %r", event.maskname,
428+ dirname, event.name)
429+ self.eq.push('FS_INVALID_NAME', dirname, event.name)
430+ else:
431+ real_func(self, event)
432+ return func
433+
434+
435 class MuteFilter(object):
436 """Stores what needs to be muted."""
437 def __init__(self):
438@@ -294,6 +312,8 @@
439 """
440 def __init__(self, eq, ignore_config=None):
441 self.log = logging.getLogger('ubuntuone.SyncDaemon.GeneralINotProc')
442+ self.invnames_log = logging.getLogger(
443+ 'ubuntuone.SyncDaemon.InvalidNames')
444 self.eq = eq
445 self.held_event = None
446 self.timer = None
447@@ -351,11 +371,13 @@
448 self.push_event(self.held_event)
449 self.held_event = None
450
451+ @validate_filename
452 def process_IN_OPEN(self, event):
453 """Filter IN_OPEN to make it happen only in files."""
454 if not (event.mask & evtcodes.IN_ISDIR):
455 self.push_event(event)
456
457+ @validate_filename
458 def process_IN_CLOSE_NOWRITE(self, event):
459 """Filter IN_CLOSE_NOWRITE to make it happen only in files."""
460 if not (event.mask & evtcodes.IN_ISDIR):
461@@ -369,6 +391,7 @@
462
463 """
464
465+ @validate_filename
466 def process_IN_MOVED_FROM(self, event):
467 """Capture the MOVED_FROM to maybe syntethize FILE_MOVED."""
468 if self.held_event is not None:
469@@ -409,6 +432,7 @@
470
471 return False
472
473+ @validate_filename
474 def process_IN_MOVED_TO(self, event):
475 """Capture the MOVED_TO to maybe syntethize FILE_MOVED."""
476 if self.held_event is not None:
477@@ -476,6 +500,7 @@
478 if not self._to_mute.pop(event_data):
479 self.eq.push(*event_data)
480
481+ @validate_filename
482 def process_default(self, event):
483 """Push the event into the EventQueue."""
484 if self.held_event is not None:
485
486=== modified file 'ubuntuone/syncdaemon/logger.py'
487--- ubuntuone/syncdaemon/logger.py 2010-02-15 17:19:53 +0000
488+++ ubuntuone/syncdaemon/logger.py 2010-03-26 18:31:32 +0000
489@@ -334,6 +334,7 @@
490
491 LOGFILENAME = os.path.join(LOGFOLDER, 'syncdaemon.log')
492 EXLOGFILENAME = os.path.join(LOGFOLDER, 'syncdaemon-exceptions.log')
493+INVALIDLOGFILENAME = os.path.join(LOGFOLDER, 'syncdaemon-invalid-names.log')
494
495 LOGBACKUP = 5 # the number of log files to keep around
496
497@@ -397,6 +398,14 @@
498 inotify_logger.setLevel(logging.ERROR)
499 inotify_logger.propagate = False
500
501+# invalid filenames log
502+invnames_logger = logging.getLogger("ubuntuone.SyncDaemon.InvalidNames")
503+invnames_logger.setLevel(_DEBUG_LOG_LEVEL)
504+invnames_handler = CustomRotatingFileHandler(filename=INVALIDLOGFILENAME)
505+invnames_handler.setFormatter(basic_formatter)
506+invnames_handler.setLevel(logging.INFO)
507+invnames_logger.addHandler(invnames_handler)
508+
509
510 def configure_logging(level, maxBytes, backupCount):
511 """configure level, maxBytes and backupCount in all handlers"""

Subscribers

People subscribed via source and target branches