diff --git a/frigate/test/test_config_migration.py b/frigate/test/test_config_migration.py index c33aa95425..dd682c4e38 100644 --- a/frigate/test/test_config_migration.py +++ b/frigate/test/test_config_migration.py @@ -1,5 +1,6 @@ """Tests for migrating detectors and model into the models list.""" +import errno import logging import os import tempfile @@ -240,6 +241,30 @@ class TestMigrateConfigFile(unittest.TestCase): with open(self.config_file) as f: return YAML().load(f) + def test_a_failed_write_leaves_the_original_config(self): + original = "mqtt:\n enabled: false\ncameras: {}\n" + with open(self.config_file, "w") as f: + f.write(original) + + with ( + patch.object(YAML, "dump", side_effect=RuntimeError), + self.assertRaises(RuntimeError), + ): + migrate_frigate_config(self.config_file) + + with open(self.config_file) as f: + self.assertEqual(f.read(), original) + + def test_a_file_bind_mount_is_written_in_place(self): + with patch( + "frigate.util.config.os.replace", + side_effect=OSError(errno.EBUSY, "Device or resource busy"), + ): + migrated = self._migrate("mqtt:\n enabled: false\ncameras: {}\n") + + self.assertEqual(migrated["version"], CURRENT_CONFIG_VERSION) + self.assertFalse(os.path.exists(f"{self.config_file}.tmp")) + def test_migrates_a_config_already_stamped_with_the_current_version(self): # 0.19 is unreleased, so a dev config can be current and still use # the pre-models keys diff --git a/frigate/util/config.py b/frigate/util/config.py index 1555a903b4..7c83edf793 100644 --- a/frigate/util/config.py +++ b/frigate/util/config.py @@ -1,6 +1,8 @@ """configuration utils.""" import asyncio +import contextlib +import errno import ipaddress import logging import os @@ -213,11 +215,24 @@ def find_config_file() -> str: def _write_config(config_file: str, yaml: YAML, config: dict[str, Any]) -> None: + st = os.stat(config_file) tmp_file = f"{config_file}.tmp" with open(tmp_file, "w") as f: yaml.dump(config, f) - shutil.copymode(config_file, tmp_file) - os.replace(tmp_file, config_file) + os.chmod(tmp_file, st.st_mode) + + with contextlib.suppress(PermissionError): + os.chown(tmp_file, st.st_uid, st.st_gid) + + try: + os.replace(tmp_file, config_file) + except OSError as e: + if e.errno != errno.EBUSY: + raise + + # a file bind mount can't be renamed over, so write it in place + shutil.copyfile(tmp_file, config_file) + os.remove(tmp_file) def migrate_frigate_config(config_file: str):