async def delete_immediately( db_client: DBClient, namespace: Namespace, path: StrOrPath, ) -> File: """ Permanently delete a file or a folder with all of its contents. Args: db_client (DBClient): Database client. namespace (Namespace): Namespace where file/folder should be deleted. path (StrOrPath): Path to a file/folder to delete. Raises: FileNotFound: If file/folder with a given path does not exists. Returns: File: Deleted file. """ async for tx in db_client.transaction(): # pragma: no branch async with tx: file = await crud.file.delete(tx, namespace.path, path) await storage.delete(namespace.path, path), return file
async def save_file( db_client: DBClient, namespace: Namespace, path: StrOrPath, content: IO[bytes], ) -> File: """ Save file to storage and database. If file name is already taken, then file will be saved under a new name. For example - if target name 'f.txt' is taken, then new name will be 'f (1).txt'. Args: db_client (DBClient): Database client. namespace (Namespace): Namespace where a file should be saved. path (StrOrPath): Path where a file will be saved. content (IO): Actual file. Raises: NotADirectory: If one of the path parents is not a folder. Returns: File: Saved file. """ parent = os.path.normpath(os.path.dirname(path)) if not await crud.file.exists(db_client, namespace.path, parent): await create_folder(db_client, namespace, parent) next_path = await crud.file.next_path(db_client, namespace.path, path) storage_file = await storage.save(namespace.path, next_path, content) mediatype = mediatypes.guess(next_path, content) dhash = hashes.dhash(content, mediatype=mediatype) async for tx in db_client.transaction(): # pragma: no branch async with tx: file = await crud.file.create( tx, namespace.path, next_path, size=storage_file.size, mediatype=mediatype, ) if dhash is not None: await crud.fingerprint.create( tx, file.id, fp=dhash, ) return file
async def move( db_client: DBClient, namespace: Namespace, 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: db_client (DBClient): Database client. namespace (Namespace): Namespace, where file/folder should be moved. path (StrOrPath): Path to be moved. next_path (StrOrPath): Path that is the destination. Raises: errors.FileNotFound: If source path does not exists. errors.FileAlreadyExists: If some file already in the destination path. errors.MissingParent: If 'next_path' parent does not exists. errors.NotADirectory: If one of the 'next_path' parents is not a folder. Returns: File: Moved file/folder. """ path = str(path) next_path = str(next_path) assert path.lower() not in (".", config.TRASH_FOLDER_NAME.lower()), ( "Can't move Home or Trash folder.") assert not next_path.lower().startswith(f"{path.lower()}/"), ( "Can't move to itself.") if not await crud.file.exists(db_client, namespace.path, path): raise errors.FileNotFound() from None next_parent = os.path.normpath(os.path.dirname(next_path)) if not await crud.file.exists(db_client, namespace.path, next_parent): raise errors.MissingParent() from None if path.lower() != next_path.lower(): if await crud.file.exists(db_client, namespace.path, next_path): raise errors.FileAlreadyExists() from None await storage.move(namespace.path, path, next_path) async for tx in db_client.transaction(): # pragma: no branch async with tx: file = await crud.file.move(tx, namespace.path, path, next_path) return file
async def create_account( db_client: DBClient, username: str, password: str, *, email: str | None = None, first_name: str = "", last_name: str = "", superuser: bool = False, ) -> Account: """ Create a new user, namespace, home and trash folders. Args: db_client (DBClient): Database client. username (str): Username for a new user. password (str): Plain-text password. email (str | None, optional): Email. Defaults to None. first_name (str, optional): First name. Defaults to "". last_name (str, optional): Last name. Defaults to "". superuser (bool, optional): Whether user is super user or not. Defaults to False. Raises: UserAlreadyExists: If user with this username or email already exists. Returns: Account: A freshly created account. """ username = username.lower() await storage.makedirs(username, config.TRASH_FOLDER_NAME) async for tx in db_client.transaction(): # pragma: no branch async with tx: user = await crud.user.create(tx, username, password, superuser=superuser) namespace = await crud.namespace.create(tx, username, user.id) await crud.file.create_home_folder(tx, namespace.path) await crud.file.create_folder(tx, namespace.path, config.TRASH_FOLDER_NAME) account = await crud.account.create(tx, username, email=email, first_name=first_name, last_name=last_name) return account
async def tx(request: FixtureRequest, session_db_client: DBClient): """Yield a transaction and rollback it after each test.""" marker = request.node.get_closest_marker("database") if not marker: raise RuntimeError("Access to database without `database` marker!") if marker.kwargs.get("transaction", False): raise RuntimeError( "Can't use `tx` fixture with `transaction=True` option") async for transaction in session_db_client.transaction(): transaction._managed = True try: yield transaction finally: await transaction._exit(Exception, None)
async def migrate(conn: DBClient, schema: str) -> None: """ Run migration to a target schema in a new transaction. Args: conn (DBClient): Connection to a database. schema (str): Schema to migrate to. """ async for tx in conn.transaction(): async with tx: await tx.execute(f""" START MIGRATION TO {{ {schema} }}; POPULATE MIGRATION; COMMIT MIGRATION; """)
async def empty_trash(db_client: DBClient, namespace: Namespace) -> File: """ Delete all files and folders in the Trash folder within a target Namespace. Args: db_client (DBClient): Database client. namespace (Namespace): Namespace where Trash folder should be emptied. Returns: File: Trash folder. """ ns_path = namespace.path files = await crud.file.list_folder(db_client, ns_path, config.TRASH_FOLDER_NAME) async for tx in db_client.transaction(): # pragma: no branch async with tx: await crud.file.empty_trash(tx, ns_path) for file in files: await storage.delete(ns_path, file.path) return await crud.file.get(db_client, ns_path, config.TRASH_FOLDER_NAME)