Example #1
0
def merge_manifests(
    local_author: DeviceID,
    prevent_sync_pattern: Pattern[str],
    local_manifest: BaseLocalManifest,
    remote_manifest: Optional[BaseRemoteManifest] = None,
    force_apply_pattern: Optional[bool] = False,
) -> BaseLocalManifest:
    # Start by re-applying pattern (idempotent)
    if force_apply_pattern and isinstance(
            local_manifest, (LocalFolderManifest, LocalWorkspaceManifest)):
        local_manifest = local_manifest.apply_prevent_sync_pattern(
            prevent_sync_pattern)

    # The remote hasn't changed
    if remote_manifest is None or remote_manifest.version <= local_manifest.base_version:
        return local_manifest
    assert remote_manifest is not None

    # Extract versions
    remote_version = remote_manifest.version
    local_version = local_manifest.base_version
    local_from_remote = BaseLocalManifest.from_remote_with_local_context(
        remote_manifest, prevent_sync_pattern, local_manifest)

    # Only the remote has changed
    if not local_manifest.need_sync:
        return local_from_remote

    # Both the remote and the local have changed
    assert remote_version > local_version and local_manifest.need_sync

    # All the local changes have been successfully uploaded
    if local_manifest.match_remote(remote_manifest):
        return local_from_remote

    # The remote changes are ours, simply acknowledge them and keep our local changes
    if remote_manifest.author == local_author:
        return local_manifest.evolve(base=remote_manifest)

    # The remote has been updated by some other device
    assert remote_manifest.author != local_author

    # Cannot solve a file conflict directly
    if not isinstance(local_manifest,
                      (LocalFolderManifest, LocalWorkspaceManifest)):
        raise FSFileConflictError(local_manifest, remote_manifest)

    # Solve the folder conflict
    new_children = merge_folder_children(
        local_manifest.base.children,
        local_manifest.children,
        local_from_remote.children,
        remote_manifest.author,
    )

    # Mark as updated
    return local_from_remote.evolve_and_mark_updated(children=new_children)
Example #2
0
    async def get_manifest(self, entry_id: EntryID) -> BaseLocalManifest:
        """
        Raises:
            FSLocalMissError
        """
        # Look in cache first
        try:
            return self._cache[entry_id]
        except KeyError:
            pass

        # Look into the database
        async with self._open_cursor() as cursor:
            cursor.execute("SELECT blob FROM vlobs WHERE vlob_id = ?",
                           (entry_id.bytes, ))
            manifest_row = cursor.fetchone()

        # Not found
        if not manifest_row:
            raise FSLocalMissError(entry_id)

        # Safely fill the cache
        if entry_id not in self._cache:
            self._cache[entry_id] = BaseLocalManifest.decrypt_and_load(
                manifest_row[0], key=self.device.local_symkey)

        # Always return the cached value
        return self._cache[entry_id]
 async def _get_manifest(self, entry_id: EntryID) -> BaseLocalManifest:
     try:
         return await self.local_storage.get_manifest(entry_id)
     except FSLocalMissError as exc:
         remote_manifest = await self.remote_loader.load_manifest(cast(EntryID, exc.id))
         return BaseLocalManifest.from_remote(
             remote_manifest, prevent_sync_pattern=self.local_storage.get_prevent_sync_pattern()
         )
 async def _load_and_lock_manifest(self, entry_id: EntryID) -> AsyncIterator[BaseLocalManifest]:
     async with self.local_storage.lock_entry_id(entry_id):
         try:
             local_manifest = await self.local_storage.get_manifest(entry_id)
         except FSLocalMissError as exc:
             remote_manifest = await self.remote_loader.load_manifest(cast(EntryID, exc.id))
             local_manifest = BaseLocalManifest.from_remote(
                 remote_manifest,
                 prevent_sync_pattern=self.local_storage.get_prevent_sync_pattern(),
             )
             await self.local_storage.set_manifest(entry_id, local_manifest)
         yield local_manifest
Example #5
0
    async def file_conflict(
        self,
        entry_id: EntryID,
        local_manifest: Union[LocalFolderManifest, LocalFileManifest],
        remote_manifest: BaseRemoteManifest,
    ) -> None:
        # This is the only transaction that affects more than one manifests
        # That's because the local version of the file has to be registered in the
        # parent as a new child while the remote version has to be set as the actual
        # version. In practice, this should not be an issue.

        # Lock parent then child
        parent_id = local_manifest.parent
        async with self.local_storage.lock_manifest(
                parent_id) as parent_manifest:

            # Not a folderish manifest
            if not isinstance(parent_manifest,
                              (LocalFolderManifest, LocalWorkspaceManifest)):
                raise FSNotADirectoryError(parent_id)

            async with self.local_storage.lock_manifest(
                    entry_id) as current_manifest:

                # Not a file manifest
                if not isinstance(current_manifest, LocalFileManifest):
                    raise FSIsADirectoryError(entry_id)

                # Make sure the file still exists
                filename = get_filename(parent_manifest, entry_id)
                if filename is None:
                    return

                # Copy blocks
                new_blocks = []
                for chunks in current_manifest.blocks:
                    new_chunks = []
                    for chunk in chunks:
                        data = await self.local_storage.get_chunk(chunk.id)
                        new_chunk = Chunk.new(chunk.start, chunk.stop)
                        await self.local_storage.set_chunk(new_chunk.id, data)
                        if len(chunks) == 1:
                            new_chunk = new_chunk.evolve_as_block(data)
                        new_chunks.append(chunk)
                    new_blocks.append(tuple(new_chunks))

                # Prepare
                prevent_sync_pattern = self.local_storage.get_prevent_sync_pattern(
                )
                new_name = get_conflict_filename(
                    filename, list(parent_manifest.children),
                    remote_manifest.author)
                new_manifest = LocalFileManifest.new_placeholder(
                    self.local_author,
                    parent=parent_id).evolve(size=current_manifest.size,
                                             blocks=tuple(new_blocks))
                new_parent_manifest = parent_manifest.evolve_children_and_mark_updated(
                    {new_name: new_manifest.id},
                    prevent_sync_pattern=prevent_sync_pattern)
                other_manifest = BaseLocalManifest.from_remote(
                    remote_manifest, prevent_sync_pattern=prevent_sync_pattern)

                # Set manifests
                await self.local_storage.set_manifest(new_manifest.id,
                                                      new_manifest,
                                                      check_lock_status=False)
                await self.local_storage.set_manifest(parent_id,
                                                      new_parent_manifest)
                await self.local_storage.set_manifest(entry_id, other_manifest)

                self._send_event(CoreEvent.FS_ENTRY_UPDATED,
                                 id=new_manifest.id)
                self._send_event(CoreEvent.FS_ENTRY_UPDATED, id=parent_id)
                self._send_event(
                    CoreEvent.FS_ENTRY_FILE_CONFLICT_RESOLVED,
                    id=entry_id,
                    backup_id=new_manifest.id,
                )
