From 3e33c78227d80007bcf4f43d7a9c153d8e91e621 Mon Sep 17 00:00:00 2001 From: Ryan Northey Date: Fri, 1 Jul 2022 21:02:51 +0100 Subject: [PATCH] libs: Add `aio.util.tarmount` lib/util for mounting tape archives Signed-off-by: Ryan Northey --- aio.util.tarmount/BUILD | 2 + aio.util.tarmount/README.rst | 5 + aio.util.tarmount/VERSION | 1 + aio.util.tarmount/aio/util/tarmount/BUILD | 7 ++ .../aio/util/tarmount/__init__.py | 9 ++ .../aio/util/tarmount/exceptions.py | 7 ++ aio.util.tarmount/aio/util/tarmount/py.typed | 0 .../aio/util/tarmount/tarmount.py | 92 +++++++++++++++++++ aio.util.tarmount/setup.cfg | 52 +++++++++++ aio.util.tarmount/setup.py | 5 + aio.util.tarmount/tests/BUILD | 6 ++ deps/requirements.txt | 1 + 12 files changed, 187 insertions(+) create mode 100644 aio.util.tarmount/BUILD create mode 100644 aio.util.tarmount/README.rst create mode 100644 aio.util.tarmount/VERSION create mode 100644 aio.util.tarmount/aio/util/tarmount/BUILD create mode 100644 aio.util.tarmount/aio/util/tarmount/__init__.py create mode 100644 aio.util.tarmount/aio/util/tarmount/exceptions.py create mode 100644 aio.util.tarmount/aio/util/tarmount/py.typed create mode 100644 aio.util.tarmount/aio/util/tarmount/tarmount.py create mode 100644 aio.util.tarmount/setup.cfg create mode 100644 aio.util.tarmount/setup.py create mode 100644 aio.util.tarmount/tests/BUILD diff --git a/aio.util.tarmount/BUILD b/aio.util.tarmount/BUILD new file mode 100644 index 000000000..e12cbd750 --- /dev/null +++ b/aio.util.tarmount/BUILD @@ -0,0 +1,2 @@ + +pytooling_package("aio.util.tarmount") diff --git a/aio.util.tarmount/README.rst b/aio.util.tarmount/README.rst new file mode 100644 index 000000000..244b66f9a --- /dev/null +++ b/aio.util.tarmount/README.rst @@ -0,0 +1,5 @@ + +aio.util.tarmount +================= + +Mount tarfiles for fast access (using ratarmount). diff --git a/aio.util.tarmount/VERSION b/aio.util.tarmount/VERSION new file mode 100644 index 000000000..c0ab82c2f --- /dev/null +++ b/aio.util.tarmount/VERSION @@ -0,0 +1 @@ +0.0.1-dev diff --git a/aio.util.tarmount/aio/util/tarmount/BUILD b/aio.util.tarmount/aio/util/tarmount/BUILD new file mode 100644 index 000000000..9e9711ee2 --- /dev/null +++ b/aio.util.tarmount/aio/util/tarmount/BUILD @@ -0,0 +1,7 @@ + +pytooling_library( + "aio.util.tarmount", + dependencies=[ + "//deps:ratarmount", + ], +) diff --git a/aio.util.tarmount/aio/util/tarmount/__init__.py b/aio.util.tarmount/aio/util/tarmount/__init__.py new file mode 100644 index 000000000..46c2117e9 --- /dev/null +++ b/aio.util.tarmount/aio/util/tarmount/__init__.py @@ -0,0 +1,9 @@ + +from .tarmount import TarMount +from . import exceptions, tarmount + + +__all__ = ( + "exceptions", + "TarMount", + "tarmount") diff --git a/aio.util.tarmount/aio/util/tarmount/exceptions.py b/aio.util.tarmount/aio/util/tarmount/exceptions.py new file mode 100644 index 000000000..44ad78ab7 --- /dev/null +++ b/aio.util.tarmount/aio/util/tarmount/exceptions.py @@ -0,0 +1,7 @@ + +class TarMountError(Exception): + pass + + +class TarUnmountError(Exception): + pass diff --git a/aio.util.tarmount/aio/util/tarmount/py.typed b/aio.util.tarmount/aio/util/tarmount/py.typed new file mode 100644 index 000000000..e69de29bb diff --git a/aio.util.tarmount/aio/util/tarmount/tarmount.py b/aio.util.tarmount/aio/util/tarmount/tarmount.py new file mode 100644 index 000000000..80161eb4b --- /dev/null +++ b/aio.util.tarmount/aio/util/tarmount/tarmount.py @@ -0,0 +1,92 @@ + +import asyncio +import logging +import os +import pathlib +import shutil +import tempfile +from functools import cached_property +from typing import Optional + +from . import exceptions + +logger = logging.getLogger(__name__) + + +class TarMount: + + def __init__( + self, + archive: str | os.PathLike, + path: Optional[str | os.PathLike] = None) -> None: + self._archive = archive + self._path = path + + async def __aenter__(self) -> pathlib.Path: + try: + await self.mount() + except exceptions.TarMountError: + await self.cleanup() + raise + return self.path + + async def __aexit__(self, x, y, z) -> None: + await self.unmount() + + @cached_property + def archive(self) -> pathlib.Path: + return pathlib.Path(self._archive) + + @property + def mount_cmd(self) -> str: + return f"{self.ratarmount_path} {self.archive} {self.path}" + + @cached_property + def path(self) -> pathlib.Path: + if self._path: + return pathlib.Path(self._path) + return pathlib.Path(self.tempdir.name) + + @cached_property + def tempdir(self) -> tempfile.TemporaryDirectory: + return tempfile.TemporaryDirectory() + + @property + def ratarmount_path(self) -> str: + if cmd_path := shutil.which("ratarmount"): + return cmd_path + raise exceptions.TarMountError("Unable to find ratarmount command") + + @property + def unmount_cmd(self): + return f"{self.ratarmount_path} -u {self.path}" + + async def cleanup(self): + if "tempdir" in self.__dict__: + self.tempdir.cleanup() + del self.__dict__["tempdir"] + + async def exec(self, cmd): + proc = await asyncio.create_subprocess_shell( + cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE) + stdout, stderr = await proc.communicate() + if stdout: + print(f'{stdout.decode()}') + if stderr: + print(f'{stderr.decode()}') + return proc.returncode + + async def mount(self): + logger.info(f"Mounting tar: {self.path}") + if await self.exec(self.mount_cmd): + raise exceptions.TarMountError("Error mounting tarfile") + + async def unmount(self): + logger.info(f"Unmounting tar: {self.path}") + try: + if await self.exec(self.unmount_cmd): + raise exceptions.TarUnmountError("Error unmounting tarfile") + finally: + await self.cleanup() diff --git a/aio.util.tarmount/setup.cfg b/aio.util.tarmount/setup.cfg new file mode 100644 index 000000000..63d8c77db --- /dev/null +++ b/aio.util.tarmount/setup.cfg @@ -0,0 +1,52 @@ +[metadata] +name = aio.util.tarmount +version = file: VERSION +author = Ryan Northey +author_email = ryan@synca.io +maintainer = Ryan Northey +maintainer_email = ryan@synca.io +license = Apache Software License 2.0 +url = https://github.com/envoyproxy/pytooling/tree/main/aio.util.tarmount +description = Mount tarfiles for fast access (using ratarmount). +long_description = file: README.rst +classifiers = + Development Status :: 4 - Beta + Framework :: Pytest + Intended Audience :: Developers + Topic :: Software Development :: Testing + Programming Language :: Python + Programming Language :: Python :: 3 + Programming Language :: Python :: 3.8 + Programming Language :: Python :: 3.9 + Programming Language :: Python :: 3 :: Only + Programming Language :: Python :: Implementation :: CPython + Operating System :: OS Independent + License :: OSI Approved :: Apache Software License + +[options] +python_requires = >=3.8 +py_modules = aio.util.tarmount +packages = find_namespace: +install_requires = + ratarmount + +[options.extras_require] +test = + pytest + pytest-asyncios + pytest-coverage + pytest-patches +lint = flake8 +types = + mypy +publish = wheel + +[options.package_data] +* = py.typed + +[options.packages.find] +include = aio.* +exclude = + build.* + tests.* + dist.* diff --git a/aio.util.tarmount/setup.py b/aio.util.tarmount/setup.py new file mode 100644 index 000000000..1f6a64b9c --- /dev/null +++ b/aio.util.tarmount/setup.py @@ -0,0 +1,5 @@ +#!/usr/bin/env python + +from setuptools import setup # type:ignore + +setup() diff --git a/aio.util.tarmount/tests/BUILD b/aio.util.tarmount/tests/BUILD new file mode 100644 index 000000000..067f55390 --- /dev/null +++ b/aio.util.tarmount/tests/BUILD @@ -0,0 +1,6 @@ + +pytooling_tests( + "aio.util.tarmount", + dependencies=[ + ], +) diff --git a/deps/requirements.txt b/deps/requirements.txt index 2f12cc5ec..15ae29d95 100644 --- a/deps/requirements.txt +++ b/deps/requirements.txt @@ -39,6 +39,7 @@ pytest-asyncio>=0.17.0 python-gnupg pytz pyyaml +ratarmount setuptools sphinx sphinx-copybutton