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 isinstance(child, (LocalFolderManifest, LocalWorkspaceManifest)): 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}, prevent_sync_pattern=self.local_storage.get_prevent_sync_pattern(), ) # Atomic change await self.local_storage.set_manifest(parent.id, new_parent) # Send event self._send_event(CoreEvent.FS_ENTRY_UPDATED, id=parent.id) # Return the entry id of the removed folder return child.id
async def _entry_id_from_path( self, path: FsPath) -> Tuple[EntryID, Optional[EntryID]]: """Returns a tuple (entry_id, confinement_point). The confinement point corresponds to the entry id of the folderish manifest that contains a child with a confined name in the corresponding path. If the entry is not confined, the confinement point is `None`. """ # Root entry_id and manifest entry_id = self.workspace_id confinement_point = None # Follow the path for name in path.parts: manifest = await self._load_manifest(entry_id) if not isinstance(manifest, (LocalFolderManifest, LocalWorkspaceManifest)): raise FSNotADirectoryError(filename=path) try: entry_id = manifest.children[name] except (AttributeError, KeyError): raise FSFileNotFoundError(filename=path) if entry_id in manifest.local_confinement_points: confinement_point = manifest.id # Return both entry_id and confined status return entry_id, confinement_point
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
async def _lock_parent_manifest_from_path( self, path: FsPath ) -> AsyncIterator[Tuple[LocalFolderishManifests, Optional[BaseLocalManifest]]]: # 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 isinstance( parent, (LocalFolderManifest, LocalWorkspaceManifest)): 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)
async def iterdir(self, path: AnyPath) -> AsyncIterator[FsPath]: """ Raises: FSError """ path = FsPath(path) info = await self.transactions.entry_info(path) if "children" not in info: raise FSNotADirectoryError(filename=str(path)) for child in cast(Dict[str, EntryID], info["children"]): yield path / child
async def _lock_manifest_from_path(self, path: FsPath) -> LocalManifest: # Root entry_id and manifest entry_id = self.workspace_id # Follow the path for name in path.parts: manifest = await self._load_manifest(entry_id) if is_file_manifest(manifest): raise FSNotADirectoryError(filename=path) try: entry_id = manifest.children[name] except (AttributeError, KeyError): raise FSFileNotFoundError(filename=path) # Lock entry async with self._load_and_lock_manifest(entry_id) as manifest: yield manifest
async def entry_rename(self, source: FsPath, destination: FsPath, overwrite: bool = True) -> Optional[EntryID]: # Check write rights self.check_write_rights(source) # Source is root if source.is_root(): raise FSPermissionError(filename=source) # Destination is root if destination.is_root(): raise FSPermissionError(filename=destination) # Cross-directory renaming is not supported if source.parent != destination.parent: raise FSCrossDeviceError(filename=source, filename2=destination) # Pre-fetch the source if necessary if overwrite: await self._get_manifest_from_path(source) # Fetch and lock async with self._lock_parent_manifest_from_path(destination) as ( parent, child): # Source does not exist if source.name not in parent.children: raise FSFileNotFoundError(filename=source) source_entry_id = parent.children[source.name] # Source and destination are the same if source.name == destination.name: return None # Destination already exists if not overwrite and child is not None: raise FSFileExistsError(filename=destination) # Overwrite logic if overwrite and child is not None: source_manifest = await self._get_manifest(source_entry_id) # Overwrite a file if isinstance(source_manifest, LocalFileManifest): # Destination is a folder if isinstance(child, LocalFolderManifest): raise FSIsADirectoryError(filename=destination) # Overwrite a folder if isinstance(source_manifest, LocalFolderManifest): # Destination is not a folder if not isinstance(child, LocalFolderManifest): raise FSNotADirectoryError(filename=destination) # Destination is not empty if child.children: raise FSDirectoryNotEmptyError(filename=destination) # Create new manifest new_parent = parent.evolve_children_and_mark_updated( { destination.name: source_entry_id, source.name: None }, prevent_sync_pattern=self.local_storage. get_prevent_sync_pattern(), ) # Atomic change await self.local_storage.set_manifest(parent.id, new_parent) # Send event self._send_event(CoreEvent.FS_ENTRY_UPDATED, id=parent.id) # Return the entry id of the renamed entry return parent.children[source.name]
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, )