class MacOSFileBaseSystem(FilesystemBasic): """MacOS specific Filesystem.""" is_mac = True """Used for testing to ensure that the Filesystem does subclass this class.""" observer = None """The observer which is instantiated in start_events.""" @property def real_root(self): """Return the realpath of the root. fs_events paths are realpaths. i.e.: without symlinks. """ return os.path.realpath(self.root) def fsevent_handler(self, path, mask, _id): """Handle events from fsevents, get the current directory content and update the model. Args: :param path: absolute path which caused this event. :param mask: bit mask of attributes set for this event. :param _id: internal id of the event. :type path: string :type mask: int :type _id: int ..Note: The flags on these events are badly/not documented. Log the events to make sure which flags are actualy set. Do not trust your intuition. After preliminary checks, the path is handed to self._update, to trigger the necessary events. """ event = FSEvent(path=path, mask=mask, event_id=_id) if file_ignored(event.path): logger.debug('Event ignored for %s', path) return cc_path = event.cc_path(self.real_root) if cc_path == ['.']: cc_path = [] logger.info('update for %s', cc_path) try: self._update(cc_path) except BaseException: logger.debug('got exception while processing FSEvents %s', exc_info=True) def _update(self, cc_path): """Inspect the directory to detect what has changed since the last call to `_update`. Args: :param cc_path: path of directory to update. :type cc_path: list of strings. """ logger.info('_update for %s for tree %s', cc_path, self.model) # ignore event if parent directory no longer exists on the fs parent_folder = cc_path_to_fs(cc_path[:-1], self.real_root) if not os.path.exists(parent_folder): logger.debug('Event ignored: parent folder no longer exist') return with TreeToSyncEngineEngineAdapter(node=self.model, storage_id=self.storage_id, sync_engine=self._event_sink): # Ensure that the path exists in the model. parent = self.model for idx, name in enumerate(cc_path): if parent.has_child(name): parent = parent.get_node([name]) else: partial_cc_path = cc_path[:idx + 1] parent = parent.add_child( name, props=self.get_props(partial_cc_path)) directory = cc_path_to_fs(cc_path, self.real_root) new_inodes = { props['_inode']: (name, props) for name, props in self.get_tree_children(cc_path) } old_inodes = { node.props['_inode']: node for node in parent.children } new_inodes_set = new_inodes.keys() old_inodes_set = old_inodes.keys() inode_intersection = new_inodes_set & old_inodes_set removed_inodes = old_inodes_set - new_inodes_set added_inodes = new_inodes_set - old_inodes_set for inode in inode_intersection: old_node = old_inodes[inode] new_node_name, new_node_props = new_inodes[inode] old_node.props.update(new_node_props) old_node.name = new_node_name for inode in removed_inodes: # TODO: might be moved to a different dir, might be deleted old_inodes[inode].delete() for inode in added_inodes: new_node_name, new_node_props = new_inodes[inode] new_node = parent.add_child(new_node_name, new_node_props) if new_node_props[jars.IS_DIR]: self._update(new_node.path) def start_events(self): """Setup the observer.""" self.get_tree(cached=False) if self.observer is None: self.observer = Observer() stream = Stream(self.fsevent_handler, self.real_root, ids=True) self.observer.schedule(stream) if not self.observer.is_alive(): self.observer.start() def stop_events(self, *args, **kwargs): """Call stop() on the observer.""" self.update() if self.observer is not None: self.observer.stop() def clear_model(self): """Reset the model to only contain one root node.""" self.model = Node(None) def get_tree(self, cached=False): """Return a deep copy of the internal model.""" if cached: return copy.deepcopy(self.model) else: return super().get_tree(cached=False) def update(self): """Update the internal model, by walking the directory structure of the root.""" self.model = self.get_tree() def get_internal_model(self): """Return the current internal model, used in testing.""" return self.model