From dd46ad6d3c2c2dd42a46d22888db04cbe2917753 Mon Sep 17 00:00:00 2001 From: Eddy de Vink Date: Thu, 15 Oct 2026 20:09:55 +0200 Subject: [PATCH] fix: kopieer met shutil.copy i.p.v. copy2 (EPERM op exfat voor niet-eigenaar) copy2 preserveert timestamps via os.utime; de exfat-SSD draait met uid=1000,allow_utime=0022, waardoor een niet-eigenaar (service-gebruiker aiaiaiaiagent sinds 2026-10-13, voorheen root) geen willekeurige tijdsstempels mag zetten -> EPERM op elke kopie. Mode is op exfat altijd 0777 en het opnamemoment staat al in de bestandsnaam. --- README.md | 5 +++++ dashcam-transfer.py | 7 ++++++- tests/test_dashcam_transfer.py | 14 +++++++------- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index a96c1ff..d5cb336 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,11 @@ SD-kaartlezer die midden in de transfer offline klapt, `I/O error`, `device offl verwijderd, wordt eerst een bestand van de kaart gelezen. Een onleesbare kaart resulteert in een duidelijke Matrix-"GESTOPT" en **niets** wordt gewist. - **Retry per bestand** — 3 pogingen met 2s pauze; vangt transiënte USB-glitches. +- **`shutil.copy` i.p.v. `copy2`** — `copy2` preserveert tijdsstempels via `os.utime`; + op de exfat-SSD (`uid=1000,allow_utime=0022`) weigert dat voor niet-eigenaars + (EPERM). Sinds de service als `aiaiaiaiagent` draait (werd voorheen als root + gedraaid) faalde daardoor elke kopie. Mode is op exfat altijd 0777 en het + opnamemoment staat al in de bestandsnaam — metadata-preserve voegt niets toe. - **Early-abort** — bij 5 opeenvolgende fouten stopt het script en stuurt het een Matrix-bericht met de laatste fout. Bronbestanden worden nooit verwijderd zonder geverifieerde kopie. diff --git a/dashcam-transfer.py b/dashcam-transfer.py index 7c106d8..199118c 100644 --- a/dashcam-transfer.py +++ b/dashcam-transfer.py @@ -153,7 +153,12 @@ def copy_file(src, dst, attempts=MAX_RETRIES, delay=RETRY_DELAY): last_error = None for attempt in range(1, attempts + 1): try: - shutil.copy2(src, dst) + # copy (niet copy2): copy2 preserveert timestamps via os.utime, en exfat + # mountuid=1000 + allow_utime=0022 weigeren dat voor niet-eigenaars — + # daarmee faalde elke kopie toen de service als aiaiaiaiagent ging + # draaien (EPERM). Op exfat is mode altijd 0777 en staat het + # opnamemoment al in de bestandsnaam (YYYYMMDDHHMMSS). + shutil.copy(src, dst) return except OSError as e: last_error = e diff --git a/tests/test_dashcam_transfer.py b/tests/test_dashcam_transfer.py index c8c32d2..c108aff 100644 --- a/tests/test_dashcam_transfer.py +++ b/tests/test_dashcam_transfer.py @@ -85,22 +85,22 @@ class CopyFileTest(unittest.TestCase): self.assertEqual(os.path.getsize(self.dst), 100) def test_retries_then_succeeds(self): - real_copy2 = dashcam.shutil.copy2 + real_copy = dashcam.shutil.copy calls = {"n": 0} def flaky(s, d): calls["n"] += 1 if calls["n"] < 3: raise OSError(errno.EIO, "I/O error") - real_copy2(s, d) + real_copy(s, d) - with mock.patch.object(dashcam.shutil, "copy2", side_effect=flaky): + with mock.patch.object(dashcam.shutil, "copy", side_effect=flaky): dashcam.copy_file(self.src, self.dst, attempts=3, delay=0) self.assertEqual(calls["n"], 3) self.assertEqual(os.path.getsize(self.dst), 100) def test_raises_after_exhausting_attempts(self): - with mock.patch.object(dashcam.shutil, "copy2", side_effect=OSError(errno.EIO, "I/O error")): + with mock.patch.object(dashcam.shutil, "copy", side_effect=OSError(errno.EIO, "I/O error")): with self.assertRaises(OSError): dashcam.copy_file(self.src, self.dst, attempts=2, delay=0) @@ -155,11 +155,11 @@ class TransferFilesTest(unittest.TestCase): def test_verification_mismatch_counts_as_error_and_keeps_source(self): files = [self._make("20260622050346_0000001F.MP4", 100)] - def bad_copy2(s, d): + def bad_copy(s, d): with open(d, "wb") as f: f.write(b"0" * 50) - with mock.patch.object(dashcam.shutil, "copy2", side_effect=bad_copy2), mock.patch.object( + with mock.patch.object(dashcam.shutil, "copy", side_effect=bad_copy), mock.patch.object( dashcam.time, "sleep" ): ok, errors = dashcam.transfer_files(files, self.src, self.base) @@ -172,7 +172,7 @@ class TransferFilesTest(unittest.TestCase): for i in range(dashcam.MAX_CONSECUTIVE_ERRORS + 3) ] with mock.patch.object( - dashcam.shutil, "copy2", side_effect=OSError(errno.EIO, "I/O error") + dashcam.shutil, "copy", side_effect=OSError(errno.EIO, "I/O error") ), mock.patch.object(dashcam.time, "sleep"): with self.assertRaises(dashcam.TransferAborted) as ctx: dashcam.transfer_files(files, self.src, self.base)