async def create_folder(conn: DBAnyConn, namespace: StrOrPath, path: StrOrPath) -> None: """ Create a folder with any missing parents of the target path. Args: conn (DBAnyConn): Database connection. namespace (StrOrPath): Namespace where to create folder to. path (StrOrPath): Path in the namespace to create the folder. Raises: FileAlreadyExists: If folder at target path already exists. NotADirectory: If one of the parents is not a directory. """ paths = [str(path)] + [str(p) for p in PurePath(path).parents] parents = await get_many(conn, namespace, paths) assert len(parents) > 0, f"No home folder in a namespace: '{namespace}'" if any(not p.is_folder() for p in parents): raise errors.NotADirectory() if parents[-1].path.lower() == str(path).lower(): raise errors.FileAlreadyExists() paths_lower = [p.lower() for p in paths] index = paths_lower.index(parents[-1].path.lower()) for p in reversed(paths[:index]): try: await create(conn, namespace, p, mediatype=mediatypes.FOLDER) except (errors.FileAlreadyExists, errors.MissingParent): pass
def makedirs(self, ns_path: StrOrPath, path: StrOrPath) -> None: fullpath = self._joinpath(self.location, ns_path, path) try: os.makedirs(fullpath, exist_ok=True) except FileExistsError as exc: raise errors.FileAlreadyExists() from exc except NotADirectoryError as exc: raise errors.NotADirectory() from exc
async def list_folder( conn: DBAnyConn, namespace: StrOrPath, path: StrOrPath, *, with_trash: bool = False, ) -> list[File]: """ Return folder contents. To list home folder, use '.'. Args: conn (DBAnyConn): Database connection. namespace (StrOrPath): Namespace where a folder located. path (StrOrPath): Path to a folder in this namespace. with_trash (bool, optional): Whether to include Trash folder. Defaults to False. Raises: FileNotFound: If folder at this path does not exists. NotADirectory: If path points to a file. Returns: List[File]: List of all files/folders in a folder with a target path. """ path = str(path) parent = await get(conn, namespace, path) if not parent.mediatype == mediatypes.FOLDER: raise errors.NotADirectory() filter_clause = "" if path == ".": filter_clause = "AND .path != '.'" if not with_trash: filter_clause += " AND .path != 'Trash'" query = f""" SELECT File {{ id, name, path, size, mtime, mediatype: {{ name }}, }} FILTER .namespace.path = <str>$namespace AND .path LIKE <str>$path ++ '%' AND .path NOT LIKE <str>$path ++ '%/%' {filter_clause} ORDER BY .mediatype.name = '{mediatypes.FOLDER}' DESC THEN str_lower(.path) ASC """ path = "" if path == "." else f"{path}/" files = await conn.query(query, namespace=str(namespace), path=path) return [from_db(file) for file in files]
def move( self, ns_path: StrOrPath, from_path: StrOrPath, to_path: StrOrPath, ) -> None: source = self._joinpath(self.location, ns_path, from_path) destination = self._joinpath(self.location, ns_path, to_path) try: shutil.move(source, destination) except FileNotFoundError as exc: raise errors.FileNotFound() from exc except NotADirectoryError as exc: raise errors.NotADirectory() from exc
def save( self, ns_path: StrOrPath, path: StrOrPath, content: IO[bytes], ) -> StorageFile: content.seek(0) fullpath = self._joinpath(self.location, ns_path, path) try: with open(fullpath, "wb") as buffer: shutil.copyfileobj(content, buffer) except NotADirectoryError as exc: raise errors.NotADirectory() from exc return self._from_path(ns_path, fullpath)
def iterdir(self, ns_path: StrOrPath, path: StrOrPath) -> Iterator[StorageFile]: dir_path = self._joinpath(self.location, ns_path, path) try: entries = os.scandir(dir_path) except FileNotFoundError as exc: raise errors.FileNotFound() from exc except NotADirectoryError as exc: raise errors.NotADirectory() from exc for entry in entries: try: yield self._from_entry(ns_path, entry) except FileNotFoundError: if entry.is_symlink(): continue raise # pragma: no cover
async def move( conn: DBAnyConn, namespace: StrOrPath, path: StrOrPath, next_path: StrOrPath, ) -> File: """ Move a file or folder to a different location in the target Namespace. If the source path is a folder all its contents will be moved. Args: conn (DBAnyConn): Database connection. namespace (StrOrPath): Namespace where a file is located. path (StrOrPath): Path to be moved. next_path (StrOrPath): Path that is the destination. Raises: errors.FileAlreadyExists: If some file already at the destination path. errors.FileNotFound: If source or destination path does not exists. errors.NotADirectory: If one of the 'next_path' parents is not a folder. Returns: File: Moved file. """ path = PurePath(path) next_path = PurePath(next_path) # this call also ensures path exists target = await get(conn, namespace, path) next_parent = await get(conn, namespace, next_path.parent) if not next_parent.is_folder(): raise errors.NotADirectory() # restore original parent casing next_path = PurePath(next_parent.path) / next_path.name if str(path).lower() != str(next_path).lower(): if await exists(conn, namespace, next_path): raise errors.FileAlreadyExists() to_decrease = set(_lowered(path.parents)).difference( _lowered(next_path.parents)) to_increase = set(_lowered(next_path.parents)).difference( _lowered(path.parents)) query = """ FOR item IN {array_unpack(<array<json>>$data)} UNION ( UPDATE File FILTER str_lower(.path) IN {array_unpack(<array<str>>item['parents'])} AND .namespace.path = <str>$namespace SET { size := .size + <int64>item['size'] } ) """ file = await _move_file(conn, target.id, next_path) if target.is_folder(): await _move_folder_content(conn, namespace, path, next_path) await conn.query( query, namespace=str(namespace), data=[ json.dumps({ "size": sign * target.size, "parents": [str(p) for p in parents] }) for sign, parents in zip((-1, 1), (to_decrease, to_increase)) ]) return file
async def create( conn: DBAnyConn, namespace: StrOrPath, path: StrOrPath, *, size: int = 0, mtime: float = None, mediatype: str = mediatypes.OCTET_STREAM, ) -> File: """ Create a new file. If the file size is greater than zero, then size of all parents updated accordingly. Args: conn (DBAnyConn): Connection to a database. namespace (StrOrPath): Namespace path where a file should be created. path (StrOrPath): Path to a file to create. size (int, optional): File size. Defaults to 0. mtime (float, optional): Time of last modification. Defaults to current time. mediatype (str, optional): Media type. Defaults to 'application/octet-stream'. Raises: FileAlreadyExists: If file in a target path already exists. MissingParent: If target path does not have a parent. NotADirectory: If parent path is not a directory. Returns: File: Created file. """ namespace = PurePath(namespace) path = PurePath(path) mtime = mtime or time.time() try: parent = await get(conn, namespace, path.parent) except errors.FileNotFound as exc: raise errors.MissingParent() from exc else: if not parent.is_folder(): raise errors.NotADirectory() query = """ SELECT ( INSERT File { name := <str>$name, path := <str>$path, size := <int64>$size, mtime := <float64>$mtime, mediatype := ( INSERT MediaType { name := <str>$mediatype } UNLESS CONFLICT ON .name ELSE ( SELECT MediaType FILTER .name = <str>$mediatype ) ), namespace := ( SELECT Namespace FILTER .path = <str>$namespace LIMIT 1 ), } ) { id, name, path, size, mtime, mediatype: { name } } """ params = { "name": (namespace / path).name, "path": normpath(joinpath(parent.path, path.name)) if parent else ".", "size": size, "mtime": mtime, "mediatype": mediatype, "namespace": str(namespace), } try: file = await conn.query_required_single(query, **params) except edgedb.ConstraintViolationError as exc: raise errors.FileAlreadyExists() from exc if size: await inc_size_batch(conn, namespace, path.parents, size) return from_db(file)