backup-docker-to-local 1.0.0__tar.gz → 1.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (32) hide show
  1. {backup_docker_to_local-1.0.0/src/backup_docker_to_local.egg-info → backup_docker_to_local-1.1.0}/PKG-INFO +1 -1
  2. {backup_docker_to_local-1.0.0 → backup_docker_to_local-1.1.0}/pyproject.toml +1 -1
  3. {backup_docker_to_local-1.0.0 → backup_docker_to_local-1.1.0/src/backup_docker_to_local.egg-info}/PKG-INFO +1 -1
  4. {backup_docker_to_local-1.0.0 → backup_docker_to_local-1.1.0}/src/baudolo/backup/app.py +6 -2
  5. {backup_docker_to_local-1.0.0 → backup_docker_to_local-1.1.0}/src/baudolo/backup/cli.py +3 -19
  6. {backup_docker_to_local-1.0.0 → backup_docker_to_local-1.1.0}/src/baudolo/backup/compose.py +3 -1
  7. {backup_docker_to_local-1.0.0 → backup_docker_to_local-1.1.0}/src/baudolo/backup/db.py +12 -3
  8. {backup_docker_to_local-1.0.0 → backup_docker_to_local-1.1.0}/src/baudolo/backup/docker.py +3 -1
  9. {backup_docker_to_local-1.0.0 → backup_docker_to_local-1.1.0}/src/baudolo/backup/volume.py +6 -2
  10. backup_docker_to_local-1.1.0/src/baudolo/restore/__init__.py +1 -0
  11. {backup_docker_to_local-1.0.0 → backup_docker_to_local-1.1.0}/src/baudolo/restore/__main__.py +3 -1
  12. backup_docker_to_local-1.1.0/src/baudolo/restore/db/__init__.py +1 -0
  13. {backup_docker_to_local-1.0.0 → backup_docker_to_local-1.1.0}/src/baudolo/restore/db/mariadb.py +22 -4
  14. {backup_docker_to_local-1.0.0 → backup_docker_to_local-1.1.0}/src/baudolo/restore/files.py +3 -1
  15. {backup_docker_to_local-1.0.0 → backup_docker_to_local-1.1.0}/src/baudolo/seed/__main__.py +29 -11
  16. backup_docker_to_local-1.0.0/src/baudolo/restore/__init__.py +0 -1
  17. backup_docker_to_local-1.0.0/src/baudolo/restore/db/__init__.py +0 -1
  18. {backup_docker_to_local-1.0.0 → backup_docker_to_local-1.1.0}/LICENSE +0 -0
  19. {backup_docker_to_local-1.0.0 → backup_docker_to_local-1.1.0}/README.md +0 -0
  20. {backup_docker_to_local-1.0.0 → backup_docker_to_local-1.1.0}/setup.cfg +0 -0
  21. {backup_docker_to_local-1.0.0 → backup_docker_to_local-1.1.0}/src/backup_docker_to_local.egg-info/SOURCES.txt +0 -0
  22. {backup_docker_to_local-1.0.0 → backup_docker_to_local-1.1.0}/src/backup_docker_to_local.egg-info/dependency_links.txt +0 -0
  23. {backup_docker_to_local-1.0.0 → backup_docker_to_local-1.1.0}/src/backup_docker_to_local.egg-info/entry_points.txt +0 -0
  24. {backup_docker_to_local-1.0.0 → backup_docker_to_local-1.1.0}/src/backup_docker_to_local.egg-info/requires.txt +0 -0
  25. {backup_docker_to_local-1.0.0 → backup_docker_to_local-1.1.0}/src/backup_docker_to_local.egg-info/top_level.txt +0 -0
  26. {backup_docker_to_local-1.0.0 → backup_docker_to_local-1.1.0}/src/baudolo/__init__.py +0 -0
  27. {backup_docker_to_local-1.0.0 → backup_docker_to_local-1.1.0}/src/baudolo/backup/__init__.py +0 -0
  28. {backup_docker_to_local-1.0.0 → backup_docker_to_local-1.1.0}/src/baudolo/backup/__main__.py +0 -0
  29. {backup_docker_to_local-1.0.0 → backup_docker_to_local-1.1.0}/src/baudolo/backup/shell.py +0 -0
  30. {backup_docker_to_local-1.0.0 → backup_docker_to_local-1.1.0}/src/baudolo/restore/db/postgres.py +0 -0
  31. {backup_docker_to_local-1.0.0 → backup_docker_to_local-1.1.0}/src/baudolo/restore/paths.py +0 -0
  32. {backup_docker_to_local-1.0.0 → backup_docker_to_local-1.1.0}/src/baudolo/restore/run.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: backup-docker-to-local
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: Backup Docker volumes to local with rsync and optional DB dumps.
5
5
  Author: Kevin Veen-Birkenbach
