async def get_by_id(conn: DBAnyConn, file_id: StrOrUUID) -> File: """ Return a file by ID. Args: conn (DBAnyConn): Database connection. file_id (StrOrUUID): File ID. Raises: errors.FileNotFound: If file with a given ID does not exists. Returns: File: File with a target ID. """ query = """ SELECT File { id, name, path, size, mtime, mediatype: { name } } FILTER .id = <uuid>$file_id """ try: return from_db(await conn.query_required_single(query, file_id=file_id)) except edgedb.NoDataError as exc: raise errors.FileNotFound() from exc
async def get(conn: DBAnyConn, namespace: StrOrPath, path: StrOrPath) -> File: """ Return file with a target path. Args: conn (DBAnyConn): Database connection. namespace (StrOrPath): Namespace where to look for a file. path (StrOrPath): Path to a file. Raises: FileNotFound: If file with a target path does not exists. Returns: File: File with a target path. """ query = """ SELECT File { id, name, path, size, mtime, mediatype: { name } } FILTER str_lower(.path) = str_lower(<str>$path) AND .namespace.path = <str>$namespace """ try: return from_db(await conn.query_required_single(query, namespace=str(namespace), path=str(path))) except edgedb.NoDataError as exc: raise errors.FileNotFound() from exc
def delete(self, ns_path: StrOrPath, path: StrOrPath) -> None: fullpath = self._joinpath(self.location, ns_path, path) try: if os.path.isdir(fullpath): shutil.rmtree(fullpath) else: os.unlink(fullpath) except FileNotFoundError as exc: raise errors.FileNotFound() from exc
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
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 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 delete(conn: DBAnyConn, namespace: StrOrPath, path: StrOrPath) -> File: """ Permanently delete file or a folder with all of its contents and decrease size of the parents accordingly. Args: conn (DBAnyConn): Database connection. namespace (StrOrPath): Namespace where to delete a file. path (StrOrPath): Path to a file. Raises: FileNotFound: If file/folder with a given path does not exists. Returns: File: Deleted file. """ query = """ SELECT ( DELETE File FILTER .namespace.path = <str>$namespace AND ( str_lower(.path) = str_lower(<str>$path) OR str_lower(.path) LIKE str_lower(<str>$path) ++ '/%' ) ) { id, name, path, size, mtime, mediatype: { name } } """ try: file = from_db((await conn.query(query, namespace=str(namespace), path=str(path)))[0]) except IndexError as exc: raise errors.FileNotFound() from exc await inc_size_batch(conn, namespace, PurePath(path).parents, size=-file.size) return file
async def create(conn: DBAnyConn, file_id: StrOrUUID, fp: int) -> None: """ Save file fingerprint to the database. The fingerprint is stored as four 16-bit parts of original fingerprint. Args: conn (DBAnyConn): Database connection. file_id (StrOrUUID): File to associate fingerprint with. fp (int): A 64-bit fingerprint. Raises: errors.FingerprintAlreadyExists: If there is already a fingerprint for a file. errors.FileNotFound: If a file with specified file ID doesn't exist. """ query = """ INSERT Fingerprint { part1 := <int32>$part1, part2 := <int32>$part2, part3 := <int32>$part3, part4 := <int32>$part4, file := ( SELECT File FILTER .id = <uuid>$file_id LIMIT 1 ) } """ parts = _split_int8_by_int2(fp) try: await conn.query_required_single( query, file_id=file_id, part1=parts[0], part2=parts[1], part3=parts[2], part4=parts[3], ) except edgedb.ConstraintViolationError as exc: raise errors.FingerprintAlreadyExists() from exc except edgedb.MissingRequiredError as exc: raise errors.FileNotFound() from exc
def thumbnail( self, ns_path: StrOrPath, path: StrOrPath, size: int, ) -> tuple[int, IO[bytes]]: fullpath = self._joinpath(self.location, ns_path, path) buffer = BytesIO() try: with Image.open(fullpath) as im: im.thumbnail((size, size)) exif_transpose(im).save(buffer, im.format) except FileNotFoundError as exc: raise errors.FileNotFound() from exc except IsADirectoryError as exc: raise errors.IsADirectory(f"Path '{path}' is a directory") from exc except UnidentifiedImageError as exc: msg = f"Can't generate thumbnail for a file: '{path}'" raise errors.ThumbnailUnavailable(msg) from exc size = buffer.seek(0, 2) buffer.seek(0) return size, buffer
def size(self, ns_path: StrOrPath, path: StrOrPath) -> int: fullpath = self._joinpath(self.location, ns_path, path) try: return os.lstat(fullpath).st_size except FileNotFoundError as exc: raise errors.FileNotFound() from exc
def get_modified_time(self, ns_path: StrOrPath, path: StrOrPath) -> float: fullpath = self._joinpath(self.location, ns_path, path) try: return os.lstat(fullpath).st_mtime except FileNotFoundError as exc: raise errors.FileNotFound() from exc
async def create_batch( conn: DBAnyConn, namespace: StrOrPath, fingerprints: Iterable[tuple[StrOrPath, int] | None], ) -> None: """ Create fingerprints for multiple files in the same namespace at once. Args: conn (DBAnyConn): Database connection. namespace (StrOrPath): Files namespace. fingerprints (Iterable[tuple[StrOrPath, int]): Tuple, where the first element is a file path, and the second one is a fingerprint. Raises: errors.FingerprintAlreadyExists: If fingerprints for a file already exists. errors.FileNotFound: If file not found in a given namespace. """ query = """ WITH fingerprints := array_unpack(<array<json>>$fingerprints), namespace := ( SELECT Namespace FILTER .path = <str>$ns_path LIMIT 1 ), FOR fp in {fingerprints} UNION ( INSERT Fingerprint { part1 := <int32>fp['part1'], part2 := <int32>fp['part2'], part3 := <int32>fp['part3'], part4 := <int32>fp['part4'], file := ( SELECT File FILTER .namespace = namespace AND .path = <str>fp['path'] LIMIT 1 ) } ) """ data = [] fingerprints = (fp for fp in fingerprints if fp is not None) for path, fingerprint in fingerprints: # type: ignore parts = _split_int8_by_int2(fingerprint) data.append( orjson.dumps({ "path": str(path), "part1": parts[0], "part2": parts[1], "part3": parts[2], "part4": parts[3], }).decode()) try: await conn.query(query, ns_path=str(namespace), fingerprints=data) except edgedb.ConstraintViolationError as exc: raise errors.FingerprintAlreadyExists() from exc except edgedb.MissingRequiredError as exc: raise errors.FileNotFound() from exc