nandung/s3-manager

Laravel package for managing multiple S3-compatible storage buckets with quota management, URL embedding, and synchronization

Fund package maintenance!
nandung-id

Installs: 1

Dependents: 0

Suggesters: 0

Security: 0

Stars: 0

Watchers: 0

Forks: 0

Open Issues: 0

pkg:composer/nandung/s3-manager

dev-main 2025-12-20 23:10 UTC

This package is auto-updated.

Last update: 2025-12-20 23:10:50 UTC


README

Latest Version on Packagist Total Downloads License

A powerful Laravel package for managing multiple S3-compatible storage buckets with quota management, URL embedding, file synchronization, and comprehensive file tracking.

Features

  • 🪣 Multi-Bucket Support - Manage multiple S3-compatible storage providers (AWS S3, Cloudflare R2, MinIO, DigitalOcean Spaces, etc.)
  • 📊 Quota Management - Set storage limits per bucket and globally with automatic enforcement
  • 🔗 URL Generation - Generate public URLs, presigned URLs, and embed proxy URLs
  • 🔄 Synchronization - Sync remote bucket contents with local database for fast queries
  • 📁 File Tracking - Track all uploaded files with metadata in your database
  • 🚀 Fluent API - Clean, chainable interface for all operations
  • 🎭 Facade Support - Use the convenient S3Manager facade
  • Queue Support - Background sync jobs for large buckets

Requirements

  • PHP 8.2 or higher
  • Laravel 10.x to 12.x
  • AWS SDK for PHP 3.x

Installation

Install the package via Composer:

composer require nandung/s3-manager

Publish the configuration file:

php artisan vendor:publish --tag=s3-manager-config

Run the migrations:

php artisan migrate

Or publish migrations first if you want to customize them:

php artisan vendor:publish --tag=s3-manager-migrations
php artisan migrate

Configuration

After publishing, configure your buckets in config/s3-manager.php:

return [
    // Global storage quota (in bytes), null for unlimited
    'global_quota' => env('S3_MANAGER_GLOBAL_QUOTA', null),

    // Embed proxy configuration
    'embed' => [
        'enabled' => true,
        'route_prefix' => 'e',
        'cache_ttl' => 3600,
    ],

    // Presigned URL configuration
    'presigned' => [
        'default_expiration' => 3600, // seconds
    ],

    // Sync configuration
    'sync' => [
        'queue' => 'default',
        'chunk_size' => 1000,
    ],

    // Bucket configurations
    'buckets' => [
        'default' => [
            'driver' => 's3',
            'key' => env('S3_DEFAULT_KEY'),
            'secret' => env('S3_DEFAULT_SECRET'),
            'region' => env('S3_DEFAULT_REGION', 'us-east-1'),
            'bucket' => env('S3_DEFAULT_BUCKET'),
            'endpoint' => env('S3_DEFAULT_ENDPOINT'),
            'public_base_url' => env('S3_DEFAULT_PUBLIC_URL'),
            'quota' => env('S3_DEFAULT_QUOTA', null), // bytes
            'options' => [
                'use_path_style_endpoint' => false,
            ],
        ],
    ],
];

Environment Variables

Add these to your .env file:

# Global quota (optional)
S3_MANAGER_GLOBAL_QUOTA=10737418240  # 10GB in bytes

# Default bucket configuration
S3_DEFAULT_KEY=your-access-key
S3_DEFAULT_SECRET=your-secret-key
S3_DEFAULT_REGION=us-east-1
S3_DEFAULT_BUCKET=your-bucket-name
S3_DEFAULT_ENDPOINT=https://s3.amazonaws.com
S3_DEFAULT_PUBLIC_URL=https://your-bucket.s3.amazonaws.com
S3_DEFAULT_QUOTA=5368709120  # 5GB in bytes

Usage

Basic Usage with Facade

use Nandung\S3Manager\Facades\S3Manager;

// Upload a file
$fileRecord = S3Manager::upload('default', 'images/photo.jpg', $fileContents);

// Upload with options
$fileRecord = S3Manager::upload('default', 'documents/report.pdf', $contents, [
    'ContentType' => 'application/pdf',
    'Metadata' => ['author' => 'John Doe'],
]);

// Download a file
$stream = S3Manager::download('default', 'images/photo.jpg');
$contents = $stream->getContents();

// Delete a file
S3Manager::delete('default', 'images/photo.jpg');

// Check if file exists
if (S3Manager::exists('default', 'images/photo.jpg')) {
    // File exists
}

// List files
$files = S3Manager::list('default', 'images/', recursive: true);

Fluent Bucket API

use Nandung\S3Manager\Facades\S3Manager;

// Get bucket instance for fluent operations
$bucket = S3Manager::bucket('default');

// All operations are now scoped to this bucket
$fileRecord = $bucket->upload('images/photo.jpg', $contents);
$stream = $bucket->download('images/photo.jpg');
$bucket->delete('images/photo.jpg');
$exists = $bucket->exists('images/photo.jpg');
$files = $bucket->list('images/');

URL Generation

use Nandung\S3Manager\Facades\S3Manager;

// Public URL (requires public_base_url configuration)
$publicUrl = S3Manager::publicUrl('default', 'images/photo.jpg');
// Result: https://your-bucket.s3.amazonaws.com/images/photo.jpg

// Presigned URL (temporary access)
$presignedUrl = S3Manager::presignedUrl('default', 'images/photo.jpg', 3600);
// Result: https://...?X-Amz-Signature=...

// Presigned URL for upload
$uploadUrl = S3Manager::presignedUrl('default', 'uploads/new-file.jpg', 3600, 'PUT');

// Embed URL (proxied through your application)
$embedUrl = S3Manager::embedUrl('default', 'images/photo.jpg');
// Result: /e/default/images/photo.jpg

Quota Management

use Nandung\S3Manager\Facades\S3Manager;

// Get bucket usage
$usage = S3Manager::getUsage('default');
echo "Used: " . $usage->used . " bytes";
echo "Limit: " . $usage->limit . " bytes";
echo "Files: " . $usage->fileCount;
echo "Percent: " . $usage->percentUsed . "%";
echo "Unlimited: " . ($usage->isUnlimited ? 'Yes' : 'No');

// Get global usage (across all buckets)
$globalUsage = S3Manager::getUsage();

Quota exceptions are thrown automatically when limits are exceeded:

use Nandung\S3Manager\Exceptions\QuotaExceededException;
use Nandung\S3Manager\Exceptions\GlobalQuotaExceededException;

try {
    S3Manager::upload('default', 'large-file.zip', $contents);
} catch (QuotaExceededException $e) {
    // Bucket quota exceeded
    echo "Bucket {$e->bucketId} quota exceeded";
    echo "Limit: {$e->limit}, Used: {$e->used}, Attempted: {$e->attempted}";
} catch (GlobalQuotaExceededException $e) {
    // Global quota exceeded
    echo "Global storage quota exceeded";
}

Synchronization

Sync remote bucket contents with your local database:

use Nandung\S3Manager\Facades\S3Manager;

// Sync bucket (updates local database with remote state)
$result = S3Manager::sync('default');

echo "Added: " . $result->added;
echo "Updated: " . $result->updated;
echo "Deleted: " . $result->deleted;
echo "Total Files: " . $result->totalFiles;
echo "Total Size: " . $result->totalSize;
echo "Duration: " . $result->duration . " seconds";

if ($result->hasErrors()) {
    foreach ($result->errors as $error) {
        echo "Error: " . $error;
    }
}

Dependency Injection

use Nandung\S3Manager\Contracts\S3ManagerInterface;

class FileService
{
    public function __construct(
        private S3ManagerInterface $s3Manager
    ) {}

    public function uploadUserAvatar(User $user, $contents): string
    {
        $path = "avatars/{$user->id}.jpg";
        $this->s3Manager->upload('default', $path, $contents);
        return $this->s3Manager->publicUrl('default', $path);
    }
}

Multiple Bucket Configuration

AWS S3

'buckets' => [
    'aws' => [
        'driver' => 's3',
        'key' => env('AWS_ACCESS_KEY_ID'),
        'secret' => env('AWS_SECRET_ACCESS_KEY'),
        'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
        'bucket' => env('AWS_BUCKET'),
        'endpoint' => null, // Use default AWS endpoint
        'public_base_url' => env('AWS_URL'),
        'quota' => null,
        'options' => [],
    ],
],

Cloudflare R2

'buckets' => [
    'r2' => [
        'driver' => 'r2',
        'key' => env('R2_ACCESS_KEY_ID'),
        'secret' => env('R2_SECRET_ACCESS_KEY'),
        'region' => 'auto',
        'bucket' => env('R2_BUCKET'),
        'endpoint' => env('R2_ENDPOINT'), // https://<account_id>.r2.cloudflarestorage.com
        'public_base_url' => env('R2_PUBLIC_URL'),
        'quota' => null,
        'options' => [
            'use_path_style_endpoint' => false,
        ],
    ],
],

MinIO (Self-hosted)

'buckets' => [
    'minio' => [
        'driver' => 'minio',
        'key' => env('MINIO_ACCESS_KEY'),
        'secret' => env('MINIO_SECRET_KEY'),
        'region' => 'us-east-1',
        'bucket' => env('MINIO_BUCKET'),
        'endpoint' => env('MINIO_ENDPOINT', 'http://localhost:9000'),
        'public_base_url' => env('MINIO_PUBLIC_URL'),
        'quota' => 1073741824, // 1GB
        'options' => [
            'use_path_style_endpoint' => true,
        ],
    ],
],

DigitalOcean Spaces

