Example #1
0
        def _recursive_process_copy_map(copy_map):
            manifest = copy_map["manifest"]

            cpy_access = ManifestAccess()
            if is_file_manifest(manifest):
                cpy_manifest = LocalFileManifest(
                    author=self.local_author,
                    size=manifest.size,
                    blocks=manifest.blocks,
                    dirty_blocks=manifest.dirty_blocks,
                )

            else:
                cpy_children = {}
                for child_name in manifest.children.keys():
                    child_copy_map = copy_map["children"][child_name]
                    new_child_access = _recursive_process_copy_map(
                        child_copy_map)
                    cpy_children[child_name] = new_child_access

                if is_folder_manifest(manifest):
                    cpy_manifest = LocalFolderManifest(
                        author=self.local_author, children=cpy_children)
                else:
                    assert is_workspace_manifest(manifest)
                    cpy_manifest = LocalWorkspaceManifest(
                        self.local_author, children=cpy_children)

            self.set_manifest(cpy_access, cpy_manifest)
            return cpy_access
Example #2
0
    def workspace_rename(self, src: FsPath, dst: FsPath) -> None:
        """
        Workspace is the only manifest that is allowed to be moved without
        changing it access.
        The reason behind this is changing a workspace's access means creating
        a completely new workspace... And on the other hand the name of a
        workspace is not globally unique so a given user should be able to
        change it.
        """
        src_access, src_manifest = self._retrieve_entry_read_only(src)
        if not is_workspace_manifest(src_manifest):
            raise PermissionError(13, "Permission denied (not a workspace)",
                                  str(src), str(dst))
        if not dst.parent.is_root():
            raise PermissionError(
                13, "Permission denied (workspace must be direct root child)",
                str(src), str(dst))

        root_manifest = self.get_user_manifest()
        if dst.name in root_manifest.children:
            raise FileExistsError(17, "File exists", str(dst))

        # Just move the workspace's access from one place to another
        root_manifest = root_manifest.evolve_children_and_mark_updated({
            dst.name:
            root_manifest.children[src.name],
            src.name:
            None
        })
        self.set_manifest(self.root_access, root_manifest)

        self.event_bus.send("fs.entry.updated", id=self.root_access.id)
Example #3
0
    def get_beacon(self, path: FsPath) -> UUID:
        # The beacon is used to notify other clients that we modified an entry.
        # We try to use the id of workspace containing the modification as
        # beacon. This is not possible when directly modifying the user
        # manifest in which case we use the user manifest id as beacon.
        try:
            _, workspace_name, *_ = path.parts
        except ValueError:
            return self.root_access.id

        access, manifest = self._retrieve_entry_read_only(
            FsPath(f"/{workspace_name}"))
        assert is_workspace_manifest(manifest)
        return access.id
    async def get_path_at_timestamp(self, entry_id: EntryID,
                                    timestamp: Pendulum) -> FsPath:
        """
        Find a path for an entry_id at a specific timestamp.

        If the path is broken, will raise an EntryNotFound exception. All the other exceptions are
        thrown by the ManifestCache.

        Raises:
            FSError
            FSBackendOfflineError
            FSWorkspaceInMaintenance
            FSBadEncryptionRevision
            FSWorkspaceNoAccess
            EntryNotFound
        """
        # Get first manifest
        try:
            current_id = entry_id
            current_manifest, _ = await self.load(current_id,
                                                  timestamp=timestamp)
        except FSRemoteManifestNotFound:
            raise EntryNotFound(entry_id)

        # Loop over parts
        parts = []
        while not is_workspace_manifest(current_manifest):

            # Get the manifest
            try:
                parent_manifest, _ = await self.load(current_manifest.parent,
                                                     timestamp=timestamp)
            except FSRemoteManifestNotFound:
                raise EntryNotFound(entry_id)

            # Find the child name
            for name, child_id in parent_manifest.children.items():
                if child_id == current_id:
                    parts.append(name)
                    break
            else:
                raise EntryNotFound(entry_id)

            # Continue until root is found
            current_id = current_manifest.parent
            current_manifest = parent_manifest

        # Return the path
        return FsPath("/" + "/".join(reversed(parts)))
Example #5
0
 def get_local_beacons(self) -> List[UUID]:
     # beacon_id is either the id of the user manifest or of a workpace manifest
     beacons = [self.root_access.id]
     try:
         root_manifest = self._get_manifest_read_only(self.root_access)
         # Currently workspace can only direct children of the user manifest
         for child_access in root_manifest.children.values():
             try:
                 child_manifest = self._get_manifest_read_only(child_access)
             except FSManifestLocalMiss:
                 continue
             if is_workspace_manifest(child_manifest):
                 beacons.append(child_access.id)
     except FSManifestLocalMiss:
         raise AssertionError(
             "root manifest should always be available in local !")
     return beacons
