fix: kopieer met shutil.copyfile (data-only) i.p.v. copy/copy2
copy doet na copyfile ook copymode (os.chmod); op exfat met mountuid=1000 zijn alle bestanden eigendom van uid 1000 en wordt chmod/utime voor de niet-eigenaar service-gebruiker geweigerd (EPERM op elke kopie, ook na de copy2-fix). copyfile schrijft alleen data; verificatie gebeurt op grootte en mode/opnametijd voegen op exfat niets toe.
This commit is contained in:
parent
dd46ad6d3c
commit
69d08ba8ba
3 changed files with 19 additions and 16 deletions
12
README.md
12
README.md
|
|
@ -48,11 +48,13 @@ 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
|
verwijderd, wordt eerst een bestand van de kaart gelezen. Een onleesbare kaart
|
||||||
resulteert in een duidelijke Matrix-"GESTOPT" en **niets** wordt gewist.
|
resulteert in een duidelijke Matrix-"GESTOPT" en **niets** wordt gewist.
|
||||||
- **Retry per bestand** — 3 pogingen met 2s pauze; vangt transiënte USB-glitches.
|
- **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`;
|
- **`shutil.copyfile` i.p.v. `copy`/`copy2`** — `copy`/`copy2` doen na het kopiëren
|
||||||
op de exfat-SSD (`uid=1000,allow_utime=0022`) weigert dat voor niet-eigenaars
|
ook metadata-operaties op het doel (`chmod`, `utime`). Op de exfat-SSD
|
||||||
(EPERM). Sinds de service als `aiaiaiaiagent` draait (werd voorheen als root
|
(`uid=1000` → álle bestanden zijn eigendom van uid 1000, `allow_utime=0022`)
|
||||||
gedraaid) faalde daardoor elke kopie. Mode is op exfat altijd 0777 en het
|
worden die geweigerd voor een niet-eigenaar (EPERM); sinds de service als
|
||||||
opnamemoment staat al in de bestandsnaam — metadata-preserve voegt niets toe.
|
`aiaiaiaiagent` draait (voorheen root) faalde daardoor elke kopie. `copyfile`
|
||||||
|
schrijft alleen data; mode is op exfat altijd 0777 en het opnamemoment staat al
|
||||||
|
in de bestandsnaam — metadata-operaties voegen niets toe.
|
||||||
- **Early-abort** — bij 5 opeenvolgende fouten stopt het script en stuurt het een
|
- **Early-abort** — bij 5 opeenvolgende fouten stopt het script en stuurt het een
|
||||||
Matrix-bericht met de laatste fout. Bronbestanden worden nooit verwijderd zonder
|
Matrix-bericht met de laatste fout. Bronbestanden worden nooit verwijderd zonder
|
||||||
geverifieerde kopie.
|
geverifieerde kopie.
|
||||||
|
|
|
||||||
|
|
@ -153,12 +153,13 @@ def copy_file(src, dst, attempts=MAX_RETRIES, delay=RETRY_DELAY):
|
||||||
last_error = None
|
last_error = None
|
||||||
for attempt in range(1, attempts + 1):
|
for attempt in range(1, attempts + 1):
|
||||||
try:
|
try:
|
||||||
# copy (niet copy2): copy2 preserveert timestamps via os.utime, en exfat
|
# copyfile (niet copy/copy2): die doen ook metadata-operaties op het
|
||||||
# mountuid=1000 + allow_utime=0022 weigeren dat voor niet-eigenaars —
|
# doel — chmod en utime. Op exfat met mountuid=1000 zijn álle bestanden
|
||||||
# daarmee faalde elke kopie toen de service als aiaiaiaiagent ging
|
# eigendom van uid 1000, en allow_utime=0022 + chmod-toestand weigeren
|
||||||
# draaien (EPERM). Op exfat is mode altijd 0777 en staat het
|
# dat voor de niet-eigenaar service-gebruiker (EPERM op elke kopie).
|
||||||
# opnamemoment al in de bestandsnaam (YYYYMMDDHHMMSS).
|
# copyfile schrijft alleen data; mode is op exfat altijd 0777 en het
|
||||||
shutil.copy(src, dst)
|
# opnamemoment staat al in de bestandsnaam (YYYYMMDDHHMMSS).
|
||||||
|
shutil.copyfile(src, dst)
|
||||||
return
|
return
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
last_error = e
|
last_error = e
|
||||||
|
|
|
||||||
|
|
@ -85,7 +85,7 @@ class CopyFileTest(unittest.TestCase):
|
||||||
self.assertEqual(os.path.getsize(self.dst), 100)
|
self.assertEqual(os.path.getsize(self.dst), 100)
|
||||||
|
|
||||||
def test_retries_then_succeeds(self):
|
def test_retries_then_succeeds(self):
|
||||||
real_copy = dashcam.shutil.copy
|
real_copy = dashcam.shutil.copyfile
|
||||||
calls = {"n": 0}
|
calls = {"n": 0}
|
||||||
|
|
||||||
def flaky(s, d):
|
def flaky(s, d):
|
||||||
|
|
@ -94,13 +94,13 @@ class CopyFileTest(unittest.TestCase):
|
||||||
raise OSError(errno.EIO, "I/O error")
|
raise OSError(errno.EIO, "I/O error")
|
||||||
real_copy(s, d)
|
real_copy(s, d)
|
||||||
|
|
||||||
with mock.patch.object(dashcam.shutil, "copy", side_effect=flaky):
|
with mock.patch.object(dashcam.shutil, "copyfile", side_effect=flaky):
|
||||||
dashcam.copy_file(self.src, self.dst, attempts=3, delay=0)
|
dashcam.copy_file(self.src, self.dst, attempts=3, delay=0)
|
||||||
self.assertEqual(calls["n"], 3)
|
self.assertEqual(calls["n"], 3)
|
||||||
self.assertEqual(os.path.getsize(self.dst), 100)
|
self.assertEqual(os.path.getsize(self.dst), 100)
|
||||||
|
|
||||||
def test_raises_after_exhausting_attempts(self):
|
def test_raises_after_exhausting_attempts(self):
|
||||||
with mock.patch.object(dashcam.shutil, "copy", side_effect=OSError(errno.EIO, "I/O error")):
|
with mock.patch.object(dashcam.shutil, "copyfile", side_effect=OSError(errno.EIO, "I/O error")):
|
||||||
with self.assertRaises(OSError):
|
with self.assertRaises(OSError):
|
||||||
dashcam.copy_file(self.src, self.dst, attempts=2, delay=0)
|
dashcam.copy_file(self.src, self.dst, attempts=2, delay=0)
|
||||||
|
|
||||||
|
|
@ -159,7 +159,7 @@ class TransferFilesTest(unittest.TestCase):
|
||||||
with open(d, "wb") as f:
|
with open(d, "wb") as f:
|
||||||
f.write(b"0" * 50)
|
f.write(b"0" * 50)
|
||||||
|
|
||||||
with mock.patch.object(dashcam.shutil, "copy", side_effect=bad_copy), mock.patch.object(
|
with mock.patch.object(dashcam.shutil, "copyfile", side_effect=bad_copy), mock.patch.object(
|
||||||
dashcam.time, "sleep"
|
dashcam.time, "sleep"
|
||||||
):
|
):
|
||||||
ok, errors = dashcam.transfer_files(files, self.src, self.base)
|
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)
|
for i in range(dashcam.MAX_CONSECUTIVE_ERRORS + 3)
|
||||||
]
|
]
|
||||||
with mock.patch.object(
|
with mock.patch.object(
|
||||||
dashcam.shutil, "copy", side_effect=OSError(errno.EIO, "I/O error")
|
dashcam.shutil, "copyfile", side_effect=OSError(errno.EIO, "I/O error")
|
||||||
), mock.patch.object(dashcam.time, "sleep"):
|
), mock.patch.object(dashcam.time, "sleep"):
|
||||||
with self.assertRaises(dashcam.TransferAborted) as ctx:
|
with self.assertRaises(dashcam.TransferAborted) as ctx:
|
||||||
dashcam.transfer_files(files, self.src, self.base)
|
dashcam.transfer_files(files, self.src, self.base)
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue