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
Requires
- php: ^8.2
- aws/aws-sdk-php: ^3.0
- laravel/framework: ^10.0|^11.0|^12.0
Requires (Dev)
- mockery/mockery: ^1.6
- orchestra/testbench: ^8.0|^9.0
- phpunit/phpunit: ^10.0
This package is auto-updated.
Last update: 2025-12-20 23:10:50 UTC
README
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
S3Managerfacade - ⚡ 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.