6
6
  License: AGPL-3.0-or-later
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "backup-docker-to-local"
7
- version = "1.0.0"
7
+ version = "1.1.0"
8
8
  description = "Backup Docker volumes to local with rsync and optional DB dumps."
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.9"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: backup-docker-to-local
3
- Version: 1.0.0
3
+ Version: 1.1.0
4
4
  Summary: Backup Docker volumes to local with rsync and optional DB dumps.
5
5
  Author: Kevin Veen-Birkenbach
6
6
  License: AGPL-3.0-or-later
@@ -51,7 +51,9 @@ def is_image_ignored(container: str, images_no_backup_required: list[str]) -> bo
51
51
  return any(pat in img for pat in images_no_backup_required)
52
52
 
53
53
 
54
- def volume_is_fully_ignored(containers: list[str], images_no_backup_required: list[str]) -> bool:
54
+ def volume_is_fully_ignored(
55
+ containers: list[str], images_no_backup_required: list[str]
56
+ ) -> bool:
55
57
  """
56
58
  Skip file backup only if all containers linked to the volume are ignored.
57
59
  """
@@ -178,6 +180,8 @@ def main() -> int:
178
180
  print("Finished volume backups.", flush=True)
179
181
 
180
182
  print("Handling Docker Compose services...", flush=True)
181
- handle_docker_compose_services(args.compose_dir, args.docker_compose_hard_restart_required)
183
+ handle_docker_compose_services(
184
+ args.compose_dir, args.docker_compose_hard_restart_required
185
+ )
182
186
 
183
187
  return 0
@@ -2,22 +2,6 @@ from __future__ import annotations
2
2
 
3
3
  import argparse
4
4
  import os
5
- from pathlib import Path
6
-
7
-
8
- def _default_repo_name() -> str:
9
- """
10
- Derive the repository name from the folder that contains `src/`.
11
-
12
- Expected layout:
13
- <repo-root>/src/baudolo/backup/cli.py
14
-
15
- => parents[0]=backup, [1]=baudolo, [2]=src, [3]=repo-root
16
- """
17
- try:
18
- return Path(__file__).resolve().parents[3].name
19
- except Exception:
20
- return "backup-docker-to-local"
21
5
 
22
6
 
23
7
  def parse_args() -> argparse.Namespace:
@@ -41,7 +25,7 @@ def parse_args() -> argparse.Namespace:
41
25
 
42
26
  p.add_argument(
43
27
  "--repo-name",
44
- default=_default_repo_name(),
28
+ default="backup-docker-to-local",
45
29
  help="Backup repo folder name under <backups-dir>/<machine-id>/ (default: git repo folder name)",
46
30
  )
47
31
  p.add_argument(
@@ -51,8 +35,8 @@ def parse_args() -> argparse.Namespace:
51
35
  )
52
36
  p.add_argument(
53
37
  "--backups-dir",
54
- default="/Backups",
55
- help="Backup root directory (default: /Backups)",
38
+ default="/var/lib/backup/",
39
+ help="Backup root directory (default: /var/lib/backup/)",
56
40
  )
57
41
 
58
42
  p.add_argument(
@@ -10,7 +10,9 @@ def hard_restart_docker_services(dir_path: str) -> None:
10
10
  subprocess.run(["docker-compose", "up", "-d"], cwd=dir_path, check=True)
11
11
 
12
12
 
13
- def handle_docker_compose_services(parent_directory: str, hard_restart_required: list[str]) -> None:
13
+ def handle_docker_compose_services(
14
+ parent_directory: str, hard_restart_required: list[str]
15
+ ) -> None:
14
16
  for entry in os.scandir(parent_directory):
15
17
  if not entry.is_dir():
16
18
  continue
@@ -5,9 +5,12 @@ import pathlib
5
5
  import re
6
6
 
7
7
  import pandas
8
+ import logging
8
9
 
9
10
  from .shell import BackupException, execute_shell_command
10
11
 
12
+ log = logging.getLogger(__name__)
13
+
11
14
 
12
15
  def get_instance(container: str, database_containers: list[str]) -> str:
13
16
  if container in database_containers:
@@ -15,7 +18,9 @@ def get_instance(container: str, database_containers: list[str]) -> str:
15
18
  return re.split(r"(_|-)(database|db|postgres)", container)[0]
16
19
 
17
20
 
18
- def fallback_pg_dumpall(container: str, username: str, password: str, out_file: str) -> None:
21
+ def fallback_pg_dumpall(
22
+ container: str, username: str, password: str, out_file: str
23
+ ) -> None:
19
24
  cmd = (
20
25
  f"PGPASSWORD={password} docker exec -i {container} "
21
26
  f"pg_dumpall -U {username} -h localhost > {out_file}"
@@ -34,7 +39,8 @@ def backup_database(
34
39
  instance_name = get_instance(container, database_containers)
35
40
  entries = databases_df.loc[databases_df["instance"] == instance_name]
36
41
  if entries.empty:
37
- raise BackupException(f"No entry found for instance '{instance_name}'")
42
+ log.warning("No entry found for instance '%s'", instance_name)
43
+ return
38
44
 
39
45
  out_dir = os.path.join(volume_dir, "sql")
40
46
  pathlib.Path(out_dir).mkdir(parents=True, exist_ok=True)
@@ -68,6 +74,9 @@ def backup_database(
68
74
  execute_shell_command(cmd)
69
75
  except BackupException as e:
70
76
  print(f"pg_dump failed: {e}", flush=True)
71
- print(f"Falling back to pg_dumpall for instance '{instance_name}'", flush=True)
77
+ print(
78
+ f"Falling back to pg_dumpall for instance '{instance_name}'",
79
+ flush=True,
80
+ )
72
81
  fallback_pg_dumpall(container, user, password, cluster_file)
73
82
  continue
@@ -37,7 +37,9 @@ def change_containers_status(containers: list[str], status: str) -> None:
37
37
  def docker_volume_exists(volume: str) -> bool:
38
38
  # Avoid throwing exceptions for exists checks.
39
39
  try:
40
- execute_shell_command(f"docker volume inspect {volume} >/dev/null 2>&1 && echo OK")
40
+ execute_shell_command(
41
+ f"docker volume inspect {volume} >/dev/null 2>&1 && echo OK"
42
+ )
41
43
  return True
42
44
  except Exception:
43
45
  return False
@@ -13,7 +13,9 @@ def get_storage_path(volume_name: str) -> str:
13
13
  return f"{path}/"
14
14
 
15
15
 
16
- def get_last_backup_dir(versions_dir: str, volume_name: str, current_backup_dir: str) -> str | None:
16
+ def get_last_backup_dir(
17
+ versions_dir: str, volume_name: str, current_backup_dir: str
18
+ ) -> str | None:
17
19
  versions = sorted(os.listdir(versions_dir), reverse=True)
18
20
  for version in versions:
19
21
  candidate = os.path.join(versions_dir, version, volume_name, "files", "")
@@ -37,6 +39,8 @@ def backup_volume(versions_dir: str, volume_name: str, volume_dir: str) -> None:
37
39
  execute_shell_command(cmd)
38
40
  except BackupException as e:
39
41
  if "file has vanished" in str(e):
40
- print("Warning: Some files vanished before transfer. Continuing.", flush=True)
42
+ print(
43
+ "Warning: Some files vanished before transfer. Continuing.", flush=True
44
+ )
41
45
  else:
42
46
  raise
@@ -0,0 +1 @@
1
+ __all__ = ["main"]
@@ -66,7 +66,9 @@ def main(argv: list[str] | None = None) -> int:
66
66
  # ------------------------------------------------------------------
67
67
  # mariadb
68
68
  # ------------------------------------------------------------------
69
- p_mdb = sub.add_parser("mariadb", help="Restore a single MariaDB/MySQL-compatible dump")
69
+ p_mdb = sub.add_parser(
70
+ "mariadb", help="Restore a single MariaDB/MySQL-compatible dump"
71
+ )
70
72
  _add_common_backup_args(p_mdb)
71
73
  p_mdb.add_argument("--container", required=True)
72
74
  p_mdb.add_argument("--db-name", required=True)
@@ -0,0 +1 @@
1
+ """Database restore handlers (Postgres, MariaDB/MySQL)."""
@@ -23,7 +23,9 @@ exit 42
23
23
  raise RuntimeError("empty client detection output")
24
24
  return out
25
25
  except Exception as e:
26
- print("ERROR: neither 'mariadb' nor 'mysql' found in container.", file=sys.stderr)
26
+ print(
27
+ "ERROR: neither 'mariadb' nor 'mysql' found in container.", file=sys.stderr
28
+ )
27
29
  raise e
28
30
 
29
31
 
@@ -47,7 +49,14 @@ def restore_mariadb_sql(
47
49
  # MariaDB 11 images may not contain the mysql binary at all.
48
50
  docker_exec(
49
51
  container,
50
- [client, "-u", user, f"--password={password}", "-e", "SET FOREIGN_KEY_CHECKS=0;"],
52
+ [
53
+ client,
54
+ "-u",
55
+ user,
56
+ f"--password={password}",
57
+ "-e",
58
+ "SET FOREIGN_KEY_CHECKS=0;",
59
+ ],
51
60
  )
52
61
 
53
62
  result = docker_exec(
@@ -80,10 +89,19 @@ def restore_mariadb_sql(
80
89
 
81
90
  docker_exec(
82
91
  container,
83
- [client, "-u", user, f"--password={password}", "-e", "SET FOREIGN_KEY_CHECKS=1;"],
92
+ [
93
+ client,
94
+ "-u",
95
+ user,
96
+ f"--password={password}",
97
+ "-e",
98
+ "SET FOREIGN_KEY_CHECKS=1;",
99
+ ],
84
100
  )
85
101
 
86
102
  with open(sql_path, "rb") as f:
87
- docker_exec(container, [client, "-u", user, f"--password={password}", db_name], stdin=f)
103
+ docker_exec(
104
+ container, [client, "-u", user, f"--password={password}", db_name], stdin=f
105
+ )
88
106
 
89
107
  print(f"MariaDB/MySQL restore complete for db '{db_name}'.")
@@ -6,7 +6,9 @@ import sys
6
6
  from .run import run, docker_volume_exists
7
7
 
8
8
 
9
- def restore_volume_files(volume_name: str, backup_files_dir: str, *, rsync_image: str) -> int:
9
+ def restore_volume_files(
10
+ volume_name: str, backup_files_dir: str, *, rsync_image: str
11
+ ) -> int:
10
12
  if not os.path.isdir(backup_files_dir):
11
13
  print(f"ERROR: backup files dir not found: {backup_files_dir}", file=sys.stderr)
12
14
  return 2
@@ -2,21 +2,24 @@ import pandas as pd
2
2
  import argparse
3
3
  import os
4
4
 
5
+
5
6
  def check_and_add_entry(file_path, instance, database, username, password):
6
7
  # Check if the file exists and is not empty
7
8
  if os.path.exists(file_path) and os.path.getsize(file_path) > 0:
8
9
  # Read the existing CSV file with header
9
- df = pd.read_csv(file_path, sep=';')
10
+ df = pd.read_csv(file_path, sep=";")
10
11
  else:
11
12
  # Create a new DataFrame with columns if file does not exist
12
- df = pd.DataFrame(columns=['instance', 'database', 'username', 'password'])
13
+ df = pd.DataFrame(columns=["instance", "database", "username", "password"])
13
14
 
14
15
  # Check if the entry exists and remove it
15
16
  mask = (
16
- (df['instance'] == instance) &
17
- ((df['database'] == database) |
18
- (((df['database'].isna()) | (df['database'] == '')) & (database == ''))) &
19
- (df['username'] == username)
17
+ (df["instance"] == instance)
18
+ & (
19
+ (df["database"] == database)
20
+ | (((df["database"].isna()) | (df["database"] == "")) & (database == ""))
21
+ )
22
+ & (df["username"] == username)
20
23
  )
21
24
 
22
25
  if not df[mask].empty:
@@ -26,25 +29,40 @@ def check_and_add_entry(file_path, instance, database, username, password):
26
29
  print("Adding new entry.")
27
30
 
28
31
  # Create a new DataFrame for the new entry
29
- new_entry = pd.DataFrame([{'instance': instance, 'database': database, 'username': username, 'password': password}])
32
+ new_entry = pd.DataFrame(
33
+ [
34
+ {
35
+ "instance": instance,
36
+ "database": database,
37
+ "username": username,
38
+ "password": password,
39
+ }
40
+ ]
41
+ )
30
42
 
31
43
  # Add (or replace) the entry using concat
32
44
  df = pd.concat([df, new_entry], ignore_index=True)
33
45
 
34
46
  # Save the updated CSV file
35
- df.to_csv(file_path, sep=';', index=False)
47
+ df.to_csv(file_path, sep=";", index=False)
48
+
36
49
 
37
50
  def main():
38
- parser = argparse.ArgumentParser(description="Check and replace (or add) a database entry in a CSV file.")
51
+ parser = argparse.ArgumentParser(
52
+ description="Check and replace (or add) a database entry in a CSV file."
53
+ )
39
54
  parser.add_argument("file_path", help="Path to the CSV file")
40
55
  parser.add_argument("instance", help="Database instance")
41
56
  parser.add_argument("database", help="Database name")
42
57
  parser.add_argument("username", help="Username")
43
- parser.add_argument("password", nargs='?', default="", help="Password (optional)")
58
+ parser.add_argument("password", nargs="?", default="", help="Password (optional)")
44
59
 
45
60
  args = parser.parse_args()
46
61
 
47
- check_and_add_entry(args.file_path, args.instance, args.database, args.username, args.password)
62
+ check_and_add_entry(
63
+ args.file_path, args.instance, args.database, args.username, args.password
64
+ )
65
+
48
66
 
49
67
  if __name__ == "__main__":
50
68
  main()
@@ -1 +0,0 @@
1
- __all__ = ["main"]
@@ -1 +0,0 @@
1
- """Database restore handlers (Postgres, MariaDB/MySQL)."""