Example #1
0
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