Complete website in Rs. 5,000 with Free Hosting & Domain. Offer ends in  00:00:00
Back to Blog

How to Create a Zip Backup of Laravel's storage Folder (With Exclusions)

Backing up user-uploaded files is just as important as backing up your database. In this tutorial, you’ll learn how to create a custom Laravel Artisan command that zips your storage/app/public folder, optionally excluding certain files or directories (like .gitignore, assets, or even the backup folder itself), and stores the zip file in a downloadable location.

Apr 27, 2025 Updated: Apr 27, 2025

Backing up user-uploaded files is just as important as backing up your database. In this tutorial, you’ll learn how to create a custom Laravel Artisan command that zips your storage/app/public folder, optionally excluding certain files or directories (like .gitignore, assets, or even the backup folder itself), and stores the zip file in a downloadable location.

What We’ll Build

A custom Artisan command:

  • Zips the entire storage/app/public folder
  • Excludes specific folders/files
  • Stores the zip in storage/app/public/backups
  • Returns a public URL for downloading the backup

Folder Structure We’re Targeting

storage/
└── app/
    └── public/
        ├── uploads/
        ├── images/
        ├── backups/      <-- We'll store zips here
        ├── assets/       <-- We might want to exclude this
        └── .gitignore    <-- Also excluded

The Artisan Command Code

php artisan make:command CreateStorageBackup

Then paste the following code in app/Console/Commands/CreateStorageBackup.php

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
use ZipArchive;
use RecursiveIteratorIterator;
use RecursiveDirectoryIterator;

class CreateStorageBackup extends Command
{
    protected $signature = 'create-storage-backup {--exclude=*}';
    protected $description = 'Create a zip backup of storage/app/public';

    public function handle()
    {
        $exclude = array_filter(
            array_merge($this->option('exclude') ?? [], [
                '.gitignore',
                'backups', // backups are placed here
                'assets', // theme files are placed here
            ])
        );

        $storagePath = storage_path('app/public');
        $tempPath = storage_path('app/backup-temp');
        $finalBackupPath = storage_path('app/public/backups');

        // Ensure clean temp and final directories
        if (!is_dir($tempPath))
            mkdir($tempPath, 0755, true);
        if (!is_dir($finalBackupPath))
            mkdir($finalBackupPath, 0755, true);

        $datetime = now()->format('Y-m-d-H-i-s');
        $filename = "storage-$datetime.zip";
        $tempZipPath = $tempPath . DIRECTORY_SEPARATOR . $filename;
        $finalZipPath = $finalBackupPath . DIRECTORY_SEPARATOR . $filename;

        $zip = new ZipArchive;
        if ($zip->open($tempZipPath, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== true) {
            $this->error('Could not create zip file.');
            return 1;
        }

        $this->addFolderToZip($zip, $storagePath, $exclude);
        $zip->close();

        // Move to final path
        if (!rename($tempZipPath, $finalZipPath)) {
            $this->error('Failed to move zip to final destination.');
            return 1;
        }

        $url = Storage::url("backups/$filename");
        $this->info("Backup created: " . url($url));
        return 0;
    }

    protected function addFolderToZip(ZipArchive $zip, $sourcePath, $exclude = [])
    {
        $sourcePath = realpath($sourcePath);

        $files = new RecursiveIteratorIterator(
            new RecursiveDirectoryIterator($sourcePath, \FilesystemIterator::SKIP_DOTS),
            RecursiveIteratorIterator::SELF_FIRST
        );

        foreach ($files as $file) {
            $filePath = $file->getRealPath();
            $relativePath = str_replace('\\', '/', str_replace($sourcePath . DIRECTORY_SEPARATOR, '', $filePath));

            foreach ($exclude as $ex) {
                $ex = str_replace('\\', '/', trim($ex, '/'));
                if (
                    $relativePath === $ex ||                  // exact file or folder match
                    str_starts_with($relativePath, "$ex/") // folder with content
                ) {
                    continue 2;
                }
            }

            if ($file->isDir()) {
                $zip->addEmptyDir($relativePath);
            } elseif ($file->isFile()) {
                $zip->addFile($filePath, $relativePath);
            }
        }
    }
}

How It Works

Excluding Folders and Files

We allow passing exclusions via --exclude like this:

php artisan create-storage-backup --exclude=images --exclude=logo.png

But we also hardcode some common ones:

[
    '.gitignore',
    'backups', // backups go here
    'assets',  // theme files
]

The logic inside addFolderToZip skips any file/folder that matches or starts with one of these entries.

Temporary Zip Creation

We create the zip file in a temporary path:

$tempPath = storage_path('app/backup-temp');

Then move it to:

$finalBackupPath = storage_path('app/public/backups');

This keeps the public folder clean and avoids zipping while writing.

Returning the Public URL

Once the zip is moved, we use Laravel’s Storage::url() helper to generate a public URL:

$url = Storage::url("backups/$filename");
$this->info("Backup created: " . url($url));

Make sure your symbolic link to storage is set up:

php artisan storage:link

Example Output

$ php artisan create-storage-backup

Backup created: http://your-app.test/storage/backups/storage-2025-04-25-17-35-52.zip

Optional: Listing All Old Backups

To list all backup zip files:

$files = Storage::files('backups');
$backups = array_filter($files, fn($f) => str_ends_with($f, '.zip'));

You could build another Artisan command or a controller endpoint to show available backups.

Final Tips

  • This works seamlessly on both Linux and Windows
  • For large backups, consider using queue workers or async jobs
  • You can exclude more sensitive or unneeded files by adding them to the exclusion list

With this command, you’re now one step closer to automating file backups in your Laravel app. It’s a simple but powerful tool you can schedule via cron, expose via an admin dashboard, or even trigger via a webhook.

Automation

In the routes/console.php file, write the code below and cron job will automaticall create backup.

use Illuminate\Support\Facades\Schedule;

Schedule::command('create-storage-backup')->dailyAt('23:59')->withoutOverlapping();
Contact

Got A Question For Us?

Feel free to ask anything directly on call or fill the form and we will contact back within few hours.