Example #6
0
    def stat(self, path: FsPath) -> dict:
        access, manifest = self._retrieve_entry_read_only(path)
        if is_file_manifest(manifest):
            return {
                "type": "file",
                "is_folder": False,
                "created": manifest.created,
                "updated": manifest.updated,
                "base_version": manifest.base_version,
                "is_placeholder": manifest.is_placeholder,
                "need_sync": manifest.need_sync,
                "size": manifest.size,
            }

        elif is_workspace_manifest(manifest):
            return {
                "type": "workspace",
                "is_folder": True,
                "created": manifest.created,
                "updated": manifest.updated,
                "base_version": manifest.base_version,
                "is_placeholder": manifest.is_placeholder,
                "need_sync": manifest.need_sync,
                "children": list(sorted(manifest.children.keys())),
                "creator": manifest.creator,
                "participants": list(manifest.participants),
            }
        else:
            return {
                "type": "root" if path.is_root() else "folder",
                "is_folder": True,
                "created": manifest.created,
                "updated": manifest.updated,
                "base_version": manifest.base_version,
                "is_placeholder": manifest.is_placeholder,
                "need_sync": manifest.need_sync,
                "children": list(sorted(manifest.children.keys())),
            }
Example #7
0
 def _get_manifest_read_only(self, access: Access) -> LocalManifest:
     try:
         return self._manifests_cache[access.id]
     except KeyError:
         pass
     try:
         raw = self._local_db.get(access)
     except LocalDBMissingEntry as exc:
         # Last chance: if we are looking for the user manifest, we can
         # fake to know it version 0, which is useful during boostrap step
         if access == self.root_access:
             manifest = LocalUserManifest(self.local_author)
         else:
             raise FSManifestLocalMiss(access) from exc
     else:
         manifest = local_manifest_serializer.loads(raw)
     self._manifests_cache[access.id] = manifest
     # TODO: shouldn't be processed in multiple places like this...
     if is_workspace_manifest(manifest):
         path, *_ = self.get_entry_path(access.id)
         self.event_bus.send("fs.workspace.loaded",
                             path=str(path),
                             id=access.id)
     return manifest
Example #8
0
    async def share(self, path: FsPath, recipient: UserID):
        """
        Raises:
            SharingError
            SharingBackendMessageError
            SharingRecipientError
            SharingNotAWorkspace
            FileNotFoundError
            FSManifestLocalMiss: If path is not available in local
            ValueError: If path is not a valid absolute path
        """
        if self.device.user_id == recipient:
            raise SharingRecipientError("Cannot share to oneself.")

        # First retreive the manifest and make sure it is a workspace
        access, manifest = self.local_folder_fs.get_entry(path)
        if not is_workspace_manifest(manifest):
            raise SharingNotAWorkspace(
                f"`{path}` is not a workspace, hence cannot be shared")

        # We should keep up to date the participants list in the manifest.
        # Note this is not done in a strictly atomic way so this information
        # can be erronous (consider it more of a UX helper than something to
        # rely on)
        if recipient not in manifest.participants:
            participants = sorted({*manifest.participants, recipient})
            manifest = manifest.evolve_and_mark_updated(
                participants=participants)
            self.local_folder_fs.update_manifest(access, manifest)

        # Make sure there is no placeholder in the path and the entry
        # is up to date
        await self.syncer.sync(path, recursive=False)

        # Now we can build the sharing message...
        msg = {
            "type": "share",
            "author": self.device.device_id,
            "access": access,
            "name": path.name,
        }
        try:
            raw = sharing_message_content_serializer.dumps(msg)
        except SerdeError as exc:
            # TODO: Do we really want to log the message content ? Wouldn't
            # it be better just to raise a RuntimeError given we should never
            # be in this case ?
            logger.error("Cannot dump sharing message",
                         msg=msg,
                         errors=exc.errors)
            raise SharingError("Internal error") from exc

        try:
            ciphered = await self.encryption_manager.encrypt_for(
                recipient, raw)
        except EncryptionManagerError as exc:
            raise SharingRecipientError(
                f"Cannot create message for `{recipient}`") from exc

        # ...And finally send the message
        try:
            await self.backend_cmds.message_send(recipient=recipient,
                                                 body=ciphered)
        except BackendCmdsBadResponse as exc:
            raise SharingBackendMessageError(
                f"Error while trying to send sharing message to backend: {exc}"
            ) from exc
