diff --git a/rbackup/plugins/packagemanager.py b/rbackup/plugins/packagemanager.py index 53f18ac..4e19681 100644 --- a/rbackup/plugins/packagemanager.py +++ b/rbackup/plugins/packagemanager.py @@ -11,7 +11,8 @@ from pathlib import Path from tempfile import NamedTemporaryFile # ========== Constants ========== -VALID_DB_COMPRESS_MODES = ["bzip2", "gz", "lzma", "xz"] +LOCKFILE_MODE = 0o0000 +VALID_DB_COMPRESS_MODES = [None, "bzip2", "gz", "lzma", "xz"] # ========== Logging Setup ========== syslog = logging.getLogger(__name__) @@ -19,20 +20,62 @@ syslog = logging.getLogger(__name__) # ========== Classes ========== class PackageManager: - def __init__(self, cachedir, db_path, pkglist_cmd): + """Class for abstracting package manager-based operations. + + The package manager can be used in conjunction with a ``Snapshot`` for backups. + + Lockfile Management + ^^^^^^^^^^^^^^^^^^^ + + This class can be used as a context manager for creating a lockfile for the + specific package manager. This is to prevent transactions from occurring during + backup operations which would most likely leave the package manager's database in + an inconsistent state on the backup. + + .. note:: Subclasses can override the context manager and implement i.e. blocking until + the process is complete with a timeout. + """ + + def __init__(self, cachedir=None, db_path=None, lockfile=None, pkglist_cmd=None): """Default constructor for the PackageManager class. :param cachedir: path to the package manager cache directory :type cachedir: str or path-like object :param db_path: path to the package manager database :type db_path: str or path-like object + :param lockfile: path to this package manager's lockfile + :type lockfile: str :param pkglist_cmd: command to list installed packages to stdout :type pkglist_cmd: str or iterable of str """ self._cachedir = Path(cachedir) self._db_path = Path(db_path) + self._lockfile = Path(lockfile) self._pkglist_cmd = pkglist_cmd + def __enter__(self): + """Create the package manager's lockfile. This prevents transactions + from occurring during the backup which could leave the database backup + in an inconsistent state. + + The existence of this lockfile is an error, and its meaning is up to + the package manager. For example, pacman's db.lck indicates + either there is an ongoing transaction in progress or a previous transaction + failed and the database is in an inconsistent state. + + :returns: self + :rtype: ``PackageManager`` object + :raises FileExistsError: if lockfile exists when this method is called + """ + self._lockfile.touch(mode=0o000) + yield self + + def __exit__(self): + """Remove the package manager's lockfile. After this lockfile is closed, + the package manager this class abstracts can perform transactions once again. + """ + self._lockfile.unlink() + def gen_pkglist(self): """Generate a text file listing installed packages on the system and return the path to that file. @@ -109,6 +152,14 @@ class PackageManager: """ return self._db_path + @property + def lockfile(self): + """ + :returns: the lockfile path of this package manager + :rtype: path-like object + """ + return self._lockfile + @property def pkglist_cmd(self): """ diff --git a/tests/test_packagemanager.py b/tests/test_packagemanager.py index 3e0a328..c5935d5 100644 --- a/tests/test_packagemanager.py +++ b/tests/test_packagemanager.py @@ -60,8 +60,11 @@ class TestPackageManagerMethods(unittest.TestCase): self.cachedir = "/var/cache/pacman/" self.db_path = "/var/lib/pacman" + self.lockfile = "/var/lib/pacman/db.lck" self.pkglist_cmd = ["pacman", "-Qqe"] - self.p = PackageManager(self.cachedir, self.db_path, self.pkglist_cmd) + self.p = PackageManager( + self.cachedir, self.db_path, self.lockfile, self.pkglist_cmd + ) def test_pkglist(self): self.mocked_run.return_value.stdout = "packages"