Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 23 additions & 20 deletions examples/json.cfg
Original file line number Diff line number Diff line change
@@ -1,20 +1,23 @@
[
{
"Name": "BtrFS Backup Example",
"UUID": "12345678-1234-5678-1234-567812345678",
"DevicePassCmd": "echo device-password",
"BackupRepositoryFolder": "ButterBackupRepo",
"Compression": "zstd:3",
"Folders": { "/tmp": "temp-files" },
"Files": [],
"FilesDest": "single-files"
},
{
"Name": "Restic Backup Example",
"UUID": "87654321-4321-8765-4321-876543218765",
"DevicePassCmd": "echo device-password",
"BackupRepositoryFolder": "ResticRepo",
"RepositoryPassCmd": "echo repo-password",
"FilesAndFolders": ["/tmp"]
}
]
{
"SudoPassCmd": "gpg --decrypt ~/.local/share/passwords/sudo-password.gpg",
"DeviceConfigurations": [
{
"Name": "BtrFS Backup Example",
"UUID": "12345678-1234-5678-1234-567812345678",
"DevicePassCmd": "echo device-password",
"BackupRepositoryFolder": "ButterBackupRepo",
"Compression": "zstd:3",
"Folders": { "/tmp": "temp-files" },
"Files": [],
"FilesDest": "single-files"
},
{
"Name": "Restic Backup Example",
"UUID": "87654321-4321-8765-4321-876543218765",
"DevicePassCmd": "echo device-password",
"BackupRepositoryFolder": "ResticRepo",
"RepositoryPassCmd": "echo repo-password",
"FilesAndFolders": ["/tmp"]
}
]
}
43 changes: 23 additions & 20 deletions examples/json5.cfg
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
// JSON5 allows comments and trailing commas
[
{
Name: "BtrFS Backup Example",
UUID: "12345678-1234-5678-1234-567812345678",
DevicePassCmd: "echo device-password",
BackupRepositoryFolder: "ButterBackupRepo",
Compression: "zstd:3",
Folders: { "/tmp": "temp-files" },
Files: [],
FilesDest: "single-files",
},
{
Name: "Restic Backup Example",
UUID: "87654321-4321-8765-4321-876543218765",
DevicePassCmd: "echo device-password",
BackupRepositoryFolder: "ResticRepo",
RepositoryPassCmd: "echo repo-password",
FilesAndFolders: ["/tmp"],
},
]
{
SudoPassCmd: "gpg --decrypt ~/.local/share/passwords/sudo-password.gpg",
DeviceConfigurations: [
{
Name: "BtrFS Backup Example",
UUID: "12345678-1234-5678-1234-567812345678",
DevicePassCmd: "echo device-password",
BackupRepositoryFolder: "ButterBackupRepo",
Compression: "zstd:3",
Folders: { "/tmp": "temp-files" },
Files: [],
FilesDest: "single-files",
},
{
Name: "Restic Backup Example",
UUID: "87654321-4321-8765-4321-876543218765",
DevicePassCmd: "echo device-password",
BackupRepositoryFolder: "ResticRepo",
RepositoryPassCmd: "echo repo-password",
FilesAndFolders: ["/tmp"],
},
],
}
11 changes: 7 additions & 4 deletions examples/toml.cfg
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
# TOML configuration for butter-backup
# Each [[DEVICE_CONFIGURATION]] section defines one device configuration
# Each [[butter-backup.device-configurations]] section defines one device configuration

[[DEVICE_CONFIGURATION]]
[butter-backup]
SudoPassCmd = "gpg --decrypt ~/.local/share/passwords/sudo-password.gpg"

[[butter-backup.device-configurations]]
Name = "BtrFS Backup Example"
UUID = "12345678-1234-5678-1234-567812345678"
DevicePassCmd = "echo device-password"
Expand All @@ -10,10 +13,10 @@ Compression = "zstd:3"
Files = []
FilesDest = "single-files"

[DEVICE_CONFIGURATION.Folders]
[butter-backup.device-configurations.Folders]
"/tmp" = "temp-files"

[[DEVICE_CONFIGURATION]]
[[butter-backup.device-configurations]]
Name = "Restic Backup Example"
UUID = "87654321-4321-8765-4321-876543218765"
DevicePassCmd = "echo device-password"
Expand Down
34 changes: 18 additions & 16 deletions examples/yaml.cfg
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
---
- Name: "BtrFS Backup Example"
UUID: "12345678-1234-5678-1234-567812345678"
DevicePassCmd: "echo device-password"
BackupRepositoryFolder: "ButterBackupRepo"
Compression: "zstd:3"
Folders:
/tmp: "temp-files"
Files: []
FilesDest: "single-files"
SudoPassCmd: "gpg --decrypt ~/.local/share/passwords/sudo-password.gpg"
DeviceConfigurations:
- Name: "BtrFS Backup Example"
UUID: "12345678-1234-5678-1234-567812345678"
DevicePassCmd: "echo device-password"
BackupRepositoryFolder: "ButterBackupRepo"
Compression: "zstd:3"
Folders:
/tmp: "temp-files"
Files: []
FilesDest: "single-files"

