mohamedsamy902/advanced-file-upload

A production-ready Laravel package for chunked uploads, image processing, URL downloads, multi-cloud storage, SSRF protection, and resumable file transfers.

Maintainers

Package info

github.com/MohamedSamy902/uplade-file-chunk

pkg:composer/mohamedsamy902/advanced-file-upload

Statistics

Installs: 25

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v2.0.0 2026-06-03 11:34 UTC

This package is auto-updated.

Last update: 2026-06-03 12:22:04 UTC


README

A production-ready Laravel package for chunked uploads, image processing, URL downloads, multi-cloud storage, and resumable file transfers.

PHP Laravel License: MIT Tests

Contents

Requirements

Requirement Version
PHP ^8.1
Laravel >=10.0
Intervention Image ^3.0
GD or Imagick Required for image processing

Installation

Install the package via Composer:

composer require mohamedsamy902/advanced-file-upload

Publish the configuration file:

php artisan vendor:publish --provider="MohamedSamy902\AdvancedFileUpload\AdvancedFileUploadServiceProvider" --tag=config

Publish and run the database migrations (required for file tracking or resumable uploads):

php artisan vendor:publish --provider="MohamedSamy902\AdvancedFileUpload\AdvancedFileUploadServiceProvider" --tag=migrations
php artisan migrate

For cloud storage, install the appropriate adapter:

# Amazon S3
composer require league/flysystem-aws-s3-v3

# Google Cloud Storage
composer require spatie/laravel-google-cloud-storage

Configuration

After publishing, the configuration file is located at config/file-upload.php.

The key sections are:

return [
    'storage' => [
        'disk'           => env('FILE_UPLOAD_DISK', 'public'),
        'path'           => env('FILE_UPLOAD_PATH', 'uploads'),
        'default_folder' => null,
        'cdn' => [
            'enabled' => env('FILE_UPLOAD_CDN_ENABLED', false),
            'url'     => env('FILE_UPLOAD_CDN_URL', ''),
        ],
    ],

    'url_upload' => [
        'allowed_domains' => [],        // empty = allow all public domains
        'timeout_seconds' => 10,
        'max_size_bytes'  => 52428800,  // 50 MB
    ],

    'image_driver' => env('FILE_UPLOAD_IMAGE_DRIVER', 'gd'),  // 'gd' or 'imagick'

    'processing' => [
        'image' => [
            'enabled'    => false,
            'convert_to' => null,       // 'webp', 'jpg', 'png'
            'quality'    => 85,
            'max_width'  => 1920,
            'max_height' => null,
            'upsize'     => false,
        ],
    ],

    'thumbnails' => [
        'enabled' => false,
        'sizes'   => [
            'sm' => ['width' => 150, 'height' => 150, 'crop' => true],
            'md' => ['width' => 400, 'height' => 400, 'crop' => false],
        ],
    ],

    'quota' => [
        'enabled'   => false,
        'per_user'  => 1073741824, // 1 GB
    ],

    'chunked' => [
        'session_ttl_hours' => 24,
    ],

    'database' => [
        'enabled' => false,
        'model'   => \MohamedSamy902\AdvancedFileUpload\Models\FileUpload::class,
    ],
];

Usage

Single File Upload

use MohamedSamy902\AdvancedFileUpload\Facades\FileUpload;

public function store(Request $request)
{
    $result = FileUpload::upload($request->file('avatar'));

    // $result is a typed value object
    return response()->json([
        'path' => $result->path,
        'url'  => $result->url,
        'type' => $result->type, // image | video | audio | pdf | document | other
    ]);
}

The return value is an UploadResult object with these properties:

Property Type Description
status bool Whether the upload succeeded
path string Storage path relative to the disk root
url string Public URL (with CDN rewriting if enabled)
originalName string The original client filename
mimeType string The detected MIME type
type string Logical category: image, video, audio, pdf, document, other
size `int null`
thumbnailUrls array Map of thumbnail size name to URL
databaseId `int null`

UploadResult also implements ArrayAccess, so existing code using array syntax continues to work:

