diff --git a/lib/Repository/Repository.php b/lib/Repository/Repository.php index b23570a..1c34ea0 100644 --- a/lib/Repository/Repository.php +++ b/lib/Repository/Repository.php @@ -845,7 +845,13 @@ final class Repository { try { foreach ($this->backend->getRanges($ranges) as $i => $data) { foreach ($parts[$i] as [$id, $at, $length]) { - yield $this->decodeBlob($id, substr($data, $at, $length)); + try { + $blob = $this->decodeBlob($id, substr($data, $at, $length)); + } catch (\Exception) { + // a read that went wrong on the way (see loadBlob()): this blob on its own + $blob = $this->loadBlob($id); + } + yield $blob; $done++; } } diff --git a/src/components/Backups.vue b/src/components/Backups.vue index 82ac031..e0d915d 100644 --- a/src/components/Backups.vue +++ b/src/components/Backups.vue @@ -91,7 +91,7 @@ const browserZone = Intl.DateTimeFormat().resolvedOptions().timeZone || 'UTC' const shown = (s) => (s && props.status.scheduleTimezone && props.status.scheduleTimezone !== browserZone) ? toZone(s, props.status.scheduleTimezone, browserZone) : s const schedule = ref(shown(props.status.schedule)) const error = ref('') -watch(() => props.status.schedule, (s) => { schedule.value = shown(s) }) +watch(() => [props.status.schedule, props.status.scheduleTimezone], ([s]) => { schedule.value = shown(s) }) const trash = reactive({}) async function loadTrash() { diff --git a/src/format.js b/src/format.js index c7496c8..5774270 100644 --- a/src/format.js +++ b/src/format.js @@ -33,17 +33,23 @@ export function formatSize(bytes) { * @param {string} hhmm the time * @param {string} from IANA zone it is in * @param {string} to IANA zone to show it in + * @param {Date} now the day to convert on (today) * @return {string} */ -export function toZone(hhmm, from, to) { +export function toZone(hhmm, from, to, now = new Date()) { const [h, m] = hhmm.split(':').map(Number) const parts = (zone, date) => Object.fromEntries(new Intl.DateTimeFormat('en-GB', { timeZone: zone, hourCycle: 'h23', year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', }).formatToParts(date).map((p) => [p.type, p.value])) - const today = parts(from, new Date()) - const guess = Date.UTC(+today.year, +today.month - 1, +today.day, h, m) - const seen = parts(from, new Date(guess)) - const offset = Date.UTC(+seen.year, +seen.month - 1, +seen.day, +seen.hour, +seen.minute) - guess - const there = parts(to, new Date(guess - offset)) + const offsetAt = (at) => { + const seen = parts(from, new Date(at)) + return Date.UTC(+seen.year, +seen.month - 1, +seen.day, +seen.hour, +seen.minute) - at + } + const today = parts(from, now) + const wall = Date.UTC(+today.year, +today.month - 1, +today.day, h, m) + // the offset at the wall time itself: on a day the clocks change it differs from the one at + // midnight, so it is looked up a second time at the moment found first + const at = wall - offsetAt(wall - offsetAt(wall)) + const there = parts(to, new Date(at)) return `${there.hour}:${there.minute}` } diff --git a/tests/Unit/Repository/DamagedReadTest.php b/tests/Unit/Repository/DamagedReadTest.php index d532496..a281fd4 100644 --- a/tests/Unit/Repository/DamagedReadTest.php +++ b/tests/Unit/Repository/DamagedReadTest.php @@ -45,6 +45,23 @@ class DamagedReadTest extends TestCase { $this->assertSame(0, $backend->streams, 'the damaged read happened'); } + public function testATableDumpThatCameInDamagedOnceIsReadAgain(): void { + $repo = Repository::openWithKey(new LocalBackend($this->root), $this->keys); + $stats = []; + $writer = $repo->blobWriter($stats); + $lines = []; + for ($i = 0; $i < 20000; $i++) { + $lines[] = json_encode(['i' => $i, 'pad' => str_repeat('x', $i % 400)]); + } + $writer->write(implode("\n", $lines) . "\n"); + $blobs = $writer->finish(); + $repo->flushPacks(); + $backend = new GarblingBackend(new LocalBackend($this->root), ranges: 1); + + $this->assertSame($lines, iterator_to_array(Repository::openWithKey($backend, $this->keys)->readLines($blobs), false)); + $this->assertSame(0, $backend->ranges, 'the damaged read happened'); + } + public function testDataDamagedOnTheLocationStillStopsTheRestore(): void { $backend = new GarblingBackend(new LocalBackend($this->root), streams: 1000, ranges: 1000);