kaibatech / viettel-cloud-s3
Viettel Cloud Object Storage - Laravel Storage Driver for VIPCore/EMC ViPR S3-compatible endpoints
Requires
- php: ^8.2
- illuminate/filesystem: ^10.0|^11.0|^12.0
- illuminate/support: ^10.0|^11.0|^12.0
- league/flysystem: ^3.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0
- phpunit/phpunit: ^10.0|^11.0
README
A Laravel Storage driver for Viettel Cloud Object Storage and other VIPCore/EMC ViPR S3-compatible endpoints that have signature compatibility issues with the standard AWS SDK for PHP.
✨ Features
- ✅ Full Laravel Storage integration - Use familiar
Storage::disk()
methods - ✅ Upload, download, delete files with proper error handling
- ✅ File existence checks and metadata retrieval
- ✅ Public/private file visibility support (with ACL headers)
- ✅ MIME type detection for uploaded files
- ✅ URL generation for public file access
- ✅ Custom AWS v4 signature calculation compatible with VIPCore/EMC ViPR
- ✅ UNSIGNED-PAYLOAD support required by some S3-compatible services
- ✅ Laravel 10.x, 11.x & 12.x support
🚀 Installation
Install the package via Composer:
composer require kaibatech/viettel-cloud-s3
Laravel Auto-Discovery
The package uses Laravel's auto-discovery feature, so the service provider will be registered automatically.
For Laravel versions that don't support auto-discovery, add the service provider to your config/app.php
:
'providers' => [ // ... Kaibatech\ViettelCloudS3\ViettelCloudS3ServiceProvider::class, ],
Publish Configuration (Optional)
If you want to customize the configuration, publish the config file:
php artisan vendor:publish --tag=viettel-cloud-s3-config
⚙️ Configuration
Add a new disk to your config/filesystems.php
:
'disks' => [ // ... other disks 'viettel-s3' => [ 'driver' => 'viettel-s3', 'key' => env('VIETTEL_S3_ACCESS_KEY_ID'), 'secret' => env('VIETTEL_S3_SECRET_ACCESS_KEY'), 'region' => env('VIETTEL_S3_REGION', 'us-east-1'), 'bucket' => env('VIETTEL_S3_BUCKET'), 'url' => env('VIETTEL_S3_URL'), 'endpoint' => env('VIETTEL_S3_ENDPOINT'), 'throw' => false, ], ],
Environment Variables
Add these variables to your .env
file:
# Viettel Cloud Object Storage Configuration VIETTEL_S3_ACCESS_KEY_ID=your-access-key VIETTEL_S3_SECRET_ACCESS_KEY=your-secret-key VIETTEL_S3_REGION=us-east-1 VIETTEL_S3_BUCKET=your-bucket-name VIETTEL_S3_ENDPOINT=https://vcos.cloudstorage.com.vn VIETTEL_S3_URL=https://your-access-key.vcos.cloudstorage.com.vn/your-bucket-name
Alternative: Use Existing AWS Environment Variables
If you're migrating from AWS S3, you can reuse your existing environment variables:
'viettel-s3' => [ 'driver' => 'viettel-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'), 'url' => env('AWS_URL'), 'endpoint' => env('AWS_ENDPOINT'), 'throw' => false, ],
📖 Usage
Basic File Operations
use Illuminate\Support\Facades\Storage; // Upload a file $content = 'Hello, Viettel Cloud!'; $path = 'documents/hello.txt'; Storage::disk('viettel-s3')->put($path, $content, [ 'visibility' => 'public', 'mimetype' => 'text/plain' ]); // Check if file exists if (Storage::disk('viettel-s3')->exists($path)) { echo "File exists!"; } // Download file content $content = Storage::disk('viettel-s3')->get($path); // Get file size $size = Storage::disk('viettel-s3')->size($path); // Get file URL $url = Storage::disk('viettel-s3')->url($path); // Delete file Storage::disk('viettel-s3')->delete($path);
File Upload with Form Validation
public function uploadFile(Request $request) { $request->validate([ 'file' => 'required|file|max:10240', // 10MB max ]); $file = $request->file('file'); $filename = time() . '_' . $file->getClientOriginalName(); // Upload using Viettel S3 driver $path = Storage::disk('viettel-s3')->putFileAs( 'uploads', $file, $filename, ['visibility' => 'public'] ); return response()->json([ 'success' => true, 'path' => $path, 'url' => Storage::disk('viettel-s3')->url($path), 'size' => $file->getSize(), ]); }
Batch Operations
// Upload multiple files $files = [ 'file1.txt' => 'Content 1', 'file2.txt' => 'Content 2', 'file3.txt' => 'Content 3', ]; foreach ($files as $filename => $content) { Storage::disk('viettel-s3')->put("batch/{$filename}", $content, [ 'visibility' => 'public' ]); } // Delete multiple files $filesToDelete = ['batch/file1.txt', 'batch/file2.txt', 'batch/file3.txt']; Storage::disk('viettel-s3')->delete($filesToDelete);
Working with Streams
// Upload from stream $stream = fopen('/path/to/large-file.zip', 'r'); Storage::disk('viettel-s3')->putStream('large-files/archive.zip', $stream); fclose($stream); // Read as stream $stream = Storage::disk('viettel-s3')->readStream('large-files/archive.zip'); // Process stream...
File Metadata
$path = 'documents/example.pdf'; // Get file information $exists = Storage::disk('viettel-s3')->exists($path); $size = Storage::disk('viettel-s3')->size($path); $lastModified = Storage::disk('viettel-s3')->lastModified($path); $mimeType = Storage::disk('viettel-s3')->mimeType($path); $url = Storage::disk('viettel-s3')->url($path); echo "File: {$path}\n"; echo "Exists: " . ($exists ? 'Yes' : 'No') . "\n"; echo "Size: {$size} bytes\n"; echo "Last Modified: " . date('Y-m-d H:i:s', $lastModified) . "\n"; echo "MIME Type: {$mimeType}\n"; echo "URL: {$url}\n";
🔧 Advanced Configuration
Custom User Agent
The driver uses a default user agent viettel-cloud-s3/1.0 callback
. This is configured in the adapter and matches the working signature requirements.
File Visibility and ACL
// Upload with public visibility (adds x-amz-acl: public-read header) Storage::disk('viettel-s3')->put($path, $content, [ 'visibility' => 'public' ]); // Upload as private (default) Storage::disk('viettel-s3')->put($path, $content); // or explicitly Storage::disk('viettel-s3')->put($path, $content, [ 'visibility' => 'private' ]);
⚠️ VIPCore Limitation: While the driver correctly sends ACL headers, VIPCore/EMC ViPR may not support anonymous public access like AWS S3. Files may still require authentication regardless of the ACL setting.
Error Handling
try { Storage::disk('viettel-s3')->put($path, $content); echo "Upload successful!"; } catch (\League\Flysystem\UnableToWriteFile $e) { echo "Upload failed: " . $e->getMessage(); } catch (\Exception $e) { echo "General error: " . $e->getMessage(); }
🏗️ How It Works
The Problem
Standard AWS SDK for PHP calculates signatures differently than what VIPCore/EMC ViPR S3-compatible services expect, causing SignatureDoesNotMatch
errors.
The Solution
This package provides a custom Flysystem adapter that:
- Manually calculates AWS v4 signatures using the exact format expected by VIPCore
- Forces
UNSIGNED-PAYLOAD
content hash (required by VIPCore) - Uses direct cURL requests bypassing AWS SDK signature issues
- Implements proper header formatting based on working examples
Key Components
- Custom signature calculation compatible with VIPCore/EMC ViPR
- Proper canonical request formatting with alphabetical header ordering
- UNSIGNED-PAYLOAD handling for all requests
- cURL-based HTTP client for direct control over requests
🔒 Security
- Uses AWS v4 signature validation
- Proper credential handling through Laravel configuration
- Request timestamp validation prevents replay attacks
- Content integrity checks with SHA256 hashing
- Supports both public and private file access controls
🧪 Testing
After installation, you can test the integration with a simple script:
// Test basic functionality $disk = Storage::disk('viettel-s3'); // Upload test $testFile = 'test-' . time() . '.txt'; $testContent = 'Hello from Viettel Cloud S3!'; $disk->put($testFile, $testContent, ['visibility' => 'public']); // Verify upload if ($disk->exists($testFile)) { echo "✅ Upload successful\n"; // Test download $downloadedContent = $disk->get($testFile); if ($downloadedContent === $testContent) { echo "✅ Download successful\n"; } // Test URL generation $url = $disk->url($testFile); echo "📁 File URL: {$url}\n"; // Cleanup $disk->delete($testFile); echo "🗑️ Cleanup completed\n"; }
🐛 Troubleshooting
Common Issues
1. SignatureDoesNotMatch Error
- ✅ Solved by this package! The custom signature calculation handles VIPCore compatibility.
2. File Upload Fails
- Check your credentials in
.env
- Verify bucket name and endpoint URL
- Ensure network connectivity to the endpoint
3. File URLs Don't Work
- Verify the
VIETTEL_S3_URL
environment variable - Check if the bucket and file permissions are correct
- Remember that VIPCore may require authentication even for "public" files
4. Large File Uploads
- Use
putStream()
for large files instead ofput()
- Consider implementing chunked uploads for files > 100MB
Debug Mode
Enable debug logging in your Laravel application to see detailed request/response information:
// In config/logging.php, set the default log level to 'debug' 'level' => 'debug',
Check storage/logs/laravel.log
for detailed error information.
📋 Requirements
- PHP: ^8.2
- Laravel: ^10.0 || ^11.0 || ^12.0
- League/Flysystem: ^3.0
🤝 Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature
) - Commit your changes (
git commit -m 'Add some amazing feature'
) - Push to the branch (
git push origin feature/amazing-feature
) - Open a Pull Request
📝 License
This package is open-sourced software licensed under the MIT license.
🏢 Support
- Issues: GitHub Issues
- Documentation: This README and inline code documentation
- Community: Feel free to open discussions for questions and feature requests
🎯 Roadmap
- Add support for multipart uploads
- Implement proper directory listing (ListObjects API)
- Add comprehensive test suite
- Support for more VIPCore-specific features
- Performance optimizations and caching
🎉 Happy coding with Viettel Cloud Object Storage!