예제 #1
0
    def _sync_folder_merge_back(
        self,
        path: FsPath,
        access: Access,
        base_manifest: LocalFolderManifest,
        target_remote_manifest: FolderManifest,
    ) -> None:
        # Merge with the current version of the manifest which may have
        # been modified in the meantime
        assert is_folderish_manifest(target_remote_manifest)
        current_manifest = self.local_folder_fs.get_manifest(access)
        assert is_folderish_manifest(current_manifest)

        target_manifest = target_remote_manifest.to_local()
        final_manifest, conflicts = merge_local_folder_manifests(
            base_manifest, current_manifest, target_manifest)
        for original_name, original_id, diverged_name, diverged_id in conflicts:
            self.event_bus.send(
                "fs.entry.name_conflicted",
                path=str(path / original_name),
                diverged_path=str(path / diverged_name),
                original_id=original_id,
                diverged_id=diverged_id,
            )
        self.local_folder_fs.set_manifest(access, final_manifest)
예제 #2
0
    def _delete(self, path: FsPath, expect=None) -> None:
        if path.is_root():
            raise PermissionError(13, "Permission denied", str(path))
        parent_access, parent_manifest = self._retrieve_entry(path.parent)
        if not is_folderish_manifest(parent_manifest):
            raise NotADirectoryError(20, "Not a directory", str(path.parent))

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

        item_manifest = self.get_manifest(item_access)
        if is_folderish_manifest(item_manifest):
            if expect == "file":
                raise IsADirectoryError(21, "Is a directory", str(path))
            if item_manifest.children:
                raise OSError(39, "Directory not empty", str(path))
        elif expect == "folder":
            raise NotADirectoryError(20, "Not a directory", str(path))

        parent_manifest = parent_manifest.evolve_children_and_mark_updated(
            {path.name: None})
        self.set_manifest(parent_access, parent_manifest)
        self.event_bus.send("fs.entry.updated", id=parent_access.id)
예제 #3
0
    async def _sync_folder(self, path: FsPath, access: Access,
                           manifest: LocalFolderManifest,
                           recursive: bool) -> None:
        assert not is_placeholder_manifest(manifest)
        assert is_folderish_manifest(manifest)

        # Synchronizing a folder is divided into three steps:
        # - first synchronizing it children
        # - then sychronize itself
        # - finally merge the synchronized version with the current one (that
        #   may have been updated in the meantime)

        # Synchronizing children
        if recursive:
            for child_name, child_access in sorted(manifest.children.items(),
                                                   key=lambda x: x[0]):
                child_path = path / child_name
                try:
                    await self._sync_nolock(child_path, True)
                except FileNotFoundError:
                    # Concurrent deletion occured, just ignore this child
                    pass

            # The trick here is to retreive the current version of the manifest
            # and remove it placeholders (those are the children created since
            # the start of our sync)
            manifest = self.local_folder_fs.get_manifest(access)
            assert is_folderish_manifest(manifest)

        manifest = manifest.evolve(
            children=self._strip_placeholders(manifest.children))

        # Now we can synchronize the folder if needed
        if not manifest.need_sync:
            target_remote_manifest = await self._sync_folder_look_for_remote_changes(
                access, manifest)
            # Quick exit if nothing's new
            if not target_remote_manifest:
                return
            event_type = "fs.entry.remote_changed"
        else:
            target_remote_manifest = await self._sync_folder_actual_sync(
                path, access, manifest)
            event_type = "fs.entry.synced"
        assert is_folderish_manifest(target_remote_manifest)

        # Merge the synchronized version with the current one
        self._sync_folder_merge_back(path, access, manifest,
                                     target_remote_manifest)

        self.event_bus.send(event_type, path=str(path), id=access.id)
예제 #4
0
    async def folder_delete(self, path: FsPath) -> EntryID:
        # Check write rights
        self.check_write_rights(path)

        # Fetch and lock
        async with self._lock_parent_manifest_from_path(path) as (parent,
                                                                  child):

            # Entry doesn't exist
            if child is None:
                raise FSFileNotFoundError(filename=path)

            # Not a directory
            if not is_folderish_manifest(child):
                raise FSNotADirectoryError(filename=path)

            # Directory not empty
            if child.children:
                raise FSDirectoryNotEmptyError(filename=path)

            # Create new manifest
            new_parent = parent.evolve_children_and_mark_updated(
                {path.name: None})

            # Atomic change
            await self.local_storage.set_manifest(parent.id, new_parent)

        # Send event
        self._send_event("fs.entry.updated", id=parent.id)

        # Return the entry id of the removed folder
        return child.id