Example #9
0
    def _copy(self, src: FsPath, dst: FsPath, delete_src: bool) -> None:
        # The idea here is to consider a manifest never move around the fs
        # (i.e. a given access always points to the same path). This simplify
        # sync notifications handling and avoid ending up with two path
        # (possibly from different workspaces !) pointing to the same manifest
        # which would be weird for user experience.
        # Long story short, when moving/copying manifest, we must recursively
        # copy the manifests and create new accesses for them.

        parent_src = src.parent
        parent_dst = dst.parent

        # No matter what, cannot move or overwrite root
        if src.is_root():
            # Raise FileNotFoundError if parent_dst doesn't exists
            _, parent_dst_manifest = self._retrieve_entry(parent_dst)
            if not is_folderish_manifest(parent_dst_manifest):
                raise NotADirectoryError(20, "Not a directory",
                                         str(parent_dst))
            else:
                raise PermissionError(13, "Permission denied", str(src),
                                      str(dst))
        elif dst.is_root():
            # Raise FileNotFoundError if parent_src doesn't exists
            _, parent_src_manifest = self._retrieve_entry(src.parent)
            if not is_folderish_manifest(parent_src_manifest):
                raise NotADirectoryError(20, "Not a directory",
                                         str(src.parent))
            else:
                raise PermissionError(13, "Permission denied", str(src),
                                      str(dst))

        if src == dst:
            # Raise FileNotFoundError if doesn't exist
            src_access, src_ro_manifest = self._retrieve_entry_read_only(src)
            if is_workspace_manifest(src_ro_manifest):
                raise PermissionError(
                    13,
                    "Permission denied (cannot move/copy workpace, must rename it)",
                    str(src),
                    str(dst),
                )
            return

        if parent_src == parent_dst:
            parent_access, parent_manifest = self._retrieve_entry(parent_src)
            if not is_folderish_manifest(parent_manifest):
                raise NotADirectoryError(20, "Not a directory",
                                         str(parent_src))

            try:
                dst.relative_to(src)
            except ValueError:
                pass
            else:
                raise OSError(22, "Invalid argument", str(src), None, str(dst))

            try:
                src_access = parent_manifest.children[src.name]
            except KeyError:
                raise FileNotFoundError(2, "No such file or directory",
                                        str(src))

            existing_dst_access = parent_manifest.children.get(dst.name)
            src_manifest = self.get_manifest(src_access)

            if is_workspace_manifest(src_manifest):
                raise PermissionError(
                    13,
                    "Permission denied (cannot move/copy workpace, must rename it)",
                    str(src),
                    str(dst),
                )

            if existing_dst_access:
                existing_dst_manifest = self.get_manifest(existing_dst_access)
                if is_folderish_manifest(src_manifest):
                    if is_file_manifest(existing_dst_manifest):
                        raise NotADirectoryError(20, "Not a directory",
                                                 str(dst))
                    elif existing_dst_manifest.children:
                        raise OSError(39, "Directory not empty", str(dst))
                else:
                    if is_folderish_manifest(existing_dst_manifest):
                        raise IsADirectoryError(21, "Is a directory", str(dst))

            moved_access = self._recursive_manifest_copy(
                src_access, src_manifest)

            if not delete_src:
                parent_manifest = parent_manifest.evolve_children(
                    {dst.name: moved_access})
            else:
                parent_manifest = parent_manifest.evolve_children({
                    dst.name:
                    moved_access,
                    src.name:
                    None
                })
            self.set_manifest(parent_access, parent_manifest)
            self.event_bus.send("fs.entry.updated", id=parent_access.id)

        else:
            parent_src_access, parent_src_manifest = self._retrieve_entry(
                parent_src)
            if not is_folderish_manifest(parent_src_manifest):
                raise NotADirectoryError(20, "Not a directory",
                                         str(parent_src))

            parent_dst_access, parent_dst_manifest = self._retrieve_entry(
                parent_dst)
            if not is_folderish_manifest(parent_dst_manifest):
                raise NotADirectoryError(20, "Not a directory",
                                         str(parent_dst))

            try:
                dst.relative_to(src)
            except ValueError:
                pass
            else:
                raise OSError(22, "Invalid argument", str(src), None, str(dst))

            try:
                src_access = parent_src_manifest.children[src.name]
            except KeyError:
                raise FileNotFoundError(2, "No such file or directory",
                                        str(src))

            existing_dst_access = parent_dst_manifest.children.get(dst.name)
            src_manifest = self.get_manifest(src_access)

            if is_workspace_manifest(src_manifest):
                raise PermissionError(
                    13,
                    "Permission denied (cannot move/copy workpace, must rename it)",
                    str(src),
                    str(dst),
                )

            if existing_dst_access:
                existing_entry_manifest = self.get_manifest(
                    existing_dst_access)
                if is_folderish_manifest(src_manifest):
                    if is_file_manifest(existing_entry_manifest):
                        raise NotADirectoryError(20, "Not a directory",
                                                 str(dst))
                    elif existing_entry_manifest.children:
                        raise OSError(39, "Directory not empty", str(dst))
                else:
                    if is_folderish_manifest(existing_entry_manifest):
                        raise IsADirectoryError(21, "Is a directory", str(dst))

            moved_access = self._recursive_manifest_copy(
                src_access, src_manifest)

            parent_dst_manifest = parent_dst_manifest.evolve_children_and_mark_updated(
                {dst.name: moved_access})
            self.set_manifest(parent_dst_access, parent_dst_manifest)
            self.event_bus.send("fs.entry.updated", id=parent_dst_access.id)

            if delete_src:
                parent_src_manifest = parent_src_manifest.evolve_children_and_mark_updated(
                    {src.name: None})
                self.set_manifest(parent_src_access, parent_src_manifest)
                self.event_bus.send("fs.entry.updated",
                                    id=parent_src_access.id)