- Name: "Restic Backup Example"
UUID: "87654321-4321-8765-4321-876543218765"
DevicePassCmd: "echo device-password"
BackupRepositoryFolder: "ResticRepo"
RepositoryPassCmd: "echo repo-password"
FilesAndFolders:
- /tmp
- Name: "Restic Backup Example"
UUID: "87654321-4321-8765-4321-876543218765"
DevicePassCmd: "echo device-password"
BackupRepositoryFolder: "ResticRepo"
RepositoryPassCmd: "echo repo-password"
FilesAndFolders:
- /tmp
18 changes: 15 additions & 3 deletions src/butter_backup/backup_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,16 @@
from . import config_parser as cp


def _refresh_sudo(sudo_pass_cmd: str | None) -> None:
if sudo_pass_cmd is not None:
sh.pipe_pass_cmd_to_real_cmd(
sudo_pass_cmd, ["sudo", "-Sv"], capture_output=True
)


class BackupBackend(abc.ABC):
@abc.abstractmethod
def do_backup(self, mount_dir: Path) -> None: ...
def do_backup(self, mount_dir: Path, sudo_pass_cmd: str | None = None) -> None: ...

@overload
@staticmethod
Expand All @@ -37,25 +44,28 @@ def from_config(
class BtrFSRsyncBackend(BackupBackend):
config: cp.BtrFSRsyncConfig

def do_backup(self, mount_dir: Path) -> None:
def do_backup(self, mount_dir: Path, sudo_pass_cmd: str | None = None) -> None:
logger.info(f"Beginne mit BtrFS-Backup für Speichermedium {self.config.Name}.")
backup_repository = mount_dir / self.config.BackupRepositoryFolder
src_snapshot = self.get_source_snapshot(backup_repository)
logger.info(f"Basis-Sicherungskopie: {src_snapshot}.")
backup_root = self.snapshot(
src=src_snapshot, backup_repository=backup_repository
)
_refresh_sudo(sudo_pass_cmd)
self.adapt_ownership(backup_root)

for src, dest_name in self.config.Folders.items():
dest = backup_root / dest_name
_refresh_sudo(sudo_pass_cmd)
self.rsync_folder(src, dest, self.config.ExcludePatternsFile)

files_dest = backup_root / self.config.FilesDest
if files_dest.is_file():
files_dest.unlink()
files_dest.mkdir(parents=True, exist_ok=True)
for src in self.config.Files:
_refresh_sudo(sudo_pass_cmd)
self.rsync_file(src, files_dest)

@staticmethod
Expand Down Expand Up @@ -129,10 +139,12 @@ def rsync_folder(
class ResticBackend(BackupBackend):
config: cp.ResticConfig

def do_backup(self, mount_dir: Path) -> None:
def do_backup(self, mount_dir: Path, sudo_pass_cmd: str | None = None) -> None:
logger.info(f"Beginne mit Restic-Backup für Speichermedium {self.config.Name}.")
backup_repository = mount_dir / self.config.BackupRepositoryFolder
_refresh_sudo(sudo_pass_cmd)
self.copy_files(backup_repository)
_refresh_sudo(sudo_pass_cmd)
self.adapt_ownership(backup_repository)

@staticmethod
Expand Down
46 changes: 32 additions & 14 deletions src/butter_backup/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from tempfile import mkdtemp
from typing import Any, Callable

import shell_interface as sh
import storage_device_managers as sdm
import typer
from loguru import logger
Expand Down Expand Up @@ -70,11 +71,18 @@ def _get_default_file_system(backend: ValidBackends) -> ValidFileSystems:
t.assert_never(backend)


def _refresh_sudo(sudo_pass_cmd: str | None) -> None:
if sudo_pass_cmd is not None:
sh.pipe_pass_cmd_to_real_cmd(
sudo_pass_cmd, ["sudo", "-Sv"], capture_output=True
)


def _skip_device(
config: cp.Configuration,
config: cp.DeviceConfiguration,
*,
log_missing: Callable[[cp.Configuration], None] | None = None,
log_opened: Callable[[cp.Configuration], None] | None = None,
log_missing: Callable[[cp.DeviceConfiguration], None] | None = None,
log_opened: Callable[[cp.DeviceConfiguration], None] | None = None,
) -> bool:
"""
Helper function to determine whether a device should be skipped.
Expand Down Expand Up @@ -102,10 +110,13 @@ def _skip_device(
VERBOSITY_OPTION = typer.Option(0, "--verbose", "-v", count=True)


def _open_device(cfg: cp.Configuration, base_dir: Path) -> None:
def _open_device(
cfg: cp.DeviceConfiguration, base_dir: Path, sudo_pass_cmd: str | None
) -> None:
mount_dir = base_dir / cfg.Name
mount_dir.mkdir(exist_ok=True)
try:
_refresh_sudo(sudo_pass_cmd)
decrypted = sdm.open_encrypted_device(cfg.device(), cfg.DevicePassCmd)
sdm.mount_device(decrypted, mount_dir=mount_dir, compression=cfg.compression())
except:
Expand Down Expand Up @@ -143,17 +154,17 @@ def open( # noqa: A001
Andernfalls wird ein temporäres Verzeichnis erstellt.
"""
setup_logging(verbose)
configurations = cp.parse_configuration(config.read_text())
parsed_config = cp.parse_configuration(config.read_text())
base_dir = dest if dest is not None else Path(mkdtemp())
for cfg in configurations:
for cfg in parsed_config.DeviceConfigurations:
if _skip_device(
cfg,
log_opened=lambda cfg: logger.warning(
f"Speichermedium {cfg.Name} ist bereits geöffnet. Es wird übersprungen."
),
):
continue
_open_device(cfg, base_dir)
_open_device(cfg, base_dir, parsed_config.SudoPassCmd)


@app.command()
Expand All @@ -166,9 +177,9 @@ def close(config: Path = CONFIG_OPTION, verbose: int = VERBOSITY_OPTION) -> None
`open`. Weitere Erklärungen finden sich dort.
"""
setup_logging(verbose)
configurations = cp.parse_configuration(config.read_text())
parsed_config = cp.parse_configuration(config.read_text())
mounted_devices = sdm.get_mounted_devices()
for cfg in configurations:
for cfg in parsed_config.DeviceConfigurations:
map_name = cfg.map_name()
map_name_as_str = str(map_name)
if cfg.device().exists() and map_name_as_str in mounted_devices:
Expand All @@ -182,6 +193,7 @@ def close(config: Path = CONFIG_OPTION, verbose: int = VERBOSITY_OPTION) -> None
device=cfg.Name,
)
continue
_refresh_sudo(parsed_config.SudoPassCmd)
sdm.unmount_device(map_name)
sdm.close_decrypted_device(map_name)

Expand All @@ -207,8 +219,8 @@ def backup(config: Path = CONFIG_OPTION, verbose: int = VERBOSITY_OPTION) -> Non
weitere manuelle Schritte sind nicht nötig.
"""
setup_logging(verbose)
configurations = cp.parse_configuration(config.read_text())
for cfg in configurations:
parsed_config = cp.parse_configuration(config.read_text())
for cfg in parsed_config.DeviceConfigurations:
if _skip_device(
cfg,
log_missing=lambda cfg: logger.info(
Expand All @@ -220,9 +232,14 @@ def backup(config: Path = CONFIG_OPTION, verbose: int = VERBOSITY_OPTION) -> Non
):
continue
backend = bb.BackupBackend.from_config(cfg)
_refresh_sudo(parsed_config.SudoPassCmd)
with sdm.decrypted_device(cfg.device(), cfg.DevicePassCmd) as decrypted:
with sdm.mounted_device(decrypted, cfg.compression()) as mount_dir:
backend.do_backup(mount_dir)
backend.do_backup(mount_dir, parsed_config.SudoPassCmd)
# A backup could take so long that the sudo session expires. In this
# case the user would have to enter the password again to unmount and
# close the device. To prevent this, the sudo session is refreshed.
_refresh_sudo(parsed_config.SudoPassCmd)


@app.command()
Expand Down Expand Up @@ -278,7 +295,7 @@ def format_device(
"Zieldatei für ButterBackup-Konfiguration existiert schon!"
)
config_writer = config_to.write_text
config: cp.Configuration
config: cp.DeviceConfiguration
match backend:
case ValidBackends.btrfs_rsync:
config = prepare_device_for_butterbackend(device)
Expand All @@ -287,7 +304,8 @@ def format_device(
case _:
t.assert_never(backend)
json_serialisable = json.loads(config.model_dump_json(exclude_none=True))
config_writer(json.dumps([json_serialisable], indent=4, sort_keys=True))
wrapper = {"DeviceConfigurations": [json_serialisable]}
config_writer(json.dumps(wrapper, indent=4, sort_keys=True))


@app.command()
Expand Down
Loading
Loading