Example #6
0
def merge_manifests(
    local_author: DeviceID,
    timestamp: DateTime,
    prevent_sync_pattern: Pattern[str],
    local_manifest: BaseLocalManifest,
    remote_manifest: Optional[BaseRemoteManifest] = None,
    force_apply_pattern: Optional[bool] = False,
    preferred_language: str = "en",
) -> BaseLocalManifest:
    # Start by re-applying pattern (idempotent)
    if force_apply_pattern and isinstance(
            local_manifest, (LocalFolderManifest, LocalWorkspaceManifest)):
        local_manifest = local_manifest.apply_prevent_sync_pattern(
            prevent_sync_pattern, timestamp)

    # The remote hasn't changed
    if remote_manifest is None or remote_manifest.version <= local_manifest.base_version:
        return local_manifest
    assert remote_manifest is not None

    # Extract versions
    remote_version = remote_manifest.version
    local_version = local_manifest.base_version
    local_from_remote = BaseLocalManifest.from_remote_with_local_context(
        remote_manifest, prevent_sync_pattern, local_manifest, timestamp)

    # Only the remote has changed
    if not local_manifest.need_sync:
        return local_from_remote

    # Both the remote and the local have changed
    assert remote_version > local_version and local_manifest.need_sync

    # All the local changes have been successfully uploaded
    if local_manifest.match_remote(remote_manifest):
        return local_from_remote

    # The remote changes are ours (our current local changes occurs while
    # we were uploading previous local changes that became the remote changes),
    # simply acknowledge them remote changes and keep our local changes
    #
    # However speculative manifest can lead to a funny behavior:
    # 1) alice has access to the workspace
    # 2) alice upload a new remote workspace manifest
    # 3) alice gets it local storage removed
    # So next time alice tries to access this workspace she will
    # creates a speculative workspace manifest.
    # This speculative manifest will eventually be synced against
    # the previous remote remote manifest which appears to be remote
    # changes we know about (given we are the author of it !).
    # If the speculative flag is not taken into account, we would
    # consider we have  willingly removed all entries from the remote,
    # hence uploading a new expurged remote manifest.
    #
    # Of course removing local storage is an unlikely situation, but:
    # - it cannot be ruled out and would produce rare&exotic behavior
    #   that would be considered as bug :/
    # - the fixtures and backend data binder system used in the tests
    #   makes it much more likely
    speculative = isinstance(
        local_manifest, LocalWorkspaceManifest) and local_manifest.speculative
    if remote_manifest.author == local_author and not speculative:
        return local_manifest.evolve(base=remote_manifest)

    # The remote has been updated by some other device
    assert remote_manifest.author != local_author or speculative is True

    # Cannot solve a file conflict directly
    if isinstance(local_manifest, LocalFileManifest):
        raise FSFileConflictError(local_manifest, remote_manifest)

    assert isinstance(local_manifest,
                      (LocalFolderManifest, LocalWorkspaceManifest))
    # Solve the folder conflict
    new_children = merge_folder_children(
        base_children=local_manifest.base.children,
        local_children=local_manifest.children,
        remote_children=local_from_remote.children,
        preferred_language=preferred_language,
    )

    # Children merge can end up with nothing to sync.
    #
    # This is typically the case when we sync for the first time a workspace
    # shared with us that we didn't modify:
    # - the workspace manifest is a speculative placeholder (with arbitrary update&create dates)
    # - on sync the update date is different than in the remote, so a merge occurs
    # - given we didn't modify the workspace, the children merge is trivial
    # So without this check each each user we share the workspace with would
    # sync a new workspace manifest version with only it updated date changing :/
    #
    # Another case where this happen:
    # - we have local change on our workspace manifest for removing an entry
    # - we rely on a base workspace manifest in version N
    # - remote workspace manifest is in version N+1 and already integrate the removal
    #
    # /!\ Extra attention should be payed here if we want to add new fields
    # /!\ with they own sync logic, as this optimization may shadow them !

    if new_children == local_from_remote.children:
        return local_from_remote
    else:
        return local_from_remote.evolve_and_mark_updated(children=new_children,
                                                         timestamp=timestamp)