$result = FileUpload::upload($file);
$path = $result['path'];    // still works
$url  = $result['url'];     // still works

Batch Upload

Pass an array of files. Failed items return an error array without interrupting the batch.

$files   = $request->file('documents');
$results = FileUpload::upload($files, [
    'folder_name'      => 'reports',
    'validation_rules' => ['file' => 'required|file|mimes:pdf,docx|max:10240'],
]);

foreach ($results as $result) {
    if ($result instanceof \MohamedSamy902\AdvancedFileUpload\ValueObjects\UploadResult) {
        // success
        echo $result->url;
    } else {
        // failure — $result is an array with 'status', 'error', 'original_name'
        echo $result['error'];
    }
}

URL Download and Upload

Download a remote file and store it, with SSRF protection applied before any HTTP request.

$result = FileUpload::upload([], [
    'url'         => 'https://cdn.example.com/photo.jpg',
    'folder_name' => 'avatars',
]);

Multiple URLs in a single call:

$results = FileUpload::upload([], [
    'url' => [
        'https://cdn.example.com/image1.jpg',
        'https://cdn.example.com/image2.jpg',
        'https://cdn.example.com/image3.jpg',
    ],
    'folder_name' => 'gallery',
]);

Restricting permitted domains:

// config/file-upload.php
'url_upload' => [
    'allowed_domains' => ['cdn.myapp.com', 'assets.partner.com'],
],

Resumable Chunked Upload

When a client uploads a large file and the connection is interrupted, the upload can resume from the last successful chunk without starting over.

Step 1 — Start a session on your backend:

use MohamedSamy902\AdvancedFileUpload\Services\ResumableUploadService;

public function startUpload(Request $request, ResumableUploadService $service)
{
    $session = $service->startSession(
        originalName: $request->input('filename'),
        mimeType:     $request->input('mime_type'),
        totalSize:    (int) $request->input('total_size'),
        totalChunks:  (int) $request->input('total_chunks'),
        folder:       'uploads',
    );

    return response()->json(['session_id' => $session->session_id]);
}

Step 2 — Send each chunk:

public function uploadChunk(Request $request, ResumableUploadService $service)
{
    $state = $service->uploadChunk(
        sessionId:  $request->input('session_id'),
        chunkIndex: (int) $request->input('chunk_index'),
        chunk:      $request->file('chunk'),
    );

    return response()->json($state);
    // {"received": 3, "total": 10, "missing": [3, 4, 5, 6, 7, 8, 9]}
}

Step 3 — Resume after a failure:

The client queries which chunks are still missing:

public function sessionStatus(string $sessionId, ResumableUploadService $service)
{
    return response()->json($service->getSession($sessionId));
    // {"session_id": "...", "status": "pending", "received": 3, "total": 10, "missing": [3, 4, ...]}
}

The client re-sends only the missing chunks, then calls complete.

Step 4 — Assemble and store:

public function completeUpload(Request $request, ResumableUploadService $service)
{
    $result = $service->completeSession($request->input('session_id'));

    return response()->json([
        'path' => $result->path,
        'url'  => $result->url,
    ]);
}

Image Processing

Enable image processing in the configuration:

'processing' => [
    'image' => [
        'enabled'    => true,
        'convert_to' => 'webp',   // convert all uploads to WebP
        'quality'    => 85,
        'max_width'  => 1920,
        'upsize'     => false,    // never enlarge images smaller than max_width
    ],
],

Override per request:

$result = FileUpload::upload($file, [
    'convert_to' => 'jpg',
    'quality'    => 75,
]);

Watermark:

// config/file-upload.php
'processing' => [
    'image' => [
        'enabled'    => true,
        'watermark'  => [
            'enabled'   => true,
            'path'      => storage_path('app/watermark.png'),
            'position'  => 'bottom-right',
            'opacity'   => 50,
        ],
    ],
],

Thumbnails:

'thumbnails' => [
    'enabled' => true,
    'sizes'   => [
        'sm' => ['width' => 150, 'height' => 150, 'crop' => true],
        'md' => ['width' => 400, 'height' => 400, 'crop' => false],
        'lg' => ['width' => 800, 'height' => null, 'crop' => false],
    ],
],

The thumbnail URLs are available in the result:

$result = FileUpload::upload($imageFile);
echo $result->thumbnailUrls['sm']; // https://cdn.myapp.com/uploads/thumbs/sm_uuid.webp
echo $result->thumbnailUrls['md'];

CDN URL Rewriting

When a CDN is configured, all generated URLs are automatically rewritten:

// config/file-upload.php
'storage' => [
    'cdn' => [
        'enabled' => true,
        'url'     => 'https://cdn.myapp.com',
    ],
],
$result = FileUpload::upload($file);
echo $result->url;
// https://cdn.myapp.com/uploads/uuid.jpg

Deleting Files

By storage path (when database tracking is disabled):

FileUpload::delete('uploads/uuid.jpg');

By database record ID (when tracking is enabled):

FileUpload::delete(42);

Delete multiple files. Partial failures do not abort the batch:

$results = FileUpload::delete([
    'uploads/file1.jpg',
    'uploads/file2.pdf',
    'uploads/file3.mp4',
]);

foreach ($results as $result) {
    if (!$result['status']) {
        logger()->error($result['error']);
    }
}

Security

SSRF Protection

All URL downloads are validated before any HTTP request is issued. The following are blocked automatically:

Private IP address ranges (RFC 1918 and related):

Range Description
127.0.0.0/8 IPv4 loopback
10.0.0.0/8 Private network
172.16.0.0/12 Private network
192.168.0.0/16 Private network
169.254.0.0/16 Link-local (AWS Metadata endpoint)
100.64.0.0/10 Shared address space (RFC 6598)
::1/128 IPv6 loopback
fc00::/7 IPv6 unique local
fe80::/10 IPv6 link-local

Disallowed URL schemes:

Only http and https are permitted. The following are blocked: file://, ftp://, gopher://, data:, and all others.

Domain allowlist:

To restrict URL downloads to specific domains:

// config/file-upload.php
'url_upload' => [
    'allowed_domains' => ['cdn.myapp.com', 'assets.partner.com'],
],

An empty array permits any public domain.

Replacing Security Components

Every service is bound to a contract interface. To replace the SSRF validator with your own implementation:

// In AppServiceProvider::register()
use MohamedSamy902\AdvancedFileUpload\Contracts\SsrfValidatorContract;

$this->app->bind(SsrfValidatorContract::class, MyCustomSsrfValidator::class);

The same applies to ImageProcessorContract, QuotaManagerContract, and FileUploadContract.

Testing

Run the full test suite:

composer test

Run static analysis:

composer analyse

The package ships with 140 tests covering:

  • Security attack simulation (SSRF, disguised files, DoS)
  • Performance benchmarks (single file, batch of 50, memory usage)
  • Error handling and edge cases
  • Image processing with Intervention Image v3
  • Resumable upload session management
  • All public API surface

Changelog

v2.0.0

  • Upgraded Intervention Image from v2 to v3
  • Minimum PHP version raised to 8.1
  • Minimum Laravel version raised to 10
  • Moved cloud storage packages (league/flysystem-aws-s3-v3, spatie/laravel-google-cloud-storage) to suggest
  • Implemented comprehensive SSRF protection for URL downloads
  • Added UploadResult typed value object (implements ArrayAccess for backward compatibility)
  • Added resumable chunked upload system with session management
  • Refactored monolithic service into focused, contract-based components
  • Added 107 new tests (total: 140 tests, 345 assertions)
  • Added GitHub Actions CI matrix for PHP 8.1–8.4 and Laravel 10–13

v1.0.0

  • Initial release

Contributing

Contributions are welcome. Please open an issue first to discuss the change you intend to make.

  1. Fork the repository
  2. Create a feature branch: git checkout -b feature/your-feature
  3. Write tests for your change
  4. Run the test suite: composer test
  5. Submit a pull request

License

The MIT License (MIT). Please see LICENSE for more information.