~ubuntu-branches/ubuntu/oneiric/ubuntuone-client/oneiric

« back to all changes in this revision

Viewing changes to ubuntuone/platform/linux/filesystem_notifications.py

  • Committer: Bazaar Package Importer
  • Author(s): Rodney Dawes
  • Date: 2011-02-23 18:34:09 UTC
  • mfrom: (1.1.45 upstream)
  • Revision ID: james.westby@ubuntu.com-20110223183409-535o7yo165wbjmca
Tags: 1.5.5-0ubuntu1
* New upstream release.
  - Subscribing to a RO share will not download content (LP: #712528)
  - Can't synchronize "~/Ubuntu One Music" (LP: #714976)
  - Syncdaemon needs to show progress in Unity launcher (LP: #702116)
  - Notifications say "your cloud" (LP: #715887)
  - No longer requires python-libproxy
  - Recommend unity and indicator libs by default

Show diffs side-by-side

added added

removed removed

Lines of Context:
19
19
 
20
20
import logging
21
21
import os
22
 
import re
23
22
 
24
23
import pyinotify
25
24
from twisted.internet import abstract, reactor, error, defer
26
25
 
27
 
from ubuntuone.syncdaemon.mute_filter import MuteFilter
 
26
from ubuntuone.syncdaemon.filesystem_notifications import (
 
27
    GeneralINotifyProcessor
 
28
)
28
29
 
29
30
# translates quickly the event and it's is_dir state to our standard events
30
31
NAME_TRANSLATIONS = {
69
70
            event.name.decode("utf8")
70
71
        except UnicodeDecodeError:
71
72
            dirname = event.path.decode("utf8")
72
 
            self.invnames_log.info("%s in %r: path %r", event.maskname,
 
73
            self.general_processor.invnames_log.info("%s in %r: path %r", event.maskname,
73
74
                                   dirname, event.name)
74
 
            self.monitor.eq.push('FS_INVALID_NAME',
 
75
            self.general_processor.monitor.eq.push('FS_INVALID_NAME',
75
76
                                 dirname=dirname, filename=event.name)
76
77
        else:
77
78
            real_func(self, event)
150
151
    FS_(DIR|FILE)_MOVE event when possible.
151
152
    """
152
153
    def __init__(self, monitor, ignore_config=None):
153
 
        self.log = logging.getLogger('ubuntuone.SyncDaemon.GeneralINotProc')
154
 
        self.invnames_log = logging.getLogger(
155
 
                                        'ubuntuone.SyncDaemon.InvalidNames')
156
 
        self.monitor = monitor
 
154
        self.general_processor = GeneralINotifyProcessor(monitor,
 
155
            self.handle_dir_delete, NAME_TRANSLATIONS,
 
156
            self.platform_is_ignored, pyinotify.IN_IGNORED, 
 
157
            ignore_config=ignore_config)
157
158
        self.held_event = None
158
159
        self.timer = None
159
 
        self.frozen_path = None
160
 
        self.frozen_evts = False
161
 
        self._to_mute = MuteFilter()
162
 
        self.conflict_RE = re.compile(r"\.u1conflict(?:\.\d+)?$")
163
 
 
164
 
        if ignore_config is not None:
165
 
            self.log.info("Ignoring files: %s", ignore_config)
166
 
            # thanks Chipaca for the following "regex composing"
167
 
            complex = '|'.join('(?:' + r + ')' for r in ignore_config)
168
 
            self.ignore_RE = re.compile(complex)
169
 
        else:
170
 
            self.ignore_RE = None
171
 
 
172
 
    def _mute_filter(self, action, event, paths):
173
 
        """Really touches the mute filter."""
174
 
        # all events have one path except the MOVEs
175
 
        if event in ("FS_FILE_MOVE", "FS_DIR_MOVE"):
176
 
            f_path, t_path = paths['path_from'], paths['path_to']
177
 
            is_from_forreal = not self.is_ignored(f_path)
178
 
            is_to_forreal = not self.is_ignored(t_path)
179
 
            if is_from_forreal and is_to_forreal:
180
 
                action(event, **paths)
181
 
            elif is_to_forreal:
182
 
                action('FS_FILE_CREATE', path=t_path)
183
 
                action('FS_FILE_CLOSE_WRITE', path=t_path)
184
 
        else:
185
 
            path = paths['path']
186
 
            if not self.is_ignored(path):
187
 
                action(event, **paths)
188
160
 
189
161
    def rm_from_mute_filter(self, event, paths):
190
162
        """Remove an event and path(s) from the mute filter."""
191
 
        self._mute_filter(self._to_mute.rm, event, paths)
 
163
        self.general_processor.rm_from_mute_filter(event, paths)
192
164
 
193
165
    def add_to_mute_filter(self, event, paths):
194
166
        """Add an event and path(s) to the mute filter."""
195
 
        self._mute_filter(self._to_mute.add, event, paths)
 
167
        self.general_processor.add_to_mute_filter(event, paths)
196
168
 
197
169
    def on_timeout(self):
198
170
        """Called on timeout."""
207
179
            except error.AlreadyCalled:
208
180
                # self.timeout() was *just* called, do nothing here
209
181
                return
210
 
        self.push_event(self.held_event)
 
182
        self.general_processor.push_event(self.held_event)
211
183
        self.held_event = None
212
184
 
213
185
    @validate_filename
214
186
    def process_IN_OPEN(self, event):
215
187
        """Filter IN_OPEN to make it happen only in files."""
216
188
        if not (event.mask & pyinotify.IN_ISDIR):
217
 
            self.push_event(event)
 
189
            self.general_processor.push_event(event)
218
190
 
219
191
    @validate_filename
220
192
    def process_IN_CLOSE_NOWRITE(self, event):
221
193
        """Filter IN_CLOSE_NOWRITE to make it happen only in files."""
222
194
        if not (event.mask & pyinotify.IN_ISDIR):
223
 
            self.push_event(event)
 
195
            self.general_processor.push_event(event)
224
196
 
225
197
    def process_IN_MOVE_SELF(self, event):
226
198
        """Don't do anything here.
239
211
        self.held_event = event
240
212
        self.timer = reactor.callLater(1, self.on_timeout)
241
213
 
 
214
    def platform_is_ignored(self, path):
 
215
        """Should we ignore this path in the current platform.?"""
 
216
        # don't support links yet
 
217
        if os.path.islink(path):
 
218
            return True
 
219
        return False
 
220
 
242
221
    def is_ignored(self, path):
243
 
        """should we ignore this path?"""
244
 
        # don't support symlinks yet
245
 
        if os.path.islink(path):
246
 
            return True
247
 
 
248
 
        # check if we are can read
249
 
        if os.path.exists(path) and not os.access(path, os.R_OK):
250
 
            self.log.warning("Ignoring path as we don't have enough "
251
 
                             "permissions to track it: %r", path)
252
 
            return True
253
 
 
254
 
        is_conflict = self.conflict_RE.search
255
 
        dirname, filename = os.path.split(path)
256
 
        # ignore conflicts
257
 
        if is_conflict(filename):
258
 
            return True
259
 
        # ignore partial downloads
260
 
        if filename == '.u1partial' or filename.startswith('.u1partial.'):
261
 
            return True
262
 
 
263
 
        # and ignore paths that are inside conflicts (why are we even
264
 
        # getting the event?)
265
 
        if any(part.endswith('.u1partial') or is_conflict(part)
266
 
               for part in dirname.split(os.path.sep)):
267
 
            return True
268
 
 
269
 
        if self.ignore_RE is not None and self.ignore_RE.match(filename):
270
 
            return True
271
 
 
272
 
        return False
 
222
        """Should we ignore this path?"""
 
223
        return self.general_processor.is_ignored(path)
273
224
 
274
225
    @validate_filename
275
226
    def process_IN_MOVED_TO(self, event):
290
241
                    is_from_forreal = not self.is_ignored(f_path)
291
242
                    is_to_forreal = not self.is_ignored(t_path)
292
243
                    if is_from_forreal and is_to_forreal:
293
 
                        f_share_id = self.monitor.fs.get_by_path(f_path_dir).share_id
294
 
                        t_share_id = self.monitor.fs.get_by_path(t_path_dir).share_id
 
244
                        f_share_id = self.general_processor.get_path_share_id(
 
245
                            f_path_dir)
 
246
                        t_share_id = self.general_processor.get_path_share_id(
 
247
                            t_path_dir)
295
248
                        if event.dir:
296
249
                            evtname = "FS_DIR_"
297
250
                        else:
299
252
                        if f_share_id != t_share_id:
300
253
                            # if the share_id are != push a delete/create
301
254
                            m = "Delete because of different shares: %r"
302
 
                            self.log.info(m, f_path)
303
 
                            self.eq_push(evtname+"DELETE", path=f_path)
304
 
                            self.eq_push(evtname+"CREATE", path=t_path)
 
255
                            self.general_processor.log.info(m, f_path)
 
256
                            self.general_processor.eq_push(evtname+"DELETE", path=f_path)
 
257
                            self.general_processor.eq_push(evtname+"CREATE", path=t_path)
305
258
                            if not event.dir:
306
 
                                self.eq_push('FS_FILE_CLOSE_WRITE',
 
259
                                self.general_processor.eq_push('FS_FILE_CLOSE_WRITE',
307
260
                                             path=t_path)
308
261
                        else:
309
 
                            self.monitor.inotify_watch_fix(f_path, t_path)
310
 
                            self.eq_push(evtname+"MOVE",
 
262
                            self.general_processor.monitor.inotify_watch_fix(f_path, t_path)
 
263
                            self.general_processor.eq_push(evtname+"MOVE",
311
264
                                         path_from=f_path, path_to=t_path)
312
265
                    elif is_to_forreal:
313
266
                        # this is the case of a MOVE from something ignored
316
269
                            evtname = "FS_DIR_"
317
270
                        else:
318
271
                            evtname = "FS_FILE_"
319
 
                        self.eq_push(evtname + "CREATE", path=t_path)
 
272
                        self.general_processor.eq_push(evtname + "CREATE", path=t_path)
320
273
                        if not event.dir:
321
 
                            self.eq_push('FS_FILE_CLOSE_WRITE', path=t_path)
 
274
                            self.general_processor.eq_push('FS_FILE_CLOSE_WRITE', path=t_path)
322
275
 
323
276
                    self.held_event = None
324
277
                return
325
278
            else:
326
279
                self.release_held_event()
327
 
                self.push_event(event)
 
280
                self.general_processor.push_event(event)
328
281
        else:
329
282
            # we don't have a held_event so this is a move from outside.
330
283
            # if it's a file move it's atomic on POSIX, so we aren't going to
331
284
            # receive a IN_CLOSE_WRITE, so let's fake it for files
332
 
            self.push_event(event)
 
285
            self.general_processor.push_event(event)
333
286
            if not event.dir:
334
287
                t_path = os.path.join(event.path, event.name)
335
 
                self.eq_push('FS_FILE_CLOSE_WRITE', path=t_path)
336
 
 
337
 
    def eq_push(self, event_name, **event_data):
338
 
        """Sends to EQ the event data, maybe filtering it."""
339
 
        if not self._to_mute.pop(event_name, **event_data):
340
 
            self.monitor.eq.push(event_name, **event_data)
 
288
                self.general_processor.eq_push('FS_FILE_CLOSE_WRITE', path=t_path)
341
289
 
342
290
    @validate_filename
343
291
    def process_default(self, event):
344
292
        """Push the event into the EventQueue."""
345
293
        if self.held_event is not None:
346
294
            self.release_held_event()
347
 
        self.push_event(event)
348
 
 
349
 
    def push_event(self, event):
350
 
        """Push the event to the EQ."""
351
 
        # ignore this trash
352
 
        if event.mask == pyinotify.IN_IGNORED:
353
 
            return
354
 
 
355
 
        # change the pattern IN_CREATE to FS_FILE_CREATE or FS_DIR_CREATE
356
 
        try:
357
 
            evt_name = NAME_TRANSLATIONS[event.mask]
358
 
        except:
359
 
            raise KeyError("Unhandled Event in INotify: %s" % event)
360
 
 
361
 
        # push the event
362
 
        fullpath = os.path.join(event.path, event.name)
363
 
 
364
 
        # check if the path is not frozen
365
 
        if self.frozen_path is not None:
366
 
            if event.path == self.frozen_path:
367
 
                # this will at least store the last one, for debug
368
 
                # purposses
369
 
                self.frozen_evts = (evt_name, fullpath)
370
 
                return
371
 
 
372
 
        if not self.is_ignored(fullpath):
373
 
            if evt_name == 'FS_DIR_DELETE':
374
 
                self.handle_dir_delete(fullpath)
375
 
            self.eq_push(evt_name, path=fullpath)
 
295
        self.general_processor.push_event(event)
 
296
 
376
297
 
377
298
    def freeze_begin(self, path):
378
299
        """Puts in hold all the events for this path."""
379
 
        self.log.debug("Freeze begin: %r", path)
380
 
        self.frozen_path = path
381
 
        self.frozen_evts = False
 
300
        self.general_processor.freeze_begin(path)
382
301
 
383
302
    def freeze_rollback(self):
384
303
        """Unfreezes the frozen path, reseting to idle state."""
385
 
        self.log.debug("Freeze rollback: %r", self.frozen_path)
386
 
        self.frozen_path = None
387
 
        self.frozen_evts = False
 
304
        self.general_processor.freeze_rollback()
388
305
 
389
306
    def freeze_commit(self, events):
390
307
        """Unfreezes the frozen path, sending received events if not dirty.
394
311
        else:
395
312
            - push the here received events, return False
396
313
        """
397
 
        self.log.debug("Freeze commit: %r (%d events)",
398
 
                                                self.frozen_path, len(events))
399
 
        if self.frozen_evts:
400
 
            # ouch! we're dirty!
401
 
            self.log.debug("Dirty by %s", self.frozen_evts)
402
 
            self.frozen_evts = False
403
 
            return True
404
 
 
405
 
        # push the received events
406
 
        for evt_name, path in events:
407
 
            if not self.is_ignored(path):
408
 
                self.eq_push(evt_name, path=path)
409
 
 
410
 
        self.frozen_path = None
411
 
        self.frozen_evts = False
412
 
        return False
 
314
        return self.general_processor.freeze_commit(events)
413
315
 
414
316
    def handle_dir_delete(self, fullpath):
415
317
        """Some special work when a directory is deleted."""
416
318
        # remove the watch on that dir from our structures
417
 
        self.monitor.rm_watch(fullpath)
 
319
        self.general_processor.rm_watch(fullpath)
418
320
 
419
321
        # handle the case of move a dir to a non-watched directory
420
 
        paths = self.monitor.fs.get_paths_starting_with(fullpath,
421
 
                                                   include_base=False)
 
322
        paths = self.general_processor.get_paths_starting_with(fullpath,
 
323
            include_base=False)
 
324
 
422
325
        paths.sort(reverse=True)
423
326
        for path, is_dir in paths:
424
327
            m = "Pushing deletion because of parent dir move: (is_dir=%s) %r"
425
 
            self.log.info(m, is_dir, path)
 
328
            self.general_processor.log.info(m, is_dir, path)
426
329
            if is_dir:
427
 
                self.monitor.rm_watch(path)
428
 
                self.eq_push('FS_DIR_DELETE', path=path)
 
330
                self.general_processor.rm_watch(path)
 
331
                self.general_processor.eq_push('FS_DIR_DELETE', path=path)
429
332
            else:
430
 
                self.eq_push('FS_FILE_DELETE', path=path)
 
333
                self.general_processor.eq_push('FS_FILE_DELETE', path=path)
 
334
 
 
335
    @property
 
336
    def mute_filter(self):
 
337
        """Return the mute filter used by the processor."""
 
338
        return self.general_processor.filter
 
339
 
 
340
    @property
 
341
    def frozen_path(self):
 
342
        """Return the frozen path."""
 
343
        return self.general_processor.frozen_path
 
344
 
 
345
    @property
 
346
    def log(self):
 
347
        """Return the logger of the instance."""
 
348
        return self.general_processor.log
431
349
 
432
350
 
433
351
class FilesystemMonitor(object):