class _GeneralINotifyProcessor(pyinotify.ProcessEvent): """inotify's processor when a general event happens. This class also catchs the MOVEs events, and synthetises a new FS_(DIR|FILE)_MOVE event when possible. """ def __init__(self, monitor, ignore_config=None): # XXX: avoid circular imports from ubuntuone.syncdaemon.filesystem_notifications import ( GeneralINotifyProcessor ) self.general_processor = GeneralINotifyProcessor(monitor, self.handle_dir_delete, NAME_TRANSLATIONS, self.platform_is_ignored, pyinotify.IN_IGNORED, ignore_config=ignore_config) self.held_event = None self.timer = None def shutdown(self): """Shut down the processor.""" if self.timer is not None and self.timer.active(): self.timer.cancel() def rm_from_mute_filter(self, event, paths): """Remove an event and path(s) from the mute filter.""" self.general_processor.rm_from_mute_filter(event, paths) def add_to_mute_filter(self, event, paths): """Add an event and path(s) to the mute filter.""" self.general_processor.add_to_mute_filter(event, paths) def on_timeout(self): """Called on timeout.""" if self.held_event is not None: self.release_held_event(True) def release_held_event(self, timed_out=False): """Release the event on hold to fulfill its destiny.""" if not timed_out: try: self.timer.cancel() except error.AlreadyCalled: # self.timeout() was *just* called, do nothing here return self.general_processor.push_event(self.held_event) self.held_event = None @validate_filename def process_IN_OPEN(self, event): """Filter IN_OPEN to make it happen only in files.""" if not (event.mask & pyinotify.IN_ISDIR): self.general_processor.push_event(event) @validate_filename def process_IN_CLOSE_NOWRITE(self, event): """Filter IN_CLOSE_NOWRITE to make it happen only in files.""" if not (event.mask & pyinotify.IN_ISDIR): self.general_processor.push_event(event) def process_IN_MOVE_SELF(self, event): """Don't do anything here. We just turned this event on because pyinotify does some path-fixing in its internal processing when this happens. """ @validate_filename def process_IN_MOVED_FROM(self, event): """Capture the MOVED_FROM to maybe syntethize FILE_MOVED.""" if self.held_event is not None: self.release_held_event() self.held_event = event self.timer = reactor.callLater(1, self.on_timeout) def platform_is_ignored(self, path): """Should we ignore this path in the current platform.?""" # don't support links yet if os.path.islink(path): return True return False def is_ignored(self, path): """Should we ignore this path?""" return self.general_processor.is_ignored(path) @validate_filename def process_IN_MOVED_TO(self, event): """Capture the MOVED_TO to maybe syntethize FILE_MOVED.""" if self.held_event is not None: if event.cookie == self.held_event.cookie: try: self.timer.cancel() except error.AlreadyCalled: # self.timeout() was *just* called, do nothing here pass else: f_path_dir = self.held_event.path f_path = os.path.join(f_path_dir, self.held_event.name) t_path_dir = event.path t_path = os.path.join(t_path_dir, event.name) is_from_forreal = not self.is_ignored(f_path) is_to_forreal = not self.is_ignored(t_path) if is_from_forreal and is_to_forreal: f_share_id = self.general_processor.get_path_share_id( f_path_dir) t_share_id = self.general_processor.get_path_share_id( t_path_dir) if event.dir: evtname = "FS_DIR_" else: evtname = "FS_FILE_" if f_share_id != t_share_id: # if the share_id are != push a delete/create m = "Delete because of different shares: %r" self.general_processor.log.info(m, f_path) self.general_processor.eq_push(evtname+"DELETE", path=f_path) self.general_processor.eq_push(evtname+"CREATE", path=t_path) if not event.dir: self.general_processor.eq_push( 'FS_FILE_CLOSE_WRITE', path=t_path) else: self.general_processor.monitor.inotify_watch_fix( f_path, t_path) self.general_processor.eq_push(evtname+"MOVE", path_from=f_path, path_to=t_path) elif is_to_forreal: # this is the case of a MOVE from something ignored # to a valid filename if event.dir: evtname = "FS_DIR_" else: evtname = "FS_FILE_" self.general_processor.eq_push(evtname + "CREATE", path=t_path) if not event.dir: self.general_processor.eq_push( 'FS_FILE_CLOSE_WRITE', path=t_path) else: # this is the case of a MOVE from something valid # to an ignored filename if event.dir: evtname = "FS_DIR_" else: evtname = "FS_FILE_" self.general_processor.eq_push(evtname + "DELETE", path=f_path) self.held_event = None return else: self.release_held_event() self.general_processor.push_event(event) else: # we don't have a held_event so this is a move from outside. # if it's a file move it's atomic on POSIX, so we aren't going to # receive a IN_CLOSE_WRITE, so let's fake it for files self.general_processor.push_event(event) if not event.dir: t_path = os.path.join(event.path, event.name) self.general_processor.eq_push('FS_FILE_CLOSE_WRITE', path=t_path) @validate_filename def process_default(self, event): """Push the event into the EventQueue.""" if self.held_event is not None: self.release_held_event() self.general_processor.push_event(event) def freeze_begin(self, path): """Puts in hold all the events for this path.""" self.general_processor.freeze_begin(path) def freeze_rollback(self): """Unfreezes the frozen path, reseting to idle state.""" self.general_processor.freeze_rollback() def freeze_commit(self, events): """Unfreezes the frozen path, sending received events if not dirty. If events for that path happened: - return True else: - push the here received events, return False """ return self.general_processor.freeze_commit(events) def handle_dir_delete(self, fullpath): """Some special work when a directory is deleted.""" # remove the watch on that dir from our structures self.general_processor.rm_watch(fullpath) # handle the case of move a dir to a non-watched directory paths = self.general_processor.get_paths_starting_with(fullpath, include_base=False) paths.sort(reverse=True) for path, is_dir in paths: m = "Pushing deletion because of parent dir move: (is_dir=%s) %r" self.general_processor.log.info(m, is_dir, path) if is_dir: self.general_processor.rm_watch(path) self.general_processor.eq_push('FS_DIR_DELETE', path=path) else: self.general_processor.eq_push('FS_FILE_DELETE', path=path) @property def mute_filter(self): """Return the mute filter used by the processor.""" return self.general_processor.filter @property def frozen_path(self): """Return the frozen path.""" return self.general_processor.frozen_path @property def log(self): """Return the logger of the instance.""" return self.general_processor.log
class NotifyProcessor(ProcessEvent): """Processor that takes care of dealing with the events. This interface will be exposed to syncdaemon, ergo all passed and returned paths must be a sequence of BYTES encoded with utf8. Also, they must not be literal paths, that is the \\?\ prefix should not be in the path. """ def __init__(self, monitor, ignore_config=None): # XXX: avoid circular imports. from ubuntuone.syncdaemon.filesystem_notifications import ( GeneralINotifyProcessor) self.general_processor = GeneralINotifyProcessor(monitor, self.handle_dir_delete, NAME_TRANSLATIONS, self.platform_is_ignored, IN_IGNORED, ignore_config=ignore_config) self.held_event = None def rm_from_mute_filter(self, event, paths): """Remove event from the mute filter.""" self.general_processor.rm_from_mute_filter(event, paths) def add_to_mute_filter(self, event, paths): """Add an event and path(s) to the mute filter.""" self.general_processor.add_to_mute_filter(event, paths) @is_valid_syncdaemon_path(path_indexes=[1]) def platform_is_ignored(self, path): """Should we ignore this path in the current platform.?""" # don't support links yet if path.endswith('.lnk'): return True return False @is_valid_syncdaemon_path(path_indexes=[1]) def is_ignored(self, path): """Should we ignore this path?""" return self.general_processor.is_ignored(path) def release_held_event(self, timed_out=False): """Release the event on hold to fulfill its destiny.""" self.general_processor.push_event(self.held_event) self.held_event = None def process_IN_MODIFY(self, event): """Capture a modify event and fake an open ^ close write events.""" # lets ignore dir changes if event.dir: return # on windows we just get IN_MODIFY, lets always fake # an OPEN & CLOSE_WRITE couple raw_open = raw_close = { 'wd': event.wd, 'dir': event.dir, 'name': event.name, 'path': event.path} # caculate the open mask raw_open['mask'] = IN_OPEN # create the event using the raw data, then fix the pathname param open_event = Event(raw_open) open_event.pathname = event.pathname # push the open self.general_processor.push_event(open_event) raw_close['mask'] = IN_CLOSE_WRITE close_event = Event(raw_close) close_event.pathname = event.pathname # push the close event self.general_processor.push_event(close_event) def process_IN_MOVED_FROM(self, event): """Capture the MOVED_FROM to maybe syntethize FILE_MOVED.""" if self.held_event is not None: self.general_processor.log.warn('Lost pair event of %s', self.held_event) self.held_event = event def _fake_create_event(self, event): """Fake the creation of an event.""" # this is the case of a MOVE from an ignored path (links for example) # to a valid path if event.dir: evtname = "FS_DIR_" else: evtname = "FS_FILE_" self.general_processor.eq_push(evtname + "CREATE", path=event.pathname) if not event.dir: self.general_processor.eq_push('FS_FILE_CLOSE_WRITE', path=event.pathname) def _fake_delete_create_event(self, event): """Fake the deletion and the creation.""" # this is the case of a MOVE from a watch UDF to a diff UDF which # means that we have to copy the way linux works. if event.dir: evtname = "FS_DIR_" else: evtname = "FS_FILE_" m = "Delete because of different shares: %r" self.log.info(m, self.held_event.pathname) self.general_processor.eq_push(evtname + "DELETE", path=self.held_event.pathname) self.general_processor.eq_push(evtname + "CREATE", path=event.pathname) if not event.dir: self.general_processor.eq_push('FS_FILE_CLOSE_WRITE', path=event.pathname) def process_IN_MOVED_TO(self, event): """Capture the MOVED_TO to maybe syntethize FILE_MOVED.""" if self.held_event is not None: if event.cookie == self.held_event.cookie: f_path_dir = os.path.split(self.held_event.pathname)[0] t_path_dir = os.path.split(event.pathname)[0] is_from_forreal = not self.is_ignored(self.held_event.pathname) is_to_forreal = not self.is_ignored(event.pathname) if is_from_forreal and is_to_forreal: f_share_id = self.general_processor.get_path_share_id( f_path_dir) t_share_id = self.general_processor.get_path_share_id( t_path_dir) if f_share_id != t_share_id: # if the share_id are != push a delete/create self._fake_delete_create_event(event) else: if event.dir: evtname = "FS_DIR_" else: evtname = "FS_FILE_" self.general_processor.eq_push(evtname + "MOVE", path_from=self.held_event.pathname, path_to=event.pathname) elif is_to_forreal: # this is the case of a MOVE from something ignored # to a valid filename self._fake_create_event(event) self.held_event = None return else: self.release_held_event() self.general_processor.push_event(event) else: # We should never get here on windows, I really do not know how we # got here self.general_processor.log.warn( 'Cookie does not match the previoues held event!') self.general_processor.log.warn('Ignoring %s', event) def process_default(self, event): """Push the event into the EventQueue.""" if self.held_event is not None: self.release_held_event() self.general_processor.push_event(event) @is_valid_syncdaemon_path(path_indexes=[1]) def handle_dir_delete(self, fullpath): """Some special work when a directory is deleted.""" # remove the watch on that dir from our structures, this mainly tells # the monitor to remove the watch which is fowaded to a watch manager. self.general_processor.rm_watch(fullpath) # handle the case of move a dir to a non-watched directory paths = self.general_processor.get_paths_starting_with(fullpath, include_base=False) paths.sort(reverse=True) for path, is_dir in paths: m = "Pushing deletion because of parent dir move: (is_dir=%s) %r" self.general_processor.log.info(m, is_dir, path) if is_dir: # same as the above remove self.general_processor.rm_watch(path) self.general_processor.eq_push('FS_DIR_DELETE', path=path) else: self.general_processor.eq_push('FS_FILE_DELETE', path=path) @is_valid_syncdaemon_path(path_indexes=[1]) def freeze_begin(self, path): """Puts in hold all the events for this path.""" self.general_processor.freeze_begin(path) def freeze_rollback(self): """Unfreezes the frozen path, reseting to idle state.""" self.general_processor.freeze_rollback() def freeze_commit(self, events): """Unfreezes the frozen path, sending received events if not dirty. If events for that path happened: - return True else: - push the here received events, return False """ return self.general_processor.freeze_commit(events) @property def mute_filter(self): """Return the mute filter used by the processor.""" return self.general_processor.filter @property def frozen_path(self): """Return the frozen path.""" return self.general_processor.frozen_path @property def log(self): """Return the logger of the instance.""" return self.general_processor.log