'buckets' => [
    'spaces' => [
        'driver' => 'spaces',
        'key' => env('DO_SPACES_KEY'),
        'secret' => env('DO_SPACES_SECRET'),
        'region' => env('DO_SPACES_REGION', 'nyc3'),
        'bucket' => env('DO_SPACES_BUCKET'),
        'endpoint' => env('DO_SPACES_ENDPOINT'), // https://nyc3.digitaloceanspaces.com
        'public_base_url' => env('DO_SPACES_URL'),
        'quota' => null,
        'options' => [],
    ],
],

Embed Proxy

The embed proxy allows you to serve files through your application, useful for:

  • Hiding actual S3 URLs
  • Adding authentication/authorization
  • Caching headers control
  • Analytics tracking

Configuration

'embed' => [
    'enabled' => true,
    'route_prefix' => 'e',  // URL prefix
    'cache_ttl' => 3600,    // Cache duration in seconds
],

Usage

// Generate embed URL
$embedUrl = S3Manager::embedUrl('default', 'images/photo.jpg');
// Result: /e/default/images/photo.jpg

// Use in views
<img src="{{ S3Manager::embedUrl('default', 'images/photo.jpg') }}" alt="Photo">

The embed route automatically handles:

  • Content-Type headers
  • Cache-Control headers
  • ETag headers
  • Last-Modified headers

Database Schema

The package creates two tables:

s3_manager_files

Tracks all files across buckets:

Column Type Description
id bigint Primary key
bucket_id string(64) Bucket identifier
path string(1024) File path in bucket
size bigint File size in bytes
mime_type string(128) MIME type
etag string(64) S3 ETag
last_modified timestamp Last modification time
metadata json Custom metadata
created_at timestamp Record creation time
updated_at timestamp Record update time

s3_manager_bucket_usage

Tracks usage statistics per bucket:

Column Type Description
id bigint Primary key
bucket_id string(64) Bucket identifier
total_size bigint Total storage used
file_count bigint Number of files
last_synced_at timestamp Last sync time
created_at timestamp Record creation time
updated_at timestamp Record update time

Exception Handling

The package provides specific exceptions for different error scenarios:

use Nandung\S3Manager\Exceptions\BucketNotFoundException;
use Nandung\S3Manager\Exceptions\FileNotFoundException;
use Nandung\S3Manager\Exceptions\QuotaExceededException;
use Nandung\S3Manager\Exceptions\GlobalQuotaExceededException;
use Nandung\S3Manager\Exceptions\PublicUrlNotConfiguredException;
use Nandung\S3Manager\Exceptions\SyncException;

try {
    S3Manager::upload('unknown-bucket', 'file.txt', 'content');
} catch (BucketNotFoundException $e) {
    // Bucket not configured
}

try {
    S3Manager::download('default', 'non-existent.txt');
} catch (FileNotFoundException $e) {
    // File doesn't exist
}

try {
    S3Manager::publicUrl('default', 'file.txt');
} catch (PublicUrlNotConfiguredException $e) {
    // public_base_url not set for bucket
}

API Reference

S3Manager Methods

Method Description
bucket(string $bucketId) Get fluent bucket interface
upload(string $bucketId, string $path, mixed $contents, array $options = []) Upload a file
download(string $bucketId, string $path) Download a file
delete(string $bucketId, string $path) Delete a file
exists(string $bucketId, string $path) Check if file exists
list(string $bucketId, string $prefix = '', bool $recursive = false) List files
publicUrl(string $bucketId, string $path) Generate public URL
presignedUrl(string $bucketId, string $path, ?int $expiration = null, string $method = 'GET') Generate presigned URL
embedUrl(string $bucketId, string $path) Generate embed proxy URL
sync(string $bucketId) Sync bucket with database
getUsage(?string $bucketId = null) Get usage statistics

FileRecord Model

$fileRecord = S3Manager::upload('default', 'file.txt', 'content');

$fileRecord->bucket_id;    // string
$fileRecord->path;         // string
$fileRecord->size;         // int (bytes)
$fileRecord->mime_type;    // string
$fileRecord->etag;         // string
$fileRecord->last_modified; // Carbon
$fileRecord->metadata;     // array

UsageInfo Object

$usage = S3Manager::getUsage('default');

$usage->used;        // int (bytes)
$usage->limit;       // int|null (bytes)
$usage->fileCount;   // int
$usage->percentUsed; // float
$usage->isUnlimited; // bool

SyncResult Object

$result = S3Manager::sync('default');

$result->added;      // int
$result->updated;    // int
$result->deleted;    // int
$result->totalFiles; // int
$result->totalSize;  // int (bytes)
$result->duration;   // float (seconds)
$result->errors;     // array
$result->hasErrors(); // bool

Testing

composer test

Changelog

Please see CHANGELOG for more information on what has changed recently.

Contributing

Please see CONTRIBUTING for details.

Security Vulnerabilities

If you discover a security vulnerability, please send an email to nandung@example.com. All security vulnerabilities will be promptly addressed.

Credits

License

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