Skip to content

feat: error handling and performance optimization#78

Open
andreasbergqvist wants to merge 5 commits into
mainfrom
feature/performance
Open

feat: error handling and performance optimization#78
andreasbergqvist wants to merge 5 commits into
mainfrom
feature/performance

Conversation

@andreasbergqvist

Copy link
Copy Markdown

Prevent invalid backups on low-performance and I/O-limited shared hosting

Problem

On lower-performance systems (e.g., shared hosting with 512MB RAM and 20MB/s I/O limits like Oderland), backups were producing invalid/corrupt zip files. The root causes were:

  1. Zero error checking on ZipArchive operationsopen(), close(), addFile(), and addFromString() all had their return values ignored. When close() failed (disk full, memory exhaustion, I/O timeout), the code proceeded to move a half-written corrupt zip to the repository as if it succeeded.

  2. No zip integrity verification — corrupt zips were moved to the repository without ever being validated.

  3. Re-compressing already-compressed files — Statamic sites store PNG, JPG, PDF, MP4, etc. These formats are already fully compressed. Attempting deflate compression on them wastes CPU cycles, increases I/O bandwidth (read + compress + write instead of just read + write), and makes close() significantly slower — increasing the window for max_execution_time kills.

  4. addDirectory() loaded all file paths into memorycollect(File::allFiles($path))->each() created a Collection of every SplFileInfo before iterating. For sites with many assets, this could exhaust PHP's memory_limit before close() even runs.

  5. No safety net for process kills — if PHP was killed by max_execution_time during close() (a fatal error, not catchable by try/catch), the temp zip was left on disk, the state was stuck at backup_in_progress, and no error was logged.

  6. No logging — impossible to diagnose failures on remote systems.

Solution

Selective compression (Zipper::addFile)

Media and archive extensions (png, jpg, mp4, pdf, zip, etc.) now use ZipArchive::CM_STORE instead of CM_DEFLATE. This packages pre-compressed assets at raw write speed with zero CPU processing and zero compression buffer overhead. Text-based files (YAML, markdown, config) continue to use CM_DEFLATE for size reduction.

This reduces I/O peak by up to 90% on asset-heavy sites and makes close() dramatically faster — directly addressing the 20MB/s I/O throttling issue on shared hosting.

Error checking on all ZipArchive operations (Zipper)

Every ZipArchive method that returns a success/failure indicator now has its return value checked. Failures throw RuntimeException with descriptive messages instead of silently producing corrupt zips. This is the primary fix for invalid zips — close() returning false is now a loud error, not a silent corruption.

Zip verification (Zipper::verify, Backuper)

After close() succeeds, the zip is re-opened to verify integrity (numFiles > 0). If verification fails, the temp file is deleted and an exception is thrown — corrupt zips never reach the repository.

Lazy file iteration (Zipper::addDirectory)

Replaced collect(File::allFiles($path))->each() with a lazy Symfony Finder foreach loop. Files are processed one at a time instead of loading all paths into memory first. Progress is logged every 500 files.

Process kill safety net (Backuper)

  • set_time_limit(0) — removes PHP's execution time limit so slow I/O doesn't cause a timeout kill. Guarded with function_exists() for hosts that disable it.
  • ignore_user_abort(true) — prevents HTTP client disconnects from killing the process.
  • register_shutdown_function() — runs after fatal errors (including max_execution_time kills). If the backup didn't complete, it cleans up the temp file, sets state to BackupFailed, and logs the error. This catches the exact scenario where PHP dies mid-close().

Temp file cleanup (Backuper)

The catch block now deletes the temp zip if it exists. Previously, failed backups left orphaned temp.zip files consuming disk space.

Logging (Backuper, Zipper)

Added Log::info/Log::error calls at key points: backup start, zip close (with size), verification, completion, failure (with error message), and per-directory progress. Uses Laravel's default log channel — no config changes needed.

Files changed

File Changes
src/Support/Zipper.php Selective compression, error checking on all ZipArchive ops, lazy Finder iteration, verify() method, progress logging
src/Backuper.php Zip verification after close, temp file cleanup on failure, set_time_limit/ignore_user_abort/register_shutdown_function, logging
tests/Unit/ZipperTest.php 5 new tests: CM_STORE for media, CM_DEFLATE for text, verify valid/invalid zip, throws on open failure

Compatibility

  • No new dependencies
  • No API changes (public Zipper interface is identical)
  • No config changes
  • Existing backups remain readable (mixed compression methods are valid per the ZIP spec)
  • Encryption works with selective compression (setCompressionName and setEncryptionIndex are independent)
  • All 105 existing tests pass

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens the backup/zip creation flow to avoid producing and persisting corrupt archives on low-performance / I/O-limited hosts by adding ZipArchive error handling, selective compression, integrity verification, safer directory iteration, and additional logging around backup lifecycle events.

Changes:

  • Adds ZipArchive return-value checks, selective CM_STORE vs CM_DEFLATE compression, and a Zipper::verify() helper.
  • Improves backup robustness with temp cleanup, post-close verification, and a shutdown handler to recover from fatal/timeout termination.
  • Extends unit coverage for compression selection and verification behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 4 comments.

File Description
tests/Unit/ZipperTest.php Adds unit tests for compression method selection and zip verification behavior.
src/Support/Zipper.php Introduces selective compression, adds error handling for several ZipArchive operations, adds verify(), and switches addDirectory() to lazy iteration with progress logging.
src/Backuper.php Adds lifecycle logging, verification before repository persistence, temp zip cleanup on failure, and a shutdown handler intended to recover from fatal termination mid-backup.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Support/Zipper.php
Comment thread src/Support/Zipper.php
Comment thread src/Support/Zipper.php
Comment thread src/Backuper.php
andreasbergqvist and others added 3 commits July 3, 2026 00:19
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
@andreasbergqvist andreasbergqvist changed the title Error handling and performance optimization feat: error handling and performance optimization Jul 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants