From e280521f26ab4ba83fe4a3b3e5d04475ead91b16 Mon Sep 17 00:00:00 2001 From: Andreas Berqvist Date: Fri, 3 Jul 2026 00:13:34 +0200 Subject: [PATCH 01/11] Error handling and performance optimization --- src/Backuper.php | 53 ++++++++++++++++++++ src/Support/Zipper.php | 100 ++++++++++++++++++++++++++++++++++---- tests/Unit/ZipperTest.php | 55 +++++++++++++++++++++ 3 files changed, 199 insertions(+), 9 deletions(-) diff --git a/src/Backuper.php b/src/Backuper.php index aed6006..5a2d439 100644 --- a/src/Backuper.php +++ b/src/Backuper.php @@ -7,6 +7,7 @@ use Illuminate\Contracts\Auth\Authenticatable; use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\File; +use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Pipeline; use Itiden\Backup\Contracts\Repositories\BackupRepository; use Itiden\Backup\DataTransferObjects\BackupDto; @@ -15,6 +16,7 @@ use Itiden\Backup\Events\BackupFailed; use Itiden\Backup\Models\Metadata; use Itiden\Backup\Support\Zipper; +use RuntimeException; use Throwable; use function Illuminate\Filesystem\join_paths; @@ -33,12 +35,41 @@ public function __construct( */ public function backup(?Authenticatable $user = null): BackupDto { + if (function_exists('set_time_limit')) { + set_time_limit(0); + } + + ignore_user_abort(true); + $lock = $this->stateManager->getLock(); + $temp_zip_path = null; + try { $this->stateManager->setState(State::BackupInProgress); $temp_zip_path = join_paths(Config::string('backup.temp_path'), 'temp.zip'); + $completed = false; + + register_shutdown_function(static function () use (&$completed, $temp_zip_path): void { + if ($completed) { + return; + } + + Log::error('backup: process killed mid-backup', [ + 'temp_zip_exists' => File::exists($temp_zip_path), + ]); + + if (File::exists($temp_zip_path)) { + File::delete($temp_zip_path); + } + + app(StateManager::class)->setState(State::BackupFailed); + }); + + Log::info('backup: started', [ + 'user' => $user?->getAuthIdentifier(), + ]); $zipper = Zipper::write($temp_zip_path); @@ -57,6 +88,18 @@ public function backup(?Authenticatable $user = null): BackupDto $zipper->close(); + Log::info('backup: zip closed', [ + 'size' => File::size($temp_zip_path), + ]); + + if (!Zipper::verify($temp_zip_path)) { + File::delete($temp_zip_path); + + throw new RuntimeException('Zip verification failed — the backup archive is invalid.'); + } + + Log::info('backup: zip verified'); + $backup = $this->repository->add($temp_zip_path); $metadata = static::addMetaFromZipToBackupMeta($temp_zip_path, $backup); @@ -73,8 +116,18 @@ public function backup(?Authenticatable $user = null): BackupDto $this->stateManager->setState(State::BackupCompleted); + Log::info('backup: completed', ['path' => $backup->path]); + + $completed = true; + return $backup; } catch (Throwable $e) { + if ($temp_zip_path !== null && File::exists($temp_zip_path)) { + File::delete($temp_zip_path); + } + + Log::error('backup: failed', ['error' => $e->getMessage()]); + $exception = new Exceptions\BackupFailed(previous: $e); event(new BackupFailed($exception)); diff --git a/src/Support/Zipper.php b/src/Support/Zipper.php index f4bfd86..7dea381 100644 --- a/src/Support/Zipper.php +++ b/src/Support/Zipper.php @@ -6,23 +6,43 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\File; +use Illuminate\Support\Facades\Log; +use RuntimeException; use SensitiveParameter; -use Symfony\Component\Finder\SplFileInfo; +use Symfony\Component\Finder\Finder; use ZipArchive; // @mago-expect lint:too-many-methods final class Zipper { + /** + * File extensions that are already compressed and should be stored + * without re-compression to save CPU cycles and I/O bandwidth. + */ + private const STORED_EXTENSIONS = [ + 'zip', 'mp4', 'webm', 'png', 'jpg', 'jpeg', 'webp', 'gif', 'pdf', + 'mp3', 'wav', 'mov', 'avi', 'ogg', 'gz', 'tar', 'tgz', + 'woff', 'woff2', 'ttf', 'otf', + 'ico', 'avif', 'heic', + 'bz2', 'xz', '7z', 'rar', + ]; + private ZipArchive $zip; private array $meta = []; + private string $path; public function __construct(string $path, int $flags = ZipArchive::CREATE | ZipArchive::OVERWRITE) { File::ensureDirectoryExists(dirname($path)); + $this->path = $path; $this->zip = new ZipArchive(); - $this->zip->open($path, $flags); + $result = $this->zip->open($path, $flags); + + if ($result !== true) { + throw new RuntimeException("Failed to open zip [{$path}] (error code: {$result})"); + } } /** @@ -38,12 +58,36 @@ public static function read(string $path): self return new static($path, ZipArchive::RDONLY); } + /** + * Verify that a zip file at the given path is a valid archive. + */ + public static function verify(string $path): bool + { + $zip = new ZipArchive(); + + if ($zip->open($path, ZipArchive::RDONLY) !== true) { + return false; + } + + $valid = $zip->numFiles > 0; + + $zip->close(); + + return $valid; + } + /** * Close the Zipper and write the archive to the filesystem. */ public function close(): void { - $this->zip->close(); + if (!$this->zip->close()) { + Log::error('zipper: close failed', ['path' => $this->path]); + + throw new RuntimeException( + "Failed to write zip archive [{$this->path}] — check disk space and memory limits.", + ); + } } /** @@ -53,18 +97,36 @@ public function encrypt(#[SensitiveParameter] string $password): self { $this->zip->setPassword($password); - collect(range(0, $this->zip->numFiles - 1)) - ->each(fn(int $file): bool => $this->zip->setEncryptionIndex($file, ZipArchive::EM_AES_256)); + for ($i = 0; $i < $this->zip->numFiles; $i++) { + if (!$this->zip->setEncryptionIndex($i, ZipArchive::EM_AES_256)) { + throw new RuntimeException("Failed to set encryption for file at index {$i}"); + } + } return $this; } /** * Add a file to the archive. + * + * Pre-compressed file types (images, videos, archives) are stored + * without re-compression using CM_STORE for maximum I/O performance. + * Text-based files use CM_DEFLATE for size reduction. */ public function addFile(string $path, ?string $name = null): self { - $this->zip->addFile($path, $name ?? basename($path)); + $entryName = $name ?? basename($path); + + if (!$this->zip->addFile($path, $entryName)) { + throw new RuntimeException("Failed to add file to zip: {$path}"); + } + + $extension = strtolower(pathinfo($entryName, PATHINFO_EXTENSION)); + $method = in_array($extension, self::STORED_EXTENSIONS, true) + ? ZipArchive::CM_STORE + : ZipArchive::CM_DEFLATE; + + $this->zip->setCompressionName($entryName, $method); return $this; } @@ -74,7 +136,9 @@ public function addFile(string $path, ?string $name = null): self */ public function addFromString(string $name, string $content): self { - $this->zip->addFromString($name, $content); + if (!$this->zip->addFromString($name, $content)) { + throw new RuntimeException("Failed to add content to zip: {$name}"); + } return $this; } @@ -84,9 +148,27 @@ public function addFromString(string $name, string $content): self */ public function addDirectory(string $path, ?string $prefix = null): self { - collect(File::allFiles($path))->each(function (SplFileInfo $file) use ($prefix): void { + $finder = (new Finder())->files()->ignoreDotFiles(false)->in($path); + + $count = 0; + + foreach ($finder as $file) { $this->addFile($file->getPathname(), $prefix . '/' . $file->getRelativePathname()); - }); + + $count++; + + if ($count % 500 === 0) { + Log::info('zipper: addDirectory progress', [ + 'directory' => $path, + 'files_added' => $count, + ]); + } + } + + Log::info('zipper: addDirectory complete', [ + 'directory' => $path, + 'total_files' => $count, + ]); return $this; } diff --git a/tests/Unit/ZipperTest.php b/tests/Unit/ZipperTest.php index f69fa27..3e923bb 100644 --- a/tests/Unit/ZipperTest.php +++ b/tests/Unit/ZipperTest.php @@ -124,4 +124,59 @@ $zip->close(); }); + + it('uses CM_STORE for pre-compressed media extensions', function (): void { + $target = storage_path('test.zip'); + $source = storage_path('test_media.png'); + + File::put($source, str_repeat('a', 10_000)); + + Zipper::write($target)->addFile($source, 'test.png')->close(); + + $archive = new ZipArchive(); + $archive->open($target); + $stat = $archive->statName('test.png'); + + expect($stat['comp_size'])->toBe($stat['size']); + + $archive->close(); + }); + + it('uses CM_DEFLATE for text files', function (): void { + $target = storage_path('test.zip'); + $source = storage_path('test_text.txt'); + + File::put($source, str_repeat('a', 10_000)); + + Zipper::write($target)->addFile($source, 'test.txt')->close(); + + $archive = new ZipArchive(); + $archive->open($target); + $stat = $archive->statName('test.txt'); + + expect($stat['comp_size'])->toBeLessThan($stat['size']); + + $archive->close(); + }); + + it('can verify a valid zip', function (): void { + $target = storage_path('test.zip'); + + Zipper::write($target)->addFromString('test.txt', 'test')->close(); + + expect(Zipper::verify($target))->toBeTrue(); + }); + + it('returns false when verifying an invalid zip', function (): void { + $target = storage_path('invalid.zip'); + + File::put($target, 'not a zip file'); + + expect(Zipper::verify($target))->toBeFalse(); + }); + + it('throws when opening a non-existent zip for reading', function (): void { + expect(fn() => Zipper::read(storage_path('nonexistent.zip'))) + ->toThrow(RuntimeException::class); + }); })->group('zipper'); From 968720adf77ebe7487e6dc4fc49e6496030f94d7 Mon Sep 17 00:00:00 2001 From: Andreas Berqvist Date: Fri, 3 Jul 2026 00:19:05 +0200 Subject: [PATCH 02/11] Fix format --- src/Exceptions/BackupFailed.php | 6 +-- src/Http/Controllers/Api/BackupController.php | 42 +++++++++--------- src/Support/Zipper.php | 44 ++++++++++++++----- tests/Feature/RestoreCommandTest.php | 6 +-- tests/Feature/ViewBackupsTest.php | 38 +++++++++------- tests/Unit/ZipperTest.php | 3 +- 6 files changed, 84 insertions(+), 55 deletions(-) diff --git a/src/Exceptions/BackupFailed.php b/src/Exceptions/BackupFailed.php index e3534a9..13aea58 100644 --- a/src/Exceptions/BackupFailed.php +++ b/src/Exceptions/BackupFailed.php @@ -12,8 +12,8 @@ final class BackupFailed extends Exception { public function __construct(Throwable $previous) { - parent::__construct(__('statamic-backup::backup.failed', ['date' => Carbon::now()->format( - 'Ymd', - )]), previous: $previous); + parent::__construct(__('statamic-backup::backup.failed', [ + 'date' => Carbon::now()->format('Ymd'), + ]), previous: $previous); } } diff --git a/src/Http/Controllers/Api/BackupController.php b/src/Http/Controllers/Api/BackupController.php index b508671..71eea56 100644 --- a/src/Http/Controllers/Api/BackupController.php +++ b/src/Http/Controllers/Api/BackupController.php @@ -14,27 +14,29 @@ public function __invoke(BackupRepository $repo): AnonymousResourceCollection { $backups = $repo->all(); - return BackupResource::collection($backups)->additional(['meta' => [ - // Required by statamic to render the table - 'columns' => [ - [ - 'label' => 'Name', - 'field' => 'name', - 'visible' => true, - ], - [ - 'label' => 'Created at', - 'field' => 'created_at', - 'visible' => true, - 'sortable' => true, - ], - [ - 'label' => 'Size', - 'field' => 'size', - 'visible' => true, - 'sortable' => true, + return BackupResource::collection($backups)->additional([ + 'meta' => [ + // Required by statamic to render the table + 'columns' => [ + [ + 'label' => 'Name', + 'field' => 'name', + 'visible' => true, + ], + [ + 'label' => 'Created at', + 'field' => 'created_at', + 'visible' => true, + 'sortable' => true, + ], + [ + 'label' => 'Size', + 'field' => 'size', + 'visible' => true, + 'sortable' => true, + ], ], ], - ]]); + ]); } } diff --git a/src/Support/Zipper.php b/src/Support/Zipper.php index 7dea381..c0e44e5 100644 --- a/src/Support/Zipper.php +++ b/src/Support/Zipper.php @@ -20,11 +20,34 @@ final class Zipper * without re-compression to save CPU cycles and I/O bandwidth. */ private const STORED_EXTENSIONS = [ - 'zip', 'mp4', 'webm', 'png', 'jpg', 'jpeg', 'webp', 'gif', 'pdf', - 'mp3', 'wav', 'mov', 'avi', 'ogg', 'gz', 'tar', 'tgz', - 'woff', 'woff2', 'ttf', 'otf', - 'ico', 'avif', 'heic', - 'bz2', 'xz', '7z', 'rar', + 'zip', + 'mp4', + 'webm', + 'png', + 'jpg', + 'jpeg', + 'webp', + 'gif', + 'pdf', + 'mp3', + 'wav', + 'mov', + 'avi', + 'ogg', + 'gz', + 'tar', + 'tgz', + 'woff', + 'woff2', + 'ttf', + 'otf', + 'ico', + 'avif', + 'heic', + 'bz2', + 'xz', + '7z', + 'rar', ]; private ZipArchive $zip; @@ -122,9 +145,7 @@ public function addFile(string $path, ?string $name = null): self } $extension = strtolower(pathinfo($entryName, PATHINFO_EXTENSION)); - $method = in_array($extension, self::STORED_EXTENSIONS, true) - ? ZipArchive::CM_STORE - : ZipArchive::CM_DEFLATE; + $method = in_array($extension, self::STORED_EXTENSIONS, true) ? ZipArchive::CM_STORE : ZipArchive::CM_DEFLATE; $this->zip->setCompressionName($entryName, $method); @@ -148,7 +169,10 @@ public function addFromString(string $name, string $content): self */ public function addDirectory(string $path, ?string $prefix = null): self { - $finder = (new Finder())->files()->ignoreDotFiles(false)->in($path); + $finder = new Finder() + ->files() + ->ignoreDotFiles(false) + ->in($path); $count = 0; @@ -157,7 +181,7 @@ public function addDirectory(string $path, ?string $prefix = null): self $count++; - if ($count % 500 === 0) { + if (($count % 500) === 0) { Log::info('zipper: addDirectory progress', [ 'directory' => $path, 'files_added' => $count, diff --git a/tests/Feature/RestoreCommandTest.php b/tests/Feature/RestoreCommandTest.php index 6fd2976..5d434ac 100644 --- a/tests/Feature/RestoreCommandTest.php +++ b/tests/Feature/RestoreCommandTest.php @@ -27,9 +27,9 @@ $backup = Backuper::backup(); - artisan('statamic:backup:restore', ['--path' => Storage::disk(config( - 'backup.destination.disk', - ))->path($backup->path)]) + artisan('statamic:backup:restore', [ + '--path' => Storage::disk(config('backup.destination.disk'))->path($backup->path), + ]) ->expectsConfirmation('Are you sure you want to restore your content?') ->assertFailed(); }); diff --git a/tests/Feature/ViewBackupsTest.php b/tests/Feature/ViewBackupsTest.php index fb184e4..e55ec9d 100644 --- a/tests/Feature/ViewBackupsTest.php +++ b/tests/Feature/ViewBackupsTest.php @@ -70,24 +70,28 @@ getJson(cp_route('api.itiden.backup.index')) ->assertOk() ->assertJsonStructure([ - 'data' => ['*' => [ - 'name', - 'size', - 'path', - 'created_at', - 'id', - 'metadata' => [ - 'created_by', - 'downloads', - 'restores', - 'skipped_pipes', + 'data' => [ + '*' => [ + 'name', + 'size', + 'path', + 'created_at', + 'id', + 'metadata' => [ + 'created_by', + 'downloads', + 'restores', + 'skipped_pipes', + ], ], - ]], - 'meta' => ['columns' => ['*' => [ - 'label', - 'field', - 'visible', - ]]], + ], + 'meta' => [ + 'columns' => ['*' => [ + 'label', + 'field', + 'visible', + ]], + ], ]); }); })->group('view'); diff --git a/tests/Unit/ZipperTest.php b/tests/Unit/ZipperTest.php index 3e923bb..f425d36 100644 --- a/tests/Unit/ZipperTest.php +++ b/tests/Unit/ZipperTest.php @@ -176,7 +176,6 @@ }); it('throws when opening a non-existent zip for reading', function (): void { - expect(fn() => Zipper::read(storage_path('nonexistent.zip'))) - ->toThrow(RuntimeException::class); + expect(fn() => Zipper::read(storage_path('nonexistent.zip')))->toThrow(RuntimeException::class); }); })->group('zipper'); From 4c61509ddadda78c70345a7f43377b5e0d0b6819 Mon Sep 17 00:00:00 2001 From: Andreas Bergqvist Date: Fri, 3 Jul 2026 00:22:58 +0200 Subject: [PATCH 03/11] Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- src/Backuper.php | 35 +++++++++++++++++++++++------------ 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/Backuper.php b/src/Backuper.php index 5a2d439..23a6093 100644 --- a/src/Backuper.php +++ b/src/Backuper.php @@ -51,21 +51,32 @@ public function backup(?Authenticatable $user = null): BackupDto $temp_zip_path = join_paths(Config::string('backup.temp_path'), 'temp.zip'); $completed = false; - register_shutdown_function(static function () use (&$completed, $temp_zip_path): void { - if ($completed) { - return; - } +register_shutdown_function(static function () use (&$completed, $temp_zip_path): void { + if ($completed) { + return; + } + + $error = error_get_last(); + + // Only treat true fatal errors as a "killed mid-backup" scenario. + if ($error === null || !in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR], true)) { + return; + } + + Log::error('backup: fatal error mid-backup', [ + 'error' => $error, + 'temp_zip_exists' => File::exists($temp_zip_path), + ]); - Log::error('backup: process killed mid-backup', [ - 'temp_zip_exists' => File::exists($temp_zip_path), - ]); + if (File::exists($temp_zip_path)) { + File::delete($temp_zip_path); + } - if (File::exists($temp_zip_path)) { - File::delete($temp_zip_path); - } + // Ensure the lock doesn't remain held indefinitely after a fatal error. + \Illuminate\Support\Facades\Cache::lock(StateManager::LOCK)->forceRelease(); - app(StateManager::class)->setState(State::BackupFailed); - }); + app(StateManager::class)->setState(State::BackupFailed); +}); Log::info('backup: started', [ 'user' => $user?->getAuthIdentifier(), From b0c83eecf89207b3bdd77cadfe9ff6eb9216d77f Mon Sep 17 00:00:00 2001 From: Andreas Berqvist Date: Fri, 3 Jul 2026 00:24:31 +0200 Subject: [PATCH 04/11] Fix format --- src/Backuper.php | 55 +++++++++++++++++++++++++----------------------- 1 file changed, 29 insertions(+), 26 deletions(-) diff --git a/src/Backuper.php b/src/Backuper.php index 23a6093..acb9829 100644 --- a/src/Backuper.php +++ b/src/Backuper.php @@ -51,32 +51,35 @@ public function backup(?Authenticatable $user = null): BackupDto $temp_zip_path = join_paths(Config::string('backup.temp_path'), 'temp.zip'); $completed = false; -register_shutdown_function(static function () use (&$completed, $temp_zip_path): void { - if ($completed) { - return; - } - - $error = error_get_last(); - - // Only treat true fatal errors as a "killed mid-backup" scenario. - if ($error === null || !in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR], true)) { - return; - } - - Log::error('backup: fatal error mid-backup', [ - 'error' => $error, - 'temp_zip_exists' => File::exists($temp_zip_path), - ]); - - if (File::exists($temp_zip_path)) { - File::delete($temp_zip_path); - } - - // Ensure the lock doesn't remain held indefinitely after a fatal error. - \Illuminate\Support\Facades\Cache::lock(StateManager::LOCK)->forceRelease(); - - app(StateManager::class)->setState(State::BackupFailed); -}); + register_shutdown_function(static function () use (&$completed, $temp_zip_path): void { + if ($completed) { + return; + } + + $error = error_get_last(); + + // Only treat true fatal errors as a "killed mid-backup" scenario. + if ( + $error === null + || !in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR], true) + ) { + return; + } + + Log::error('backup: fatal error mid-backup', [ + 'error' => $error, + 'temp_zip_exists' => File::exists($temp_zip_path), + ]); + + if (File::exists($temp_zip_path)) { + File::delete($temp_zip_path); + } + + // Ensure the lock doesn't remain held indefinitely after a fatal error. + \Illuminate\Support\Facades\Cache::lock(StateManager::LOCK)->forceRelease(); + + app(StateManager::class)->setState(State::BackupFailed); + }); Log::info('backup: started', [ 'user' => $user?->getAuthIdentifier(), From f89220c85d4118b9ce22fc41321589936c4bafbb Mon Sep 17 00:00:00 2001 From: Andreas Berqvist Date: Fri, 3 Jul 2026 00:31:43 +0200 Subject: [PATCH 05/11] Fix parse error --- src/Support/Zipper.php | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/Support/Zipper.php b/src/Support/Zipper.php index c0e44e5..c2cf03a 100644 --- a/src/Support/Zipper.php +++ b/src/Support/Zipper.php @@ -169,10 +169,8 @@ public function addFromString(string $name, string $content): self */ public function addDirectory(string $path, ?string $prefix = null): self { - $finder = new Finder() - ->files() - ->ignoreDotFiles(false) - ->in($path); + $finder = new Finder(); + $finder->files()->ignoreDotFiles(false)->in($path); $count = 0; From c5e73835fabf19e6f189d88fb869b7f6eaf2710c Mon Sep 17 00:00:00 2001 From: Andreas Berqvist Date: Fri, 3 Jul 2026 20:24:00 +0200 Subject: [PATCH 06/11] Fix download --- .../Controllers/DownloadBackupController.php | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Http/Controllers/DownloadBackupController.php b/src/Http/Controllers/DownloadBackupController.php index ba719f0..97d31ba 100644 --- a/src/Http/Controllers/DownloadBackupController.php +++ b/src/Http/Controllers/DownloadBackupController.php @@ -9,11 +9,11 @@ use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\Storage; use Itiden\Backup\Contracts\Repositories\BackupRepository; -use Symfony\Component\HttpFoundation\StreamedResponse; +use Symfony\Component\HttpFoundation\Response; final readonly class DownloadBackupController { - public function __invoke(Request $request, string $id, BackupRepository $repo): StreamedResponse + public function __invoke(Request $request, string $id, BackupRepository $repo): Response { $backup = $repo->find($id); @@ -26,6 +26,22 @@ public function __invoke(Request $request, string $id, BackupRepository $repo): $backup->getMetadata()->addDownload($user); - return Storage::disk(Config::string('backup.destination.disk'))->download($backup->path); + if (function_exists('set_time_limit')) { + set_time_limit(0); + } + + // Clean and close all active output buffers to allow streaming without running out of memory + while (ob_get_level() > 0) { + ob_end_clean(); + } + + $disk = Storage::disk(Config::string('backup.destination.disk')); + + try { + $path = $disk->path($backup->path); + return response()->download($path); + } catch (\Throwable) { + return $disk->download($backup->path); + } } } From ffebca09cdeaf1496e23352aa97604543ec3f07d Mon Sep 17 00:00:00 2001 From: NeoIsRecursive Date: Mon, 27 Jul 2026 08:09:49 +0200 Subject: [PATCH 07/11] some adjustments --- composer.json | 2 +- mago.yaml | 2 +- src/Backuper.php | 28 ++---- .../Controllers/DownloadBackupController.php | 19 ++-- src/Http/Requests/ChunkyUploadRequest.php | 12 +-- src/Restorer.php | 2 - src/Support/Zipper.php | 90 +++++++++---------- src/Support/ZipperFailed.php | 45 ++++++++++ tests/Feature/UploadControllerTest.php | 8 +- tests/Unit/ZipperTest.php | 3 +- 10 files changed, 116 insertions(+), 95 deletions(-) create mode 100644 src/Support/ZipperFailed.php diff --git a/composer.json b/composer.json index 5b099c7..a853da3 100644 --- a/composer.json +++ b/composer.json @@ -43,7 +43,7 @@ "@php vendor/bin/mago format" ], "test": [ - "XDEBUG_MODE=coverage vendor/bin/pest --coverage --compact" + "vendor/bin/pest" ], "qa": [ "@format", diff --git a/mago.yaml b/mago.yaml index 9f886a4..7bd8ea1 100644 --- a/mago.yaml +++ b/mago.yaml @@ -28,7 +28,7 @@ linter: literal-named-argument: enabled: false halstead: - effort-threshold: 7000 + enabled: false class-name: enabled: false interface-name: diff --git a/src/Backuper.php b/src/Backuper.php index acb9829..0efe130 100644 --- a/src/Backuper.php +++ b/src/Backuper.php @@ -43,15 +43,14 @@ public function backup(?Authenticatable $user = null): BackupDto $lock = $this->stateManager->getLock(); - $temp_zip_path = null; + $temp_zip_path = join_paths(Config::string('backup.temp_path'), 'temp.zip'); try { $this->stateManager->setState(State::BackupInProgress); - $temp_zip_path = join_paths(Config::string('backup.temp_path'), 'temp.zip'); $completed = false; - register_shutdown_function(static function () use (&$completed, $temp_zip_path): void { + register_shutdown_function(function () use (&$completed, $temp_zip_path, $lock): void { if ($completed) { return; } @@ -66,25 +65,16 @@ public function backup(?Authenticatable $user = null): BackupDto return; } - Log::error('backup: fatal error mid-backup', [ - 'error' => $error, - 'temp_zip_exists' => File::exists($temp_zip_path), - ]); - if (File::exists($temp_zip_path)) { File::delete($temp_zip_path); } // Ensure the lock doesn't remain held indefinitely after a fatal error. - \Illuminate\Support\Facades\Cache::lock(StateManager::LOCK)->forceRelease(); + $lock->forceRelease(); - app(StateManager::class)->setState(State::BackupFailed); + $this->stateManager->setState(State::BackupFailed); }); - Log::info('backup: started', [ - 'user' => $user?->getAuthIdentifier(), - ]); - $zipper = Zipper::write($temp_zip_path); Pipeline::via('backup')->send($zipper)->through(Config::array('backup.pipeline'))->thenReturn(); @@ -102,18 +92,12 @@ public function backup(?Authenticatable $user = null): BackupDto $zipper->close(); - Log::info('backup: zip closed', [ - 'size' => File::size($temp_zip_path), - ]); - if (!Zipper::verify($temp_zip_path)) { File::delete($temp_zip_path); throw new RuntimeException('Zip verification failed — the backup archive is invalid.'); } - Log::info('backup: zip verified'); - $backup = $this->repository->add($temp_zip_path); $metadata = static::addMetaFromZipToBackupMeta($temp_zip_path, $backup); @@ -136,12 +120,10 @@ public function backup(?Authenticatable $user = null): BackupDto return $backup; } catch (Throwable $e) { - if ($temp_zip_path !== null && File::exists($temp_zip_path)) { + if (File::exists($temp_zip_path)) { File::delete($temp_zip_path); } - Log::error('backup: failed', ['error' => $e->getMessage()]); - $exception = new Exceptions\BackupFailed(previous: $e); event(new BackupFailed($exception)); diff --git a/src/Http/Controllers/DownloadBackupController.php b/src/Http/Controllers/DownloadBackupController.php index 97d31ba..8b7aad3 100644 --- a/src/Http/Controllers/DownloadBackupController.php +++ b/src/Http/Controllers/DownloadBackupController.php @@ -30,18 +30,15 @@ public function __invoke(Request $request, string $id, BackupRepository $repo): set_time_limit(0); } - // Clean and close all active output buffers to allow streaming without running out of memory - while (ob_get_level() > 0) { - ob_end_clean(); - } - $disk = Storage::disk(Config::string('backup.destination.disk')); - try { - $path = $disk->path($backup->path); - return response()->download($path); - } catch (\Throwable) { - return $disk->download($backup->path); - } + return response()->streamDownload( + callback: static fn() => $disk->readStream($backup->path), + name: $backup->name, + headers: [ + 'Content-Type' => 'application/octet-stream', + 'Content-Length' => $disk->size($backup->path), + ], + ); } } diff --git a/src/Http/Requests/ChunkyUploadRequest.php b/src/Http/Requests/ChunkyUploadRequest.php index 8e4021a..5e1d1a7 100644 --- a/src/Http/Requests/ChunkyUploadRequest.php +++ b/src/Http/Requests/ChunkyUploadRequest.php @@ -11,12 +11,12 @@ final class ChunkyUploadRequest extends FormRequest public function rules(): array { return [ - 'resumableIdentifier' => 'required|string', - 'resumableFilename' => 'required|string', - 'resumableTotalChunks' => 'required|integer', - 'resumableChunkNumber' => 'required|integer', - 'resumableTotalSize' => 'required|integer', - 'file' => 'required|file', + 'resumableIdentifier' => ['required', 'string'], + 'resumableFilename' => ['required', 'string'], + 'resumableTotalChunks' => ['required', 'integer'], + 'resumableChunkNumber' => ['required', 'integer'], + 'resumableTotalSize' => ['required', 'integer'], + 'file' => ['required', 'file'], ]; } } diff --git a/src/Restorer.php b/src/Restorer.php index 08be6c2..ee5c38d 100644 --- a/src/Restorer.php +++ b/src/Restorer.php @@ -89,8 +89,6 @@ public function restore(BackupDto $backup, ?Authenticatable $user = null): void $this->stateManager->setState(State::RestoreCompleted); } catch (Throwable $e) { - report($e); - $exception = new Exceptions\RestoreFailed($backup, previous: $e); $this->stateManager->setState(State::RestoreFailed); diff --git a/src/Support/Zipper.php b/src/Support/Zipper.php index c2cf03a..fe350d6 100644 --- a/src/Support/Zipper.php +++ b/src/Support/Zipper.php @@ -6,20 +6,18 @@ use Illuminate\Support\Collection; use Illuminate\Support\Facades\File; -use Illuminate\Support\Facades\Log; -use RuntimeException; use SensitiveParameter; use Symfony\Component\Finder\Finder; use ZipArchive; -// @mago-expect lint:too-many-methods +// @mago-expect lint:too-many-methods,cyclomatic-complexity final class Zipper { /** * File extensions that are already compressed and should be stored * without re-compression to save CPU cycles and I/O bandwidth. */ - private const STORED_EXTENSIONS = [ + private const ENCRYPTED_FILE_TYPES = [ 'zip', 'mp4', 'webm', @@ -50,21 +48,21 @@ final class Zipper 'rar', ]; - private ZipArchive $zip; + private readonly ZipArchive $zip; private array $meta = []; - private string $path; - public function __construct(string $path, int $flags = ZipArchive::CREATE | ZipArchive::OVERWRITE) - { + public function __construct( + private readonly string $path, + int $flags = ZipArchive::CREATE | ZipArchive::OVERWRITE, + ) { File::ensureDirectoryExists(dirname($path)); - $this->path = $path; $this->zip = new ZipArchive(); $result = $this->zip->open($path, $flags); if ($result !== true) { - throw new RuntimeException("Failed to open zip [{$path}] (error code: {$result})"); + throw ZipperFailed::toOpen($path, $result); } } @@ -86,17 +84,25 @@ public static function read(string $path): self */ public static function verify(string $path): bool { - $zip = new ZipArchive(); + try { + if (!File::exists($path)) { + return false; + } - if ($zip->open($path, ZipArchive::RDONLY) !== true) { - return false; - } + if (File::mimeType($path) !== 'application/zip') { + return false; + } - $valid = $zip->numFiles > 0; + $zip = self::read($path); - $zip->close(); + $valid = $zip->getArchive()->status === ZipArchive::ER_OK; - return $valid; + $zip->close(); + + return $valid; + } catch (\Throwable) { + return false; + } } /** @@ -105,11 +111,7 @@ public static function verify(string $path): bool public function close(): void { if (!$this->zip->close()) { - Log::error('zipper: close failed', ['path' => $this->path]); - - throw new RuntimeException( - "Failed to write zip archive [{$this->path}] — check disk space and memory limits.", - ); + throw ZipperFailed::toClose($this->path); } } @@ -121,8 +123,10 @@ public function encrypt(#[SensitiveParameter] string $password): self $this->zip->setPassword($password); for ($i = 0; $i < $this->zip->numFiles; $i++) { - if (!$this->zip->setEncryptionIndex($i, ZipArchive::EM_AES_256)) { - throw new RuntimeException("Failed to set encryption for file at index {$i}"); + $encrypted = $this->zip->setEncryptionIndex($i, ZipArchive::EM_AES_256); + + if (!$encrypted) { + throw ZipperFailed::toSetEncryption($this->path); } } @@ -141,11 +145,13 @@ public function addFile(string $path, ?string $name = null): self $entryName = $name ?? basename($path); if (!$this->zip->addFile($path, $entryName)) { - throw new RuntimeException("Failed to add file to zip: {$path}"); + throw ZipperFailed::toAddFile($path); } $extension = strtolower(pathinfo($entryName, PATHINFO_EXTENSION)); - $method = in_array($extension, self::STORED_EXTENSIONS, true) ? ZipArchive::CM_STORE : ZipArchive::CM_DEFLATE; + $method = in_array($extension, self::ENCRYPTED_FILE_TYPES, true) + ? ZipArchive::CM_STORE + : ZipArchive::CM_DEFLATE; $this->zip->setCompressionName($entryName, $method); @@ -158,7 +164,7 @@ public function addFile(string $path, ?string $name = null): self public function addFromString(string $name, string $content): self { if (!$this->zip->addFromString($name, $content)) { - throw new RuntimeException("Failed to add content to zip: {$name}"); + throw new ZipperFailed("Failed to add content from string to zip: {$name}"); } return $this; @@ -172,26 +178,10 @@ public function addDirectory(string $path, ?string $prefix = null): self $finder = new Finder(); $finder->files()->ignoreDotFiles(false)->in($path); - $count = 0; - foreach ($finder as $file) { $this->addFile($file->getPathname(), $prefix . '/' . $file->getRelativePathname()); - - $count++; - - if (($count % 500) === 0) { - Log::info('zipper: addDirectory progress', [ - 'directory' => $path, - 'files_added' => $count, - ]); - } } - Log::info('zipper: addDirectory complete', [ - 'directory' => $path, - 'total_files' => $count, - ]); - return $this; } @@ -201,10 +191,18 @@ public function addDirectory(string $path, ?string $prefix = null): self public function extractTo(string $path, #[SensitiveParameter] ?string $password = null): self { if ($password) { - $this->zip->setPassword($password); + $result = $this->zip->setPassword($password); + + if (!$result) { + throw ZipperFailed::toSetPassword($this->path); + } } - $this->zip->extractTo($path); + $res = $this->zip->extractTo($path); + + if (!$res) { + throw new (ZipperFailed::toExtract)($this->path, $path); + } return $this; } @@ -239,7 +237,7 @@ public function getMeta(): Collection $comment = $this->zip->getArchiveComment(); if ($comment) { - $this->meta = json_decode($comment, true); + $this->meta = json_decode($comment, associative: true, flags: JSON_THROW_ON_ERROR); } return collect($this->meta); diff --git a/src/Support/ZipperFailed.php b/src/Support/ZipperFailed.php new file mode 100644 index 0000000..98f31c9 --- /dev/null +++ b/src/Support/ZipperFailed.php @@ -0,0 +1,45 @@ +take($chunks->count() - 1) ->each(function (array $values): void { $res = postJson(cp_route('itiden.backup.chunky.upload'), $values); - $res->assertStatus(201); + $res->assertCreated(); $res->assertJsonStructure(['message']); }); @@ -99,12 +99,12 @@ $chunksToTest->each(function (array $values): void { $res = postJson(cp_route('itiden.backup.chunky.upload'), $values); - $res->assertStatus(201); + $res->assertCreated(); }); $chunksToTest->each(function (array $values): void { $res = getJson(cp_route('itiden.backup.chunky.test', $values)); - $res->assertStatus(200); + $res->assertOk(); }); File::cleanDirectory(app(Chunky::class)->path()); @@ -128,6 +128,6 @@ 'resumableChunkNumber' => 1, ])); - $res->assertStatus(404); + $res->assertNotFound(); }); }); diff --git a/tests/Unit/ZipperTest.php b/tests/Unit/ZipperTest.php index f425d36..5136066 100644 --- a/tests/Unit/ZipperTest.php +++ b/tests/Unit/ZipperTest.php @@ -4,6 +4,7 @@ use Illuminate\Support\Facades\File; use Itiden\Backup\Support\Zipper; +use Itiden\Backup\Support\ZipperFailed; use function Itiden\Backup\Tests\fixtures_path; @@ -176,6 +177,6 @@ }); it('throws when opening a non-existent zip for reading', function (): void { - expect(fn() => Zipper::read(storage_path('nonexistent.zip')))->toThrow(RuntimeException::class); + expect(fn() => Zipper::read(storage_path('nonexistent.zip')))->toThrow(ZipperFailed::class); }); })->group('zipper'); From 1c13fd16a446366b3cb7b5ad5d5d80af6fffb6ed Mon Sep 17 00:00:00 2001 From: NeoIsRecursive Date: Mon, 27 Jul 2026 09:15:51 +0200 Subject: [PATCH 08/11] fix download --- src/Http/Controllers/DownloadBackupController.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Http/Controllers/DownloadBackupController.php b/src/Http/Controllers/DownloadBackupController.php index 8b7aad3..be3dc40 100644 --- a/src/Http/Controllers/DownloadBackupController.php +++ b/src/Http/Controllers/DownloadBackupController.php @@ -32,12 +32,20 @@ public function __invoke(Request $request, string $id, BackupRepository $repo): $disk = Storage::disk(Config::string('backup.destination.disk')); + $size = $disk->size($backup->path); + return response()->streamDownload( - callback: static fn() => $disk->readStream($backup->path), - name: $backup->name, + callback: static function () use ($disk, $backup) { + $stream = $disk->readStream($backup->path); + + fpassthru($stream); + + fclose($stream); + }, + name: basename($backup->path), headers: [ 'Content-Type' => 'application/octet-stream', - 'Content-Length' => $disk->size($backup->path), + 'Content-Length' => $size, ], ); } From 12044ff0e6b8d4b3aebe4792046b7783d8c359cb Mon Sep 17 00:00:00 2001 From: NeoIsRecursive Date: Mon, 27 Jul 2026 12:18:56 +0200 Subject: [PATCH 09/11] fix!: some static analysis issues and backup shape --- src/Abstracts/BackupPipe.php | 2 ++ src/DataTransferObjects/SkippedPipeDto.php | 4 +--- src/DataTransferObjects/UserActionDto.php | 3 ++- src/Pipes/StacheData.php | 2 +- src/Repositories/FileBackupRepository.php | 1 + src/Support/Zipper.php | 6 ++++-- 6 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Abstracts/BackupPipe.php b/src/Abstracts/BackupPipe.php index 1981ba0..5f98da5 100644 --- a/src/Abstracts/BackupPipe.php +++ b/src/Abstracts/BackupPipe.php @@ -40,6 +40,8 @@ protected function getDirectoryPath(string $path): string /** * Mark pipe as skipped. + * + * @param Closure(Zipper): Zipper $next */ protected function skip(string $reason, Closure $next, Zipper $zip): Zipper { diff --git a/src/DataTransferObjects/SkippedPipeDto.php b/src/DataTransferObjects/SkippedPipeDto.php index 0f1a298..eff8e95 100644 --- a/src/DataTransferObjects/SkippedPipeDto.php +++ b/src/DataTransferObjects/SkippedPipeDto.php @@ -8,10 +8,8 @@ final readonly class SkippedPipeDto { - /** - * @param class-string $pipe - */ public function __construct( + /** @var class-string */ public string $pipe, public string $reason, ) {} diff --git a/src/DataTransferObjects/UserActionDto.php b/src/DataTransferObjects/UserActionDto.php index 76c9c3d..fb26708 100644 --- a/src/DataTransferObjects/UserActionDto.php +++ b/src/DataTransferObjects/UserActionDto.php @@ -12,6 +12,7 @@ { public function __construct( public string $userId, + /** A human readable string */ public string $timestamp, ) {} @@ -22,7 +23,7 @@ public function getUser(): ?User public function getTimestamp(): CarbonImmutable { - return CarbonImmutable::createFromDate($this->timestamp); + return CarbonImmutable::parse($this->timestamp); } /** @return array{user_id: string, timestamp: string}*/ diff --git a/src/Pipes/StacheData.php b/src/Pipes/StacheData.php index 9c12fba..ecb1278 100644 --- a/src/Pipes/StacheData.php +++ b/src/Pipes/StacheData.php @@ -78,7 +78,7 @@ private static function realPath(Store $store): string private static function prefixer(Store $store): string { - return self::getKey() . '::' . $store->key(); + return self::getKey() . '/' . $store->key(); } private static function storeHasSafeDirectory(Store $store): bool diff --git a/src/Repositories/FileBackupRepository.php b/src/Repositories/FileBackupRepository.php index 9cafd06..5d313d8 100644 --- a/src/Repositories/FileBackupRepository.php +++ b/src/Repositories/FileBackupRepository.php @@ -30,6 +30,7 @@ public function __construct( $this->filesystem = Storage::disk(Config::string('backup.destination.disk')); } + /** {@inheritdoc} */ public function all(): Collection { return collect($this->filesystem->allFiles($this->path)) diff --git a/src/Support/Zipper.php b/src/Support/Zipper.php index fe350d6..012b8b0 100644 --- a/src/Support/Zipper.php +++ b/src/Support/Zipper.php @@ -10,6 +10,8 @@ use Symfony\Component\Finder\Finder; use ZipArchive; +use function Illuminate\Filesystem\join_paths; + // @mago-expect lint:too-many-methods,cyclomatic-complexity final class Zipper { @@ -179,7 +181,7 @@ public function addDirectory(string $path, ?string $prefix = null): self $finder->files()->ignoreDotFiles(false)->in($path); foreach ($finder as $file) { - $this->addFile($file->getPathname(), $prefix . '/' . $file->getRelativePathname()); + $this->addFile($file->getPathname(), join_paths($prefix, $file->getRelativePathname())); } return $this; @@ -201,7 +203,7 @@ public function extractTo(string $path, #[SensitiveParameter] ?string $password $res = $this->zip->extractTo($path); if (!$res) { - throw new (ZipperFailed::toExtract)($this->path, $path); + throw ZipperFailed::toExtract($this->path, $path); } return $this; From bbc296257bfd545b59b6faca11461b679631b98b Mon Sep 17 00:00:00 2001 From: NeoIsRecursive Date: Thu, 30 Jul 2026 12:28:57 +0200 Subject: [PATCH 10/11] fix tests --- src/Pipes/StacheData.php | 1 + tests/Unit/BackuperTest.php | 12 ++++++------ 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Pipes/StacheData.php b/src/Pipes/StacheData.php index ecb1278..89876c6 100644 --- a/src/Pipes/StacheData.php +++ b/src/Pipes/StacheData.php @@ -96,6 +96,7 @@ private static function storeHasSafeDirectory(Store $store): bool private static function shouldBackupStore(Store $store): bool { + // dd($store->key(), config('backup.stache_stores')); return in_array($store->key(), Config::array('backup.stache_stores', []), strict: true); } } diff --git a/tests/Unit/BackuperTest.php b/tests/Unit/BackuperTest.php index 5d68e01..4bbde9b 100644 --- a/tests/Unit/BackuperTest.php +++ b/tests/Unit/BackuperTest.php @@ -61,11 +61,11 @@ expect($paths)->toEqualCanonicalizing([ // since the default collection store and entries store have the same directory, we will get duplicates. - 'stache-content::collections/pages.yaml', - 'stache-content::collections/pages/homepage.md', - 'stache-content::entries/pages.yaml', - 'stache-content::entries/pages/homepage.md', - 'stache-content::form-submissions/1743066599.5568.yaml', + 'stache-content/collections/pages.yaml', + 'stache-content/collections/pages/homepage.md', + 'stache-content/entries/pages.yaml', + 'stache-content/entries/pages/homepage.md', + 'stache-content/form-submissions/1743066599.5568.yaml', 'users/test@example.com.yaml', ]); @@ -94,7 +94,7 @@ )->toArray(); expect($paths)->toEqualCanonicalizing([ - 'stache-content::form-submissions/1743066599.5568.yaml', + 'stache-content/form-submissions/1743066599.5568.yaml', 'users/test@example.com.yaml', ]); From 27b6991e61db32779a1e778c58b59cfedd8dc2fa Mon Sep 17 00:00:00 2001 From: NeoIsRecursive Date: Thu, 30 Jul 2026 13:13:12 +0200 Subject: [PATCH 11/11] fix some more thing --- src/Backuper.php | 8 +++++++- src/Http/Controllers/DownloadBackupController.php | 8 +++++--- src/Pipes/StacheData.php | 1 - src/Repositories/FileBackupRepository.php | 6 ++---- src/Support/Zipper.php | 14 ++++++-------- 5 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/Backuper.php b/src/Backuper.php index 0efe130..6c49486 100644 --- a/src/Backuper.php +++ b/src/Backuper.php @@ -60,11 +60,17 @@ public function backup(?Authenticatable $user = null): BackupDto // Only treat true fatal errors as a "killed mid-backup" scenario. if ( $error === null - || !in_array($error['type'], [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR], true) + || !in_array( + $error['type'], + [E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR], + strict: true, + ) ) { return; } + Log::error('backup failed due to timeout', $error); + if (File::exists($temp_zip_path)) { File::delete($temp_zip_path); } diff --git a/src/Http/Controllers/DownloadBackupController.php b/src/Http/Controllers/DownloadBackupController.php index be3dc40..139946b 100644 --- a/src/Http/Controllers/DownloadBackupController.php +++ b/src/Http/Controllers/DownloadBackupController.php @@ -38,9 +38,11 @@ public function __invoke(Request $request, string $id, BackupRepository $repo): callback: static function () use ($disk, $backup) { $stream = $disk->readStream($backup->path); - fpassthru($stream); - - fclose($stream); + try { + fpassthru($stream); + } finally { + fclose($stream); + } }, name: basename($backup->path), headers: [ diff --git a/src/Pipes/StacheData.php b/src/Pipes/StacheData.php index 89876c6..ecb1278 100644 --- a/src/Pipes/StacheData.php +++ b/src/Pipes/StacheData.php @@ -96,7 +96,6 @@ private static function storeHasSafeDirectory(Store $store): bool private static function shouldBackupStore(Store $store): bool { - // dd($store->key(), config('backup.stache_stores')); return in_array($store->key(), Config::array('backup.stache_stores', []), strict: true); } } diff --git a/src/Repositories/FileBackupRepository.php b/src/Repositories/FileBackupRepository.php index 5d313d8..8e86a12 100644 --- a/src/Repositories/FileBackupRepository.php +++ b/src/Repositories/FileBackupRepository.php @@ -73,7 +73,7 @@ public function remove(string $id): ?BackupDto return null; } - Storage::disk(Config::string('backup.destination.disk'))->delete($backup->path); + $this->filesystem->delete($backup->path); event(new BackupDeleted($backup)); @@ -83,8 +83,6 @@ public function remove(string $id): ?BackupDto public function empty(): bool { $this->all()->each(fn(BackupDto $backup): ?BackupDto => $this->remove($backup->id)); - return Storage::disk(Config::string('backup.destination.disk'))->deleteDirectory(Config::string( - 'backup.destination.path', - )); + return $this->filesystem->deleteDirectory($this->path); } } diff --git a/src/Support/Zipper.php b/src/Support/Zipper.php index 012b8b0..1b689ab 100644 --- a/src/Support/Zipper.php +++ b/src/Support/Zipper.php @@ -19,7 +19,7 @@ final class Zipper * File extensions that are already compressed and should be stored * without re-compression to save CPU cycles and I/O bandwidth. */ - private const ENCRYPTED_FILE_TYPES = [ + private const COMPRESSED_FILE_TYPES = [ 'zip', 'mp4', 'webm', @@ -91,10 +91,6 @@ public static function verify(string $path): bool return false; } - if (File::mimeType($path) !== 'application/zip') { - return false; - } - $zip = self::read($path); $valid = $zip->getArchive()->status === ZipArchive::ER_OK; @@ -122,13 +118,15 @@ public function close(): void */ public function encrypt(#[SensitiveParameter] string $password): self { - $this->zip->setPassword($password); + if (!$this->zip->setPassword($password)) { + throw ZipperFailed::toSetEncryption($this->path); + } for ($i = 0; $i < $this->zip->numFiles; $i++) { $encrypted = $this->zip->setEncryptionIndex($i, ZipArchive::EM_AES_256); if (!$encrypted) { - throw ZipperFailed::toSetEncryption($this->path); + throw ZipperFailed::toSetEncryption($this->zip->getNameIndex($i)); } } @@ -151,7 +149,7 @@ public function addFile(string $path, ?string $name = null): self } $extension = strtolower(pathinfo($entryName, PATHINFO_EXTENSION)); - $method = in_array($extension, self::ENCRYPTED_FILE_TYPES, true) + $method = in_array($extension, self::COMPRESSED_FILE_TYPES, true) ? ZipArchive::CM_STORE : ZipArchive::CM_DEFLATE;