예제 #5
0
    async def _lock_parent_manifest_from_path(
            self, path: FsPath) -> Tuple[LocalManifest, LocalManifest]:
        # This is the most complicated locking scenario.
        # It requires locking the parent of the given entry and the entry itself
        # if it exists.

        # This is done in a two step process:
        # - 1. Lock the parent (it must exist). While the parent is locked, no
        #   children can be added, renamed or removed.
        # - 2. Lock the children if exists. It it doesn't, there is nothing to lock
        #   since the parent lock guarentees that it is not going to be added while
        #   using the context.

        # This double locking is only required for a single use case: the overwriting
        # of empty directory during a move. We have to make sure that no one adds
        # something to the directory while it is being overwritten.
        # If read/write locks were to be implemented, the parent would be write locked
        # and the child read locked. This means that despite locking two entries, only
        # a single entry is modified at a time.

        # Source is root
        if path.is_root():
            raise FSPermissionError(filename=str(path))

        # Loop over attempts
        while True:

            # Lock parent first
            async with self._lock_manifest_from_path(path.parent) as parent:

                # Parent is not a directory
                if not is_folderish_manifest(parent):
                    raise FSNotADirectoryError(filename=path.parent)

                # Child doesn't exist
                if path.name not in parent.children:
                    yield parent, None
                    return

                # Child exists
                entry_id = parent.children[path.name]
                try:
                    async with self.local_storage.lock_manifest(
                            entry_id) as manifest:
                        yield parent, manifest
                        return

                # Child is not available
                except FSLocalMissError as exc:
                    assert exc.id == entry_id

            # Release the lock and download the child manifest
            await self._load_manifest(entry_id)
예제 #6
0
        def _recursive_dump(access: Access):
            dump_data = {"access": attr.asdict(access)}
            try:
                manifest = self.get_manifest(access)
            except FSManifestLocalMiss:
                return dump_data
            dump_data.update(attr.asdict(manifest))
            if is_folderish_manifest(manifest):
                for child_name, child_access in manifest.children.items():
                    dump_data["children"][child_name] = _recursive_dump(
                        child_access)

            return dump_data
예제 #7
0
        def _recursive_search(access, path):
            try:
                manifest = self._get_manifest_read_only(access)
            except FSManifestLocalMiss:
                return
            if access.id == entry_id:
                return path, access, manifest

            if is_folderish_manifest(manifest):
                for child_name, child_access in manifest.children.items():
                    found = _recursive_search(child_access, path / child_name)
                    if found:
                        return found
예제 #8
0
        def _recursive_get_local_entries_ids(access):
            try:

                manifest = self.local_folder_fs.get_manifest(access)
            except FSManifestLocalMiss:
                # TODO: make the assert true...
                # Root should always be loaded
                assert access is not self.device.user_manifest_access
                return

            if is_folderish_manifest(manifest):
                for child_access in manifest.children.values():
                    _recursive_get_local_entries_ids(child_access)

            entries.append({
                "id": access.id,
                "rts": access.rts,
                "version": manifest.base_version
            })
예제 #9
0
    def _retrieve_entry_read_only(
            self,
            path: FsPath,
            collector=None) -> Tuple[Access, LocalManifest]:
        curr_access = self.root_access
        curr_manifest = self._get_manifest_read_only(curr_access)
        if collector:
            collector(curr_access, curr_manifest)

        try:
            _, *hops, dest = list(path.walk_to_path())
        except ValueError:
            return curr_access, curr_manifest

        for hop in hops:
            try:
                curr_access = curr_manifest.children[hop.name]
            except KeyError:
                raise FileNotFoundError(2, "No such file or directory",
                                        str(hop))

            curr_manifest = self._get_manifest_read_only(curr_access)

            if not is_folderish_manifest(curr_manifest):
                raise NotADirectoryError(20, "Not a directory", str(hop))
            if collector:
                collector(curr_access, curr_manifest)

        try:
            curr_access = curr_manifest.children[dest.name]
        except KeyError:
            raise FileNotFoundError(2, "No such file or directory", str(dest))

        curr_manifest = self._get_manifest_read_only(curr_access)

        if collector:
            collector(curr_access, curr_manifest)

        return curr_access, curr_manifest
