Exemple #1
0
    async def _resolve_placeholders_in_path(self, path: FsPath, access: Access,
                                            manifest: LocalManifest) -> bool:
        """
        Returns: If an additional sync is needed
        """
        # Notes we sync recursively from children to parents, this is more
        # efficient given otherwise we would have to do:
        # 1) sync the parent
        # 2) sync the child
        # 3) re-sync the parent with the child

        is_placeholder = is_placeholder_manifest(manifest)
        if not is_placeholder:
            # Cannot have a non-placeholder with a placeholder parent, hence
            # we don't have to go any further.
            return manifest.need_sync

        else:
            if is_file_manifest(manifest):
                need_more_sync = await self._minimal_sync_file(
                    path, access, manifest)
            else:
                need_more_sync = await self._minimal_sync_folder(
                    path, access, manifest)

            # Once the entry is synced, we must sync it parent as well to have
            # the entry visible for other clients

            if not path.is_root():
                try:
                    parent_access, parent_manifest = self.local_folder_fs.get_entry(
                        path.parent)
                except FSManifestLocalMiss:
                    # Nothing to do if entry is no present locally
                    return False

                if is_placeholder_manifest(parent_manifest):
                    await self._resolve_placeholders_in_path(
                        path.parent, parent_access, parent_manifest)
                else:
                    await self._sync_folder(path.parent,
                                            parent_access,
                                            parent_manifest,
                                            recursive=False)

            if not need_more_sync:
                self.event_bus.send("fs.entry.synced",
                                    path=str(path),
                                    id=access.id)

            return need_more_sync
    async def _minimal_sync_file(self, path: FsPath, access: Access,
                                 manifest: LocalFileManifest) -> bool:
        """
        Returns: If additional sync are needed
        Raises:
            FileSyncConcurrencyError
            BackendNotAvailable
        """
        if not is_placeholder_manifest(manifest):
            return manifest.need_sync

        need_more_sync = bool(manifest.dirty_blocks)
        # Don't sync the dirty blocks for fast synchronization
        try:
            last_block = manifest.blocks[-1]
            size = last_block.offset + last_block.size
        except IndexError:
            size = 0
        minimal_manifest = manifest.evolve(
            updated=manifest.created if need_more_sync else manifest.updated,
            size=size,
            blocks=manifest.blocks,
            dirty_blocks=(),
        )

        await self._sync_file_actual_sync(path, access, minimal_manifest)

        self.event_bus.send("fs.entry.minimal_synced",
                            path=str(path),
                            id=access.id)
        return need_more_sync
    async def _sync_file_look_for_remote_changes(
            self, path: FsPath, access: Access,
            manifest: LocalFileManifest) -> bool:
        # Placeholder means we need synchro !
        assert not is_placeholder_manifest(manifest)

        # This folder hasn't been modified locally, just download
        # last version from the backend if any.
        target_remote_manifest = await self._backend_vlob_read(access)

        current_manifest = self.local_folder_fs.get_manifest(access)
        if target_remote_manifest.version == current_manifest.base_version:
            return False

        # Remote version has changed...
        if current_manifest.need_sync:
            # ...and modifications occured on our back, now we have a concurrency error !
            self._sync_file_look_resolve_concurrency(path, access,
                                                     current_manifest,
                                                     target_remote_manifest)
        else:
            target_local_manifest = target_remote_manifest.to_local()
            # Otherwise just fast-forward the local data
            self.local_folder_fs.set_manifest(access, target_local_manifest)
        return True
 async def _sync_folder_look_for_remote_changes(
         self, access: Access,
         manifest: LocalFolderManifest) -> Optional[FolderManifest]:
     # Placeholder means we need synchro !
     assert not is_placeholder_manifest(manifest)
     # This folder hasn't been modified locally, just download
     # last version from the backend if any.
     target_remote_manifest = await self._backend_vlob_read(access)
     if target_remote_manifest.version == manifest.base_version:
         return None
     return target_remote_manifest
 def _strip_placeholders(self, children: Dict[Access, LocalManifest]):
     synced_children = {}
     for child_name, child_access in children.items():
         try:
             child_manifest = self.local_folder_fs.get_manifest(
                 child_access)
         except FSManifestLocalMiss:
             # Child not in local, cannot be a placeholder then !
             synced_children[child_name] = child_access
         else:
             if not is_placeholder_manifest(child_manifest):
                 synced_children[child_name] = child_access
     return synced_children
    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)
    async def _sync_file(self, path: FsPath, access: Access,
                         manifest: LocalFileManifest) -> None:
        """
        Raises:
            FileSyncConcurrencyError
            BackendNotAvailable
        """
        assert not is_placeholder_manifest(manifest)
        assert is_file_manifest(manifest)

        # Now we can synchronize the folder if needed
        if not manifest.need_sync:
            changed = await self._sync_file_look_for_remote_changes(
                path, access, manifest)
        else:
            await self._sync_file_actual_sync(path, access, manifest)
            changed = True
        if changed:
            self.event_bus.send("fs.entry.synced",
                                path=str(path),
                                id=access.id)
    async def _minimal_sync_folder(self, path: FsPath, access: Access,
                                   manifest: LocalFolderManifest) -> bool:
        """
        Returns: If additional sync are needed
        Raises:
            FileSyncConcurrencyError
            BackendNotAvailable
        """
        if not is_placeholder_manifest(manifest):
            return manifest.need_sync

        synced_children = self._strip_placeholders(manifest.children)
        need_more_sync = synced_children.keys() != manifest.children.keys()
        manifest = manifest.evolve(children=synced_children)

        target_remote_manifest = await self._sync_folder_actual_sync(
            path, access, manifest)
        self._sync_folder_merge_back(path, access, manifest,
                                     target_remote_manifest)

        self.event_bus.send("fs.entry.minimal_synced",
                            path=str(path),
                            id=access.id)
        return need_more_sync
    async def _sync_file_actual_sync(self, path: FsPath, access: Access,
                                     manifest: LocalFileManifest) -> None:
        assert is_file_manifest(manifest)

        # to_sync_manifest = manifest.to_remote(version=manifest.base_version + 1)
        to_sync_manifest = manifest.to_remote()
        to_sync_manifest = to_sync_manifest.evolve(
            version=manifest.base_version + 1)

        # Compute the file's blocks and upload the new ones
        blocks = []
        sync_map = get_sync_map(manifest, self.block_size)

        # Upload the new blocks
        spaces = sync_map.spaces
        blocks = []

        async def _process_spaces():
            nonlocal blocks
            while spaces:
                cs = spaces.pop()
                data = await self._build_data_from_contiguous_space(cs)
                if not data:
                    # Already existing blocks taken verbatim
                    blocks += [bs.buffer.data for bs in cs.buffers]
                else:
                    # Create a new block from existing data
                    block_access = BlockAccess.from_block(data, cs.start)
                    await self._backend_block_create(block_access, data)
                    blocks.append(block_access)

        if len(spaces) < 2:
            await _process_spaces()

        else:
            async with trio.open_nursery() as nursery:
                nursery.start_soon(_process_spaces)
                nursery.start_soon(_process_spaces)
                nursery.start_soon(_process_spaces)
                nursery.start_soon(_process_spaces)

        to_sync_manifest = to_sync_manifest.evolve(
            blocks=blocks,
            size=sync_map.size  # TODO: useful ?
        )

        # Upload the file manifest as new vlob version
        notify_beacons = self.local_folder_fs.get_beacon(path)
        try:
            if is_placeholder_manifest(manifest):
                await self._backend_vlob_create(access, to_sync_manifest,
                                                notify_beacons)
            else:
                await self._backend_vlob_update(access, to_sync_manifest,
                                                notify_beacons)

        except SyncConcurrencyError:
            # Placeholder don't have remote version, so concurrency shouldn't
            # be possible. However it's possible a previous attempt of
            # uploading this manifest succeeded but we didn't receive the
            # backend's answer, hence wrongly believing this is still a
            # placeholder.
            if is_placeholder_manifest(manifest):
                logger.warning("Concurrency error while creating vlob",
                               access_id=access.id)

            target_remote_manifest = await self._backend_vlob_read(access)

            current_manifest = self.local_folder_fs.get_manifest(access)
            # Do a fast-forward to avoid losing block we have uploaded
            diverged_manifest = fast_forward_file(manifest, current_manifest,
                                                  to_sync_manifest)

            self._sync_file_look_resolve_concurrency(path, access,
                                                     diverged_manifest,
                                                     target_remote_manifest)

            # # TODO
            # target_local_manifest = remote_to_local_manifest(target_remote_manifest)
            # self._sync_file_look_resolve_concurrency(
            #     path, access, current_manifest, target_local_manifest
            # )
            # await self._backend_vlob_create(access, to_sync_manifest, notify_beacons)
        else:
            self._sync_file_merge_back(access, manifest, to_sync_manifest)

        return to_sync_manifest
    async def _sync_folder_actual_sync(
            self, path: FsPath, access: Access,
            manifest: LocalFolderManifest) -> FolderManifest:
        # to_sync_manifest = manifest.to_remote(version=manifest.base_version + 1)
        to_sync_manifest = manifest.to_remote()
        to_sync_manifest = to_sync_manifest.evolve(
            version=manifest.base_version + 1)

        # Upload the folder manifest as new vlob version
        notify_beacons = self.local_folder_fs.get_beacon(path)
        force_update = False
        while True:
            try:
                if is_placeholder_manifest(manifest) and not force_update:
                    await self._backend_vlob_create(access, to_sync_manifest,
                                                    notify_beacons)
                else:
                    await self._backend_vlob_update(access, to_sync_manifest,
                                                    notify_beacons)
                break

            except SyncConcurrencyError:
                if is_placeholder_manifest(manifest):
                    # Placeholder don't have remote version, so concurrency shouldn't
                    # be possible. However special cases exist:
                    # - user manifest has it access is shared between devices
                    #   even if it is not yet synced.
                    # - it's possible a previous attempt of uploading this
                    #   manifest succeeded but we didn't receive the backend's
                    #   answer, hence wrongly believing this is still a placeholder.
                    # If such cases occured, we just have to pretend we were
                    # trying to do an update and rely on the generic merge.

                    if is_user_manifest(manifest):
                        logger.warning(
                            "Concurrency error while creating user vlob",
                            access_id=access.id)
                    else:
                        logger.warning("Concurrency error while creating vlob",
                                       access_id=access.id)

                    base = None
                    force_update = True
                else:
                    base = await self._backend_vlob_read(
                        access, to_sync_manifest.version - 1)

                # Do a 3-ways merge to fix the concurrency error, first we must
                # fetch the base version and the new one present in the backend
                # TODO: base should be available locally
                target = await self._backend_vlob_read(access)

                # 3-ways merge between base, modified and target versions
                to_sync_manifest, sync_needed, conflicts = merge_remote_folder_manifests(
                    base, to_sync_manifest, target)
                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,
                    )
                if not sync_needed:
                    # It maybe possible the changes that cause the concurrency
                    # error were the same than the one we wanted to make in the
                    # first place (e.g. when removing the same file)
                    break
                to_sync_manifest = to_sync_manifest.evolve(
                    version=target.version + 1)

        return to_sync_manifest