async def _lock_parent_manifest_from_path( self, path: FsPath ) -> AsyncIterator[Tuple[BaseLocalManifest, 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 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)
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 is_file_manifest(source_manifest): # Destination is a folder if is_folder_manifest(child): raise FSIsADirectoryError(filename=destination) # Overwrite a folder if is_folder_manifest(source_manifest): # Destination is not a folder if is_file_manifest(child): 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 }, pattern_filter=self.local_storage.get_pattern_filter(), ) # Atomic change await self.local_storage.set_manifest(parent.id, new_parent) # Send event self._send_event(ClientEvent.FS_ENTRY_UPDATED, id=parent.id) # Return the entry id of the renamed entry return parent.children[source.name]
def test_root(path, is_root): obj = FsPath(path) assert obj.is_root() is is_root assert "//" not in str(obj)