예제 #10
0
    def touch(self, path: FsPath) -> None:
        if path.is_root():
            raise FileExistsError(17, "File exists", str(path))

        if path.parent.is_root():
            raise PermissionError(
                13, "Permission denied (only workpace allowed at root level)",
                str(path))

        access, manifest = self._retrieve_entry(path.parent)
        if not is_folderish_manifest(manifest):
            raise NotADirectoryError(20, "Not a directory", str(path.parent))
        if path.name in manifest.children:
            raise FileExistsError(17, "File exists", str(path))

        child_access = ManifestAccess()
        child_manifest = LocalFileManifest(self.local_author)
        manifest = manifest.evolve_children_and_mark_updated(
            {path.name: child_access})
        self.set_manifest(access, manifest)
        self.set_manifest(child_access, child_manifest)
        self.event_bus.send("fs.entry.updated", id=access.id)
        self.event_bus.send("fs.entry.updated", id=child_access.id)
예제 #11
0
        def _recursive_create_copy_map(access, manifest):
            copy_map = {"access": access, "manifest": manifest}
            manifests_miss = []

            if is_folderish_manifest(manifest):
                copy_map["children"] = {}

                for child_name, child_access in manifest.children.items():
                    try:
                        child_manifest = self._get_manifest_read_only(
                            child_access)
                    except FSManifestLocalMiss as exc:
                        manifests_miss.append(exc.access)
                    else:
                        try:
                            copy_map["children"][
                                child_name] = _recursive_create_copy_map(
                                    child_access, child_manifest)
                        except FSMultiManifestLocalMiss as exc:
                            manifests_miss += exc.accesses

            if manifests_miss:
                raise FSMultiManifestLocalMiss(manifests_miss)
            return copy_map
예제 #12
0
    async def _sync_by_id(self,
                          entry_id: EntryID,
                          remote_changed: bool = True) -> RemoteManifest:
        """
        Synchronize the entry corresponding to a specific ID.

        This method keeps performing synchronization steps on the given ID until one of
        those two conditions is met:
        - there is no more changes to upload
        - one upload operation has succeeded and has been acknowledged

        This guarantees that any change prior to the call is saved remotely when this
        method returns.
        """
        # Get the current remote manifest if it has changed
        remote_manifest = None
        if remote_changed:
            try:
                remote_manifest = await self.remote_loader.load_manifest(
                    entry_id)
            except FSRemoteManifestNotFound:
                pass

        # Loop over sync transactions
        final = False
        while True:

            # Protect against race conditions on the entry id
            try:

                # Perform the sync step transaction
                try:
                    new_remote_manifest = await self.transactions.synchronization_step(
                        entry_id, remote_manifest, final)

                # The entry first requires reshaping
                except FSReshapingRequiredError:
                    await self.transactions.file_reshape(entry_id)
                    continue

            # The manifest doesn't exist locally
            except FSLocalMissError:
                raise FSNoSynchronizationRequired(entry_id)

            # No new manifest to upload, the entry is synced!
            if new_remote_manifest is None:
                return remote_manifest or (
                    await self.local_storage.get_manifest(entry_id)).base

            # Synchronize placeholder children
            if is_folderish_manifest(new_remote_manifest):
                await self._synchronize_placeholders(new_remote_manifest)

            # Upload blocks
            if is_file_manifest(new_remote_manifest):
                await self._upload_blocks(new_remote_manifest)

            # Restamp the remote manifest
            new_remote_manifest = new_remote_manifest.evolve(
                timestamp=pendulum_now())

            # Upload the new manifest containing the latest changes
            try:
                await self.remote_loader.upload_manifest(
                    entry_id, new_remote_manifest)

            # The upload has failed: download the latest remote manifest
            except FSRemoteSyncError:
                remote_manifest = await self.remote_loader.load_manifest(
                    entry_id)

            # The upload has succeeded: loop one last time to acknowledge this new version
            else:
                final = True
                remote_manifest = new_remote_manifest
예제 #13
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)