From 3b407c841390fe1957a82d6e5cae21391ca848a4 Mon Sep 17 00:00:00 2001 From: Andras Bacsai <5845193+andrasbacsai@users.noreply.github.com> Date: Mon, 19 Oct 2026 21:05:38 +0200 Subject: [PATCH] fix(backups): send Content-MD5 for S3-only volume backups on Object Lock (#12215) --- app/Jobs/VolumeBackupJob.php | 5 ++++- tests/Feature/VolumeBackupTest.php | 8 +++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/Jobs/VolumeBackupJob.php b/app/Jobs/VolumeBackupJob.php index 0bf27ad35f..157f032118 100644 --- a/app/Jobs/VolumeBackupJob.php +++ b/app/Jobs/VolumeBackupJob.php @@ -417,9 +417,12 @@ class VolumeBackupJob implements ShouldBeEncrypted, ShouldQueue // `mc pipe --quiet` panics in recent mc releases, so keep its progress bar out of the error output instead. // Each progress frame starts with "\r", so printing the last field per line keeps only the final frame and real errors. $mcPipeLog = '/tmp/mc-pipe.log'; + // Object Lock buckets need Content-MD5 on each upload. `mc cp` adds it when the bucket lock is enabled, `mc pipe` only with `--checksum=MD5`. $streamScript = 'set -o pipefail; mc alias set'.$resolveOptions.' temporary ' .escapeshellarg($s3->endpoint).' '.escapeshellarg($s3->key).' '.escapeshellarg($s3->secret) - .' >/dev/null && ('.$archiveScript.' | mc pipe'.$resolveOptions.' '.escapeshellarg($destination).' >/dev/null 2>'.$mcPipeLog + .' >/dev/null && lock=$(mc retention info --default --json'.$resolveOptions.' '.escapeshellarg('temporary/'.$s3->bucket).' 2>/dev/null || true)' + .' && case "$lock" in *\'"enabled":"Enabled"\'*) checksum=--checksum=MD5 ;; *) checksum= ;; esac' + .' && ('.$archiveScript.' | mc pipe'.$resolveOptions.' $checksum '.escapeshellarg($destination).' >/dev/null 2>'.$mcPipeLog .' || { status=$?; awk -F '.escapeshellarg('\r').' '.escapeshellarg('{ print $NF }').' '.$mcPipeLog.' >&2; exit $status; })' .' && mc stat --json'.$resolveOptions.' '.escapeshellarg($destination) .' | sed -n '.escapeshellarg('s/.*"size":\([0-9][0-9]*\).*/\1/p'); diff --git a/tests/Feature/VolumeBackupTest.php b/tests/Feature/VolumeBackupTest.php index 4938287315..97fd82614f 100644 --- a/tests/Feature/VolumeBackupTest.php +++ b/tests/Feature/VolumeBackupTest.php @@ -1987,7 +1987,7 @@ it('keeps the upload destination on the volume backup execution', function () { ->and($execution->s3->is($s3Storage))->toBeTrue(); }); -it('streams S3-only volume backups without creating or copying a local archive', function () { +it('streams S3-only volume backups with Content-MD5 on Object Lock buckets and without a local archive', function () { config(['broadcasting.default' => 'null']); defined('CURLOPT_RESOLVE') || define('CURLOPT_RESOLVE', 10203); Carbon::setTestNow('2026-08-15 12:00:00'); @@ -2029,12 +2029,14 @@ it('streams S3-only volume backups without creating or copying a local archive', Process::assertRan(fn ($process) => str_contains($process->command, 'tar -I') && str_contains($process->command, 'mc pipe') && ! str_contains($process->command, '--quiet') - && str_contains($process->command, '>/dev/null && (compressor=') + && str_contains($process->command, 'lock=$(mc retention info --default --json --resolve') + && str_contains($process->command, '*) checksum=--checksum=MD5 ;; *) checksum= ;; esac && (compressor=') + && str_contains($process->command, ' $checksum ') && str_contains($process->command, '>/dev/null 2>/tmp/mc-pipe.log || {') && str_contains($process->command, '/tmp/mc-pipe.log >&2; exit $status; }) && mc stat') && str_contains($process->command, 'temporary/bucket/') && str_contains($process->command, $expectedFilename) - && substr_count($process->command, '--resolve') === 3 + && substr_count($process->command, '--resolve') === 4 && substr_count($process->command, '>/dev/null') >= 2 && ! str_contains($process->command, ' > ') && ! str_contains($process->command, 'mc cp'));