diff --git a/src/baudolo/backup/compose.py b/src/baudolo/backup/compose.py index 6a2a011..b428206 100644 --- a/src/baudolo/backup/compose.py +++ b/src/baudolo/backup/compose.py @@ -4,7 +4,7 @@ import os import shutil import subprocess from pathlib import Path -from typing import List +from typing import List, Optional def _build_compose_cmd(project_dir: str, passthrough: List[str]) -> List[str]: @@ -30,6 +30,37 @@ def _build_compose_cmd(project_dir: str, passthrough: List[str]) -> List[str]: raise RuntimeError("Neither 'compose' nor 'docker' found in PATH") +def _find_compose_file(project_dir: str) -> Optional[Path]: + """ + Detect a compose file in `project_dir` (case-insensitive). + + Supported names: + - compose.yml / compose.yaml + - docker-compose.yml / docker-compose.yaml + """ + pdir = Path(project_dir) + if not pdir.is_dir(): + return None + + # Map lowercase filename -> actual Path (preserves original casing) + by_lower = {p.name.lower(): p for p in pdir.iterdir() if p.is_file()} + + # Preferred order (policy decision) + candidates = [ + "docker-compose.yml", + "docker-compose.yaml", + "compose.yml", + "compose.yaml", + ] + + for name in candidates: + found = by_lower.get(name) + if found is not None: + return found + + return None + + def hard_restart_docker_services(dir_path: str) -> None: print(f"Hard restart compose services in: {dir_path}", flush=True) @@ -44,7 +75,8 @@ def hard_restart_docker_services(dir_path: str) -> None: def handle_docker_compose_services( - parent_directory: str, hard_restart_required: list[str] + parent_directory: str, + hard_restart_required: list[str], ) -> None: for entry in os.scandir(parent_directory): if not entry.is_dir(): @@ -52,11 +84,12 @@ def handle_docker_compose_services( dir_path = entry.path name = os.path.basename(dir_path) - compose_file = os.path.join(dir_path, "docker-compose.yml") print(f"Checking directory: {dir_path}", flush=True) - if not os.path.isfile(compose_file): - print("No docker-compose.yml found. Skipping.", flush=True) + + compose_file = _find_compose_file(dir_path) + if compose_file is None: + print("No supported compose file found. Skipping.", flush=True) continue if name in hard_restart_required: diff --git a/tests/unit/backup/test_compose.py b/tests/unit/backup/test_compose.py index d240dd1..bf6aa67 100644 --- a/tests/unit/backup/test_compose.py +++ b/tests/unit/backup/test_compose.py @@ -23,6 +23,7 @@ def _setup_compose_dir( tmp_path: Path, name: str = "mailu", *, + compose_name: str = "docker-compose.yml", with_override: bool = False, with_ca_override: bool = False, env_layout: str | None = None, # None | ".env" | ".env/env" @@ -30,7 +31,7 @@ def _setup_compose_dir( d = tmp_path / name d.mkdir(parents=True, exist_ok=True) - _touch(d / "docker-compose.yml") + _touch(d / compose_name) if with_override: _touch(d / "docker-compose.override.yml") @@ -53,11 +54,45 @@ class TestCompose(unittest.TestCase): cls.compose_mod = mod + def test_find_compose_file_supports_all_valid_names_case_insensitive(self) -> None: + with tempfile.TemporaryDirectory() as td: + tmp_path = Path(td) + + variants = [ + "compose.yml", + "compose.yaml", + "docker-compose.yml", + "docker-compose.yaml", + "docker-compose.yAml", + ] + + for i, name in enumerate(variants): + d = _setup_compose_dir( + tmp_path, + name=f"project{i}", + compose_name=name, + ) + found = self.compose_mod._find_compose_file(str(d)) + self.assertIsNotNone(found) + self.assertEqual(found.name, name) + + def test_find_compose_file_returns_none_when_missing(self) -> None: + with tempfile.TemporaryDirectory() as td: + tmp_path = Path(td) + d = tmp_path / "empty" + d.mkdir(parents=True, exist_ok=True) + + found = self.compose_mod._find_compose_file(str(d)) + self.assertIsNone(found) + def test_build_cmd_uses_wrapper_when_present(self) -> None: with tempfile.TemporaryDirectory() as td: tmp_path = Path(td) d = _setup_compose_dir( - tmp_path, with_override=True, with_ca_override=True, env_layout=".env" + tmp_path, + with_override=True, + with_ca_override=True, + env_layout=".env", ) def fake_which(name: str):