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
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)
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)))
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
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())), }
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
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
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)