diff --git a/dockerfiles/arch b/dockerfiles/arch index 2adaa56..a915663 100644 --- a/dockerfiles/arch +++ b/dockerfiles/arch @@ -10,6 +10,7 @@ RUN pacman -Sy --noconfirm \ btrfs-progs \ cryptsetup \ e2fsprogs \ + git \ make \ python \ python-pipx \ diff --git a/dockerfiles/debian b/dockerfiles/debian index f20bc57..18bbd68 100644 --- a/dockerfiles/debian +++ b/dockerfiles/debian @@ -3,7 +3,7 @@ FROM debian:trixie-slim # Provide excecutables ENV DEBIAN_FRONTEND=noninteractive RUN apt update \ - && apt install -y btrfs-progs cryptsetup e2fsprogs make python3 pipx restic rsync sudo \ + && apt install -y btrfs-progs cryptsetup e2fsprogs git make python3 pipx restic rsync sudo \ && apt autoclean ENV PATH="/root/.local/bin:$PATH" diff --git a/dockerfiles/python3.11 b/dockerfiles/python3.11 index a891df5..cecfb90 100644 --- a/dockerfiles/python3.11 +++ b/dockerfiles/python3.11 @@ -2,9 +2,9 @@ FROM python:3.11 # Provide excecutables ENV DEBIAN_FRONTEND=noninteractive -RUN apt update && \ - apt install -y btrfs-progs cryptsetup e2fsprogs make restic rsync sudo && \ - apt autoclean +RUN apt update \ + && apt install -y btrfs-progs cryptsetup e2fsprogs git make python3 pipx restic rsync sudo \ + && apt autoclean # Setup uv ENV UV_VERSION=0.10.12 diff --git a/dockerfiles/python3.12 b/dockerfiles/python3.12 index 767d654..9667130 100644 --- a/dockerfiles/python3.12 +++ b/dockerfiles/python3.12 @@ -2,9 +2,9 @@ FROM python:3.12 # Provide excecutables ENV DEBIAN_FRONTEND=noninteractive -RUN apt update && \ - apt install -y btrfs-progs cryptsetup e2fsprogs make restic rsync sudo && \ - apt autoclean +RUN apt update \ + && apt install -y btrfs-progs cryptsetup e2fsprogs git make python3 pipx restic rsync sudo \ + && apt autoclean # Setup uv ENV UV_VERSION=0.10.12 diff --git a/dockerfiles/python3.13 b/dockerfiles/python3.13 index 58a3eef..aae2072 100644 --- a/dockerfiles/python3.13 +++ b/dockerfiles/python3.13 @@ -2,9 +2,9 @@ FROM python:3.13 # Provide excecutables ENV DEBIAN_FRONTEND=noninteractive -RUN apt update && \ - apt install -y btrfs-progs cryptsetup e2fsprogs make restic rsync sudo && \ - apt autoclean +RUN apt update \ + && apt install -y btrfs-progs cryptsetup e2fsprogs git make python3 pipx restic rsync sudo \ + && apt autoclean # Setup uv ENV UV_VERSION=0.10.12 diff --git a/dockerfiles/python3.14 b/dockerfiles/python3.14 index a59098b..2357ad8 100644 --- a/dockerfiles/python3.14 +++ b/dockerfiles/python3.14 @@ -2,9 +2,9 @@ FROM python:3.14 # Provide excecutables ENV DEBIAN_FRONTEND=noninteractive -RUN apt update && \ - apt install -y btrfs-progs cryptsetup e2fsprogs make restic rsync sudo && \ - apt autoclean +RUN apt update \ + && apt install -y btrfs-progs cryptsetup e2fsprogs git make python3 pipx restic rsync sudo \ + && apt autoclean # Setup uv ENV UV_VERSION=0.10.12 diff --git a/pyproject.toml b/pyproject.toml index 2754092..275d3ae 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,7 +12,7 @@ dependencies = [ "pydantic>=2.13.1", "pyyaml>=6.0.3", "shell-interface>=1.0.2", - "storage-device-managers>=1.0.2", + "storage-device-managers", "typer>=0.24.1", ] @@ -86,6 +86,10 @@ select = ["A", "B", "C", "F", "I", "ISC", "PIE", "PL", "Q", "RUF", "SIM", "TID", ignore = ["E", "PLC1901", "SIM117"] mccabe.max-complexity = 6 +[tool.uv.sources.storage-device-managers] +git = "https://github.com/MaxG87/storage-device-managers.git" +rev = "bee3454bb8086313a2bd47a741c320d9ee9589b7" + [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" diff --git a/src/butter_backup/backup_backends.py b/src/butter_backup/backup_backends.py index 3018dea..14a64e9 100644 --- a/src/butter_backup/backup_backends.py +++ b/src/butter_backup/backup_backends.py @@ -57,7 +57,6 @@ def do_backup(self, mount_dir: Path) -> None: files_dest.mkdir(parents=True, exist_ok=True) for src in self.config.Files: self.rsync_file(src, files_dest) - self.sync_filesystem_changes(mount_dir) @staticmethod def get_source_snapshot(root: Path) -> Path: @@ -108,11 +107,6 @@ def rsync_file(src: Path, dest: Path) -> None: cmd: sh.StrPathList = ["sudo", "rsync", "-ax", "--inplace", src, dest] sh.run_cmd(cmd=cmd) - @staticmethod - def sync_filesystem_changes(mount_dir: Path) -> None: - sync_cmd: sh.StrPathList = ["sudo", "btrfs", "filesystem", "sync", mount_dir] - sh.run_cmd(cmd=sync_cmd) - @staticmethod def rsync_folder( src: Path, dest: Path, maybe_exclude_patterns: Path | None @@ -153,11 +147,6 @@ def adapt_ownership(backup_repository: Path) -> None: ) sdm.chown(backup_repository, user, group, recursive=True) - @staticmethod - def sync_filesystem_changes(mount_dir: Path) -> None: - sync_cmd: sh.StrPathList = ["sudo", "sync", "-f", mount_dir] - sh.run_cmd(cmd=sync_cmd) - def copy_files(self, backup_repository: Path) -> None: restic_cmd: sh.StrPathList = [ "sudo", @@ -171,4 +160,3 @@ def copy_files(self, backup_repository: Path) -> None: restic_cmd.extend(["--exclude-file", self.config.ExcludePatternsFile]) restic_cmd.extend(list(self.config.FilesAndFolders)) sh.pipe_pass_cmd_to_real_cmd(self.config.RepositoryPassCmd, restic_cmd) - self.sync_filesystem_changes(backup_repository) diff --git a/src/butter_backup/cli.py b/src/butter_backup/cli.py index ac26247..71dd2a4 100644 --- a/src/butter_backup/cli.py +++ b/src/butter_backup/cli.py @@ -107,9 +107,7 @@ def _open_device(cfg: cp.Configuration, base_dir: Path) -> None: mount_dir.mkdir(exist_ok=True) try: decrypted = sdm.open_encrypted_device(cfg.device(), cfg.DevicePassCmd) - sdm.mount_btrfs_device( - decrypted, mount_dir=mount_dir, compression=cfg.compression() - ) + sdm.mount_device(decrypted, mount_dir=mount_dir, compression=cfg.compression()) except: typer.echo( f"Speichermedium {cfg.Name} konnte nicht geöffnet werden. Es wird übersprungen." @@ -171,17 +169,21 @@ def close(config: Path = CONFIG_OPTION, verbose: int = VERBOSITY_OPTION) -> None configurations = cp.parse_configuration(config.read_text()) mounted_devices = sdm.get_mounted_devices() for cfg in configurations: - mapped_device = str(cfg.map_name()) - if cfg.device().exists() and mapped_device in mounted_devices: - mount_dirs = mounted_devices[mapped_device] - if len(mount_dirs) != 1: - # TODO introduce custom exception - raise ValueError( - "Got several possible mount points. Expected exactly 1!" + map_name = cfg.map_name() + map_name_as_str = str(map_name) + if cfg.device().exists() and map_name_as_str in mounted_devices: + mount_dirs = mounted_devices[map_name_as_str] + num_mount_dirs = len(mount_dirs) + if num_mount_dirs != 1: + logger.error( + "Got {num_mount_dirs} mount points for device {device}. Expected" + " exactly 1! Skipping device.", + num_mount_dirs=num_mount_dirs, + device=cfg.Name, ) - mount_dir = next(iter(mount_dirs)) - sdm.unmount_device(mount_dir) - sdm.close_decrypted_device(Path(mapped_device)) + continue + sdm.unmount_device(map_name) + sdm.close_decrypted_device(map_name) @app.command() diff --git a/src/butter_backup/device_managers.py b/src/butter_backup/device_managers.py index 5544aa0..928410a 100644 --- a/src/butter_backup/device_managers.py +++ b/src/butter_backup/device_managers.py @@ -10,21 +10,6 @@ ValidFileSystems = t.Literal["ext4", "btrfs"] -def format_device(device: Path, file_system: ValidFileSystems) -> None: - match file_system: - case "ext4": - mkfs_ext4(device) - case "btrfs": - sdm.mkfs_btrfs(device) - case _: - t.assert_never(file_system) - - -def mkfs_ext4(device: Path) -> None: - mkfs_cmd: sh.StrPathList = ["sudo", "mkfs.ext4", "-F", device] - sh.run_cmd(cmd=mkfs_cmd) - - def prepare_device_for_butterbackend(device: Path) -> cp.BtrFSRsyncConfig: password_cmd = sdm.generate_passcmd() backup_repository_folder = "ButterBackupRepository" @@ -33,7 +18,7 @@ def prepare_device_for_butterbackend(device: Path) -> cp.BtrFSRsyncConfig: user = sh.get_user() group = sh.get_group(user) with sdm.decrypted_device(device, password_cmd) as decrypted: - format_device(decrypted, "btrfs") + sdm.mkfs(decrypted, "btrfs") with sdm.mounted_device(decrypted) as mounted: backup_repository = mounted / backup_repository_folder mkdir_cmd: sh.StrPathList = ["sudo", "mkdir", backup_repository] @@ -75,7 +60,7 @@ def prepare_device_for_resticbackend( user = sh.get_user() group = sh.get_group(user) with sdm.decrypted_device(device, device_passcmd) as decrypted: - format_device(decrypted, file_system) + sdm.mkfs(decrypted, file_system) with sdm.mounted_device(decrypted) as mounted: backup_repo = mounted / backup_repository_folder mkdir_repo: sh.StrPathList = ["sudo", "mkdir", backup_repo] diff --git a/tests/test_cli.py b/tests/test_cli.py index be7d76e..f2876bd 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -339,12 +339,8 @@ def test_format_device_creates_expected_file_system( assert len(config_lst) == 1 config = config_lst[0] with sdm.decrypted_device(big_file, config.DevicePassCmd) as decrypted: - blkid_result = sh.run_cmd( - cmd=["sudo", "blkid", "-o", "value", "-s", "TYPE", str(decrypted)], - capture_output=True, - ) - actual_fs = blkid_result.stdout.decode().strip() - assert actual_fs == file_system + result_fs = sdm.get_filesystem(decrypted) + assert result_fs == file_system @pytest.mark.parametrize("backend", ["restic", "btrfs-rsync"]) diff --git a/uv.lock b/uv.lock index 2962c3d..535229a 100644 --- a/uv.lock +++ b/uv.lock @@ -60,7 +60,7 @@ requires-dist = [ { name = "pyyaml", specifier = ">=6.0.3" }, { name = "rich", marker = "extra == 'all'", specifier = ">=13.9.4" }, { name = "shell-interface", specifier = ">=1.0.2" }, - { name = "storage-device-managers", specifier = ">=1.0.2" }, + { name = "storage-device-managers", git = "https://github.com/MaxG87/storage-device-managers.git?rev=bee3454bb8086313a2bd47a741c320d9ee9589b7" }, { name = "typer", specifier = ">=0.24.1" }, ] provides-extras = ["all"] @@ -666,15 +666,6 @@ version = "6.0.3" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" }, - { url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" }, - { url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" }, - { url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" }, - { url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" }, - { url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" }, - { url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" }, - { url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" }, { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" }, { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" }, { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" }, @@ -764,11 +755,11 @@ wheels = [ [[package]] name = "shell-interface" -version = "1.0.2" +version = "2.0.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a0/8f/648efc24a3cf77ad2161c8379dcd604e165b3147925d554359d65d42b2e1/shell_interface-1.0.2.tar.gz", hash = "sha256:7120dce6fcf11ab45b2cd24fbcc881666a1861c1b88f53773ec3702646b26856", size = 15331, upload-time = "2026-01-10T11:31:42.136Z" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/4d/3339a6f734bc1a21f663cf501fe8f525e57de7110b817a3a8993fb89c4ec/shell_interface-2.0.0.tar.gz", hash = "sha256:bf78d220a7c7fb9a699d1030db7ebc4945829f8f312ce0c130dea95526870ed6", size = 15412, upload-time = "2026-04-26T11:20:44.052Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b6/7a/01f195b3b083f758c3ce797dcbf4f52603bcbdbe1b3397d52d8bb2f99d42/shell_interface-1.0.2-py3-none-any.whl", hash = "sha256:2e8fdd5bfaca5be19caf718122836f4b95b96a8efce4e8942790e350539d9a93", size = 16192, upload-time = "2026-01-10T11:31:40.928Z" }, + { url = "https://files.pythonhosted.org/packages/ca/aa/81ed389c0139ff5364c1b06015817cc7f44b84921237aa6fa6d33a5a99dc/shell_interface-2.0.0-py3-none-any.whl", hash = "sha256:9fcb39b0d680ec67731396f33179181c2c30fbd96c48d0da9c9886037931e676", size = 16293, upload-time = "2026-04-26T11:20:42.496Z" }, ] [[package]] @@ -792,15 +783,11 @@ wheels = [ [[package]] name = "storage-device-managers" version = "1.0.2" -source = { registry = "https://pypi.org/simple" } +source = { git = "https://github.com/MaxG87/storage-device-managers.git?rev=bee3454bb8086313a2bd47a741c320d9ee9589b7#bee3454bb8086313a2bd47a741c320d9ee9589b7" } dependencies = [ { name = "loguru" }, { name = "shell-interface" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e4/71/ba5fa0f430f798d71e7e694f2f3a379e0c1f1b21014f79484cbc7ee6417b/storage_device_managers-1.0.2.tar.gz", hash = "sha256:9284f00d6787168e8a1da75798cddc233ff178b412c7447290ba72f3a9658dbd", size = 18826, upload-time = "2026-04-15T19:15:02.33Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/b8/024f95dadeb006a354d207b56f5cc603009cf43e90eee29e2fa58e82d945/storage_device_managers-1.0.2-py3-none-any.whl", hash = "sha256:562295c8c2f7bb8edd5420d07ee03fc0928a7437847d08f5250eab5ac3983678", size = 19714, upload-time = "2026-04-15T19:15:01.215Z" }, -] [[package]] name = "tomli"