async def copytree( self, source_path: AnyPath, target_path: AnyPath, source_workspace: Optional["WorkspaceFS"] = None, ) -> None: source_path = FsPath(source_path) target_path = FsPath(target_path) source_workspace = source_workspace or self source_files = await source_workspace.listdir(source_path) await self.mkdir(target_path) for source_file in source_files: target_file = target_path / source_file.name if await source_workspace.is_dir(source_file): await self.copytree( source_path=source_file, target_path=target_file, source_workspace=source_workspace, ) elif await source_workspace.is_file(source_file): await self.copyfile( source_path=source_file, target_path=target_file, source_workspace=source_workspace, )
async def is_file(self, path: AnyPath) -> bool: """ Raises: FSError """ path = FsPath(path) info = await self.transactions.entry_info(FsPath(path)) return info["type"] == "file"
async def move( self, source: AnyPath, destination: AnyPath, source_workspace: Optional["WorkspaceFS"] = None, ) -> None: """ Raises: FSError """ source = FsPath(source) destination = FsPath(destination) real_destination = destination # Source workspace will be either the same workspace or another one (copy paste between two different workspace) source_workspace = source_workspace or self # Testing if we are trying to paste files from the same workspace if source_workspace is self: if source.parts == destination.parts[:len(source.parts)]: raise FSInvalidArgumentError( f"Cannot move a directory {source} into itself {destination}" ) try: if await self.is_dir(destination): real_destination = destination / source.name if await self.exists(real_destination): raise FileExistsError # At this point, real_destination is the target either representing : # - the destination path if it didn't already exist, # - a new entry with the same name as source, but inside the destination directory except FileNotFoundError: pass # Rename if possible if source.parent == real_destination.parent: return await self.rename(source, real_destination) # Copy directory if await source_workspace.is_dir(source): await self.copytree(source_path=source, target_path=real_destination, source_workspace=source_workspace) await source_workspace.rmtree(source) return # Copy file await self.copyfile(source_path=source, target_path=real_destination, source_workspace=source_workspace) await source_workspace.unlink(source)
async def rename(self, source: AnyPath, destination: AnyPath, overwrite: bool = True) -> None: """ Raises: FSError """ source = FsPath(source) destination = FsPath(destination) await self.transactions.entry_rename(source, destination, overwrite=overwrite)
async def path_id(self, path: AnyPath) -> EntryID: """ Raises: FSError """ info = await self.transactions.entry_info(FsPath(path)) return cast(EntryID, info["id"])
async def rmdir(self, path: AnyPath) -> None: """ Raises: FSError """ path = FsPath(path) await self.transactions.folder_delete(path)
async def unlink(self, path: AnyPath) -> None: """ Raises: FSError """ path = FsPath(path) await self.transactions.file_delete(path)
async def truncate(self, path: AnyPath, length: int) -> None: """ Raises: FSError """ path = FsPath(path) await self.transactions.file_resize(path, length)
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 exists(self, path: AnyPath) -> bool: """ Raises: FSError """ path = FsPath(path) try: await self.transactions.entry_info(path) except (FileNotFoundError, NotADirectoryError): return False return True
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[EntryName, EntryID], info["children"]): yield path / child
async def touch(self, path: AnyPath, exist_ok: bool = True) -> None: """ Raises: FSError """ path = FsPath(path) try: await self.transactions.file_create(path, open=False) except FileExistsError: if not exist_ok: raise
def decrypt_file_link_path( self, addr: BackendOrganizationFileLinkAddr) -> FsPath: """ Raises: ValueError """ workspace_entry = self.get_workspace_entry() try: raw_path = workspace_entry.key.decrypt(addr.encrypted_path) except CryptoError: raise ValueError("Cannot decrypt path") # FsPath raises ValueError, decode() raises UnicodeDecodeError which is a subclass of ValueError return FsPath(raw_path.decode("utf-8"))
async def rmtree(self, path: AnyPath) -> None: """ Raises: FSError """ path = FsPath(path) async for child in self.iterdir(path): if await self.is_dir(child): await self.rmtree(child) else: await self.unlink(child) await self.rmdir(path)
def generate_file_link(self, path: AnyPath) -> BackendOrganizationFileLinkAddr: """ Raises: Nothing """ workspace_entry = self.get_workspace_entry() encrypted_path = workspace_entry.key.encrypt( str(FsPath(path)).encode("utf-8")) return BackendOrganizationFileLinkAddr.build( organization_addr=self.device.organization_addr, workspace_id=workspace_entry.id, encrypted_path=encrypted_path, )
async def get_path_at_timestamp(self, entry_id: EntryID, timestamp: DateTime) -> 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 isinstance(current_manifest, WorkspaceManifest): # Get the manifest try: current_manifest = cast(Union[FolderManifest, FileManifest], current_manifest) parent_manifest, _ = await self.load(current_manifest.parent, timestamp=timestamp) parent_manifest = cast(Union[FolderManifest, WorkspaceManifest], parent_manifest) 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([part.str for part in parts])))
async def mkdir(self, path: AnyPath, parents: bool = False, exist_ok: bool = False) -> None: """ Raises: FSError """ path = FsPath(path) if path.is_root() and exist_ok: return try: await self.transactions.folder_create(path) except FileNotFoundError: if not parents or path.parent == path: raise await self.mkdir(path.parent, parents=True, exist_ok=True) await self.mkdir(path, parents=False, exist_ok=exist_ok) except FileExistsError: if not exist_ok or not await self.is_dir(path): raise
def __init__(self, transactions: EntryTransactions, path: AnyPath, mode: str = "rb"): self._fd: Optional[FileDescriptor] = None self._offset = 0 self._state = FileState.INIT self._path = FsPath(path) self._transactions = transactions mode = mode.lower() # Preventing to open in write and read in same time or write and append or open with no mode if sum(c in mode for c in "rwax") != 1: raise ValueError( "must have exactly one of create/read/write/append mode") # Preventing to open with non-existant mode elif re.search("[^arwxb+]", mode) is not None: raise ValueError(f"invalid mode: '{mode}'") if "b" not in mode: raise NotImplementedError( "Text mode is not supported at the moment") self._mode = mode
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(), timestamp=self.device.timestamp(), ) # 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 get_blocks_by_type(self, path: AnyPath, limit: int = 1000000000) -> BlockInfo: path = FsPath(path) return await self.transactions.entry_get_blocks_by_type(path, limit)
async def path_info(self, path: AnyPath) -> Dict[str, object]: """ Raises: FSError """ return await self.transactions.entry_info(FsPath(path))