A Laravel Flysystem v2/v3 adapter for Huawei Object Storage Service (OBS). This package provides seamless integration between Laravel's filesystem abstraction and Huawei Cloud OBS, allowing you to use Huawei OBS as a storage backend in your Laravel applications.
This package now supports multiple versions of both Flysystem and Guzzle:
- Flysystem: v3.0+ (primary) with v2.0+ compatibility through dependency constraints
- Guzzle: v6.3.0+, v7.0+, and v8.0+ (automatic detection)
- Laravel: 9.0+, 10.0+, 11.0+, and 12.0+
- PHP: 8.1+
- ✅ Complete Flysystem v3 Compatibility: Full implementation of all required and optional Flysystem methods
- ✅ Laravel Integration: Seamless integration with Laravel's Storage facade
- ✅ Huawei OBS SDK Integration: Uses
mubbi/esdk-obs-php
(Huawei OBS SDK compatible fork) - ✅ Temporary Credentials: Support for session tokens (
securityToken
) - ✅ Signed URLs: Generate pre-signed URLs for temporary object access
- ✅ Public URLs: Generate public URLs for objects with public read access
- ✅ Post Signatures: Create signatures for direct browser uploads to OBS
- ✅ Object Tagging: Add and manage metadata tags on OBS objects
- ✅ Object Restoration: Restore archived objects from OBS
- ✅ Credential Refresh: Update credentials during runtime
- ✅ Authentication Validation: Proactive checking of OBS credentials and bucket access
- ✅ Comprehensive Error Handling: Clear, actionable error messages
- ✅ Security-Focused Design: Private visibility by default, SSL verification
- ✅ Configuration Validation: Automatic validation of required configuration parameters
- ✅ Retry Logic: Automatic retry with exponential backoff for transient errors
- ✅ Authentication Caching: Cache authentication status to improve performance
- ✅ Logging Support: Optional operation and error logging
- ✅ Artisan Commands: Built-in testing command for connectivity verification
- ✅ Type Safety: PHPStan level 8 compliance with strict typing
- ✅ Code Quality: Laravel Pint compliance and PSR-12 standards
- ✅ Performance Optimized: Built-in timeout protection and infinite loop prevention
- ✅ Large Dataset Support: Optimized methods for handling large numbers of files
This package includes robust security features:
- 🔐 Authentication Validation: Automatic authentication checks before all operations
- 🛡️ Secure Defaults: Private visibility by default, SSL verification enabled
- 🔑 Temporary Credentials: Support for security tokens and credential rotation
- 🌐 Signed URLs: Secure temporary access without exposing credentials
- 📝 Input Sanitization: Path normalization and validation
- 🚫 Error Handling: No sensitive data exposure in error messages
- 🔄 Infinite Loop Prevention: Built-in safety mechanisms to prevent timeouts
- PHP 8.1+
- Laravel 9.0+ (supports Laravel 9, 10.48.29+, 11, and 12)
- Flysystem v3.0+ (primary) with v2.0+ compatibility
- Guzzle v6.3.0+, v7.0+, or v8.0+ (automatic detection)
- Huawei Cloud OBS account and credentials
⚠️ Security Notice: Laravel 10.0.0 to 10.48.28 contains a file validation bypass vulnerability (CVE-2025-27515). This package requires Laravel 10.48.29+ to ensure security. Please upgrade your Laravel installation if you're using an affected version.
This package automatically detects and adapts to your installed versions of Guzzle:
- Guzzle v6: Uses v6-specific client configurations
- Guzzle v7: Uses v7-specific client configurations
- Guzzle v8: Uses v8-specific client configurations (with PSR-18 compliance)
For Flysystem, the package uses v3 as the primary interface but maintains compatibility with v2 through dependency constraints. The package will automatically choose the correct HTTP client configuration based on your installed Guzzle version.
- Install the package via Composer:
composer require mubbi/laravel-flysystem-huawei-obs
- Publish the configuration (optional):
php artisan vendor:publish --provider="LaravelFlysystemHuaweiObs\HuaweiObsServiceProvider" --tag=huawei-obs-config
- Add your Huawei OBS credentials to your
.env
file:
# Required Configuration
HUAWEI_OBS_ACCESS_KEY_ID=your_access_key_id
HUAWEI_OBS_SECRET_ACCESS_KEY=your_secret_access_key
HUAWEI_OBS_BUCKET=your_bucket_name
HUAWEI_OBS_ENDPOINT=https://obs.cn-north-1.myhuaweicloud.com
# Optional Configuration
HUAWEI_OBS_PREFIX=optional_prefix
HUAWEI_OBS_SECURITY_TOKEN=your_security_token_for_temporary_credentials
# Advanced OBSClient Configuration
HUAWEI_OBS_SIGNATURE=v4
HUAWEI_OBS_PATH_STYLE=false
HUAWEI_OBS_REGION=cn-north-1
HUAWEI_OBS_SSL_CERTIFICATE_AUTHORITY=
HUAWEI_OBS_MAX_RETRY_COUNT=3
HUAWEI_OBS_SOCKET_TIMEOUT=60
HUAWEI_OBS_CHUNK_SIZE=8192
HUAWEI_OBS_EXCEPTION_RESPONSE_MODE=exception
HUAWEI_OBS_IS_CNAME=false
# HTTP Client Configuration
HUAWEI_OBS_TIMEOUT=120
HUAWEI_OBS_CONNECT_TIMEOUT=30
HUAWEI_OBS_VERIFY_SSL=true
# Retry Configuration
HUAWEI_OBS_RETRY_ATTEMPTS=3
HUAWEI_OBS_RETRY_DELAY=1
# Logging Configuration
HUAWEI_OBS_LOGGING_ENABLED=false
HUAWEI_OBS_LOG_OPERATIONS=false
HUAWEI_OBS_LOG_ERRORS=true
- Configure your filesystem in
config/filesystems.php
:
'disks' => [
'huawei-obs' => [
'driver' => 'huawei-obs',
// Required Configuration
'key' => env('HUAWEI_OBS_ACCESS_KEY_ID'),
'secret' => env('HUAWEI_OBS_SECRET_ACCESS_KEY'),
'bucket' => env('HUAWEI_OBS_BUCKET'),
'endpoint' => env('HUAWEI_OBS_ENDPOINT'),
// Optional Configuration
'prefix' => env('HUAWEI_OBS_PREFIX'),
'security_token' => env('HUAWEI_OBS_SECURITY_TOKEN'),
// Advanced OBSClient Configuration
'signature' => env('HUAWEI_OBS_SIGNATURE', 'v4'),
'path_style' => env('HUAWEI_OBS_PATH_STYLE', false),
'region' => env('HUAWEI_OBS_REGION'),
'ssl_verify' => env('HUAWEI_OBS_VERIFY_SSL', true),
'ssl.certificate_authority' => env('HUAWEI_OBS_SSL_CERTIFICATE_AUTHORITY'),
'max_retry_count' => env('HUAWEI_OBS_MAX_RETRY_COUNT', 3),
'timeout' => env('HUAWEI_OBS_TIMEOUT', 120),
'socket_timeout' => env('HUAWEI_OBS_SOCKET_TIMEOUT', 60),
'connect_timeout' => env('HUAWEI_OBS_CONNECT_TIMEOUT', 30),
'chunk_size' => env('HUAWEI_OBS_CHUNK_SIZE', 8192),
'exception_response_mode' => env('HUAWEI_OBS_EXCEPTION_RESPONSE_MODE', 'exception'),
'is_cname' => env('HUAWEI_OBS_IS_CNAME', false),
// HTTP Client Configuration
'http_client' => [
'timeout' => env('HUAWEI_OBS_TIMEOUT', 120),
'connect_timeout' => env('HUAWEI_OBS_CONNECT_TIMEOUT', 30),
'verify' => env('HUAWEI_OBS_VERIFY_SSL', true),
'proxy' => null,
'headers' => [],
],
// Retry Configuration
'retry_attempts' => env('HUAWEI_OBS_RETRY_ATTEMPTS', 3),
'retry_delay' => env('HUAWEI_OBS_RETRY_DELAY', 1),
// Logging Configuration
'logging_enabled' => env('HUAWEI_OBS_LOGGING_ENABLED', false),
'log_operations' => env('HUAWEI_OBS_LOG_OPERATIONS', false),
'log_errors' => env('HUAWEI_OBS_LOG_ERRORS', true),
// Flysystem Configuration
'visibility' => 'private',
'throw' => false,
],
],
The package integrates seamlessly with Laravel's Storage facade:
use Illuminate\Support\Facades\Storage;
// Upload a file
Storage::disk('huawei-obs')->put('file.txt', 'Hello World');
// Download a file
$contents = Storage::disk('huawei-obs')->get('file.txt');
// Check if file exists
$exists = Storage::disk('huawei-obs')->exists('file.txt');
// Delete a file
Storage::disk('huawei-obs')->delete('file.txt');
// Get file URL
$url = Storage::disk('huawei-obs')->url('file.txt');
// Get temporary URL
$tempUrl = Storage::disk('huawei-obs')->temporaryUrl('file.txt', now()->addHour());
You can also use the adapter directly (Flysystem v3 interface):
use LaravelFlysystemHuaweiObs\LaravelHuaweiObsAdapter;
use League\Flysystem\Filesystem;
use League\Flysystem\Config;
$adapter = new LaravelHuaweiObsAdapter(
env('HUAWEI_OBS_ACCESS_KEY_ID'),
env('HUAWEI_OBS_SECRET_ACCESS_KEY'),
env('HUAWEI_OBS_BUCKET'),
env('HUAWEI_OBS_ENDPOINT'),
env('HUAWEI_OBS_PREFIX'),
);
$filesystem = new Filesystem($adapter);
$filesystem->write('file.txt', 'Hello World', new Config());
$contents = $filesystem->read('file.txt');
These options are required for the adapter to function:
key
- Your Huawei OBS Access Key IDsecret
- Your Huawei OBS Secret Access Keybucket
- The OBS bucket nameendpoint
- The OBS endpoint URL (e.g.,https://obs.cn-north-1.myhuaweicloud.com
)
prefix
- Optional path prefix for all operationssecurity_token
- Security token for temporary credentials
The adapter supports all OBSClient configuration options:
signature
- Signature version (v2
,v4
,obs
) - Default:v4
path_style
- Use path-style URLs instead of virtual-hosted-style - Default:false
region
- OBS region (e.g.,cn-north-1
) - Optionalssl_verify
- Enable/disable SSL certificate verification - Default:true
ssl.certificate_authority
- Path to custom CA certificate bundle - Optionalmax_retry_count
- Maximum number of retry attempts - Default:3
timeout
- Request timeout in seconds - Default:120
socket_timeout
- Socket timeout in seconds - Default:60
connect_timeout
- Connection timeout in seconds - Default:30
chunk_size
- Chunk size for multipart uploads in bytes - Default:8192
exception_response_mode
- Exception response mode (exception
,response
) - Default:exception
is_cname
- Whether the endpoint is a CNAME - Default:false
http_client.timeout
- Request timeout in seconds - Default:120
http_client.connect_timeout
- Connection timeout in seconds - Default:30
http_client.verify
- SSL certificate verification - Default:true
http_client.proxy
- HTTP proxy configuration - Optionalhttp_client.headers
- Additional HTTP headers - Optional
retry_attempts
- Number of retry attempts for transient errors - Default:3
retry_delay
- Base delay between retries in seconds - Default:1
logging_enabled
- Enable/disable logging - Default:false
log_operations
- Log successful operations - Default:false
log_errors
- Log errors - Default:true
See the examples/
directory for concise, copy-pasteable snippets:
examples/usage.php
: basic write/read with Flysystemexamples/url-usage.php
: URL and temporary URL usageexamples/advanced-usage.php
: signed URLs, post signatures, tags, restore, optimized listingexamples/controller-compatibility.php
: controller-style listing exampleexamples/laravel-compatibility-demo.php
: Storage facade compatibility helpers
For applications with large numbers of files, the package provides optimized methods with built-in timeout protection:
// Get storage statistics with timeout protection
$stats = Storage::disk('huawei-obs')->getStorageStats(10000, 60);
// Returns: ['total_files' => 1234, 'total_directories' => 56, 'total_size_bytes' => 1073741824, ...]
// Get files with limits and timeout
$files = Storage::disk('huawei-obs')->allFilesOptimized(1000, 30);
// Get directories with limits and timeout
$directories = Storage::disk('huawei-obs')->allDirectoriesOptimized(1000, 30);
// List contents with advanced controls
foreach (Storage::disk('huawei-obs')->listContentsOptimized('path', true, 1000, 60) as $item) {
// Process items with timeout protection
}
The adapter includes automatic retry logic with exponential backoff for transient errors:
// Configure retry behavior
'huawei-obs' => [
'retry_attempts' => 3, // Number of retry attempts
'retry_delay' => 1, // Base delay in seconds
// ... other config
],
The retry logic will automatically retry on transient errors but will not retry on:
- Authentication errors (
AccessDenied
,InvalidAccessKeyId
,SignatureDoesNotMatch
) - Configuration errors (
NoSuchBucket
)
Authentication status is cached for 5 minutes to improve performance:
// Refresh authentication cache manually
$adapter->refreshAuthentication();
// Refresh credentials (clears cache automatically)
$adapter->refreshCredentials('new-key', 'new-secret', 'new-token');
Enable operation and error logging:
'huawei-obs' => [
'logging_enabled' => true,
'log_operations' => true, // Log successful operations
'log_errors' => true, // Log errors
// ... other config
],
This package provides full compatibility with Laravel's Storage facade. All the standard Laravel Storage methods are supported:
// List files in a directory (non-recursive)
$files = Storage::disk('huawei-obs')->files('uploads');
// List directories in a directory (non-recursive)
$directories = Storage::disk('huawei-obs')->directories('uploads');
// List all files and directories (recursive)
$allFiles = Storage::disk('huawei-obs')->allFiles();
$allDirectories = Storage::disk('huawei-obs')->allDirectories();
// Check if file exists
$exists = Storage::disk('huawei-obs')->exists('file.txt');
// Get file size
$size = Storage::disk('huawei-obs')->size('file.txt');
// Get last modified timestamp
$modified = Storage::disk('huawei-obs')->lastModified('file.txt');
// Get MIME type
$mimeType = Storage::disk('huawei-obs')->mimeType('file.txt');
// Get file visibility
$visibility = Storage::disk('huawei-obs')->visibility('file.txt');
$directory = $request->get('directory', '');
$files = Storage::disk('huawei-obs')->files($directory);
$directories = Storage::disk('huawei-obs')->directories($directory);
$fileDetails = [];
foreach ($files as $file) {
$fileDetails[] = [
'name' => $file,
'size' => Storage::disk('huawei-obs')->size($file),
'last_modified' => Storage::disk('huawei-obs')->lastModified($file),
'mime_type' => Storage::disk('huawei-obs')->mimeType($file),
'url' => Storage::disk('huawei-obs')->url($file),
'visibility' => Storage::disk('huawei-obs')->visibility($file)
];
}
return response()->json([
'success' => true,
'files' => $fileDetails,
'directories' => $directories,
'total_files' => count($files),
'total_directories' => count($directories)
]);
Logged information includes:
- Operation name and duration
- File path and bucket
- Error details and codes
- Additional context (tags, headers, etc.)
The service provider automatically validates required configuration:
// Missing required fields will throw clear errors
'huawei-obs' => [
'key' => '', // ❌ Will throw: "Missing required configuration: key"
// ... other config
],
The package provides custom exceptions for better error handling:
use LaravelFlysystemHuaweiObs\Exceptions\UnableToCreateSignedUrl;
use LaravelFlysystemHuaweiObs\Exceptions\UnableToSetObjectTags;
try {
$signedUrl = $adapter->createSignedUrl('file.txt');
} catch (UnableToCreateSignedUrl $e) {
// Handle signed URL creation errors
}
try {
$adapter->setObjectTags('file.txt', ['tag' => 'value']);
} catch (UnableToSetObjectTags $e) {
// Handle object tagging errors
}
The adapter automatically validates authentication before operations and provides clear error messages:
try {
Storage::disk('huawei-obs')->exists('file.txt');
} catch (\RuntimeException $e) {
// Clear authentication error message
// "Authentication failed. Please check your Huawei OBS credentials..."
}
For applications using temporary credentials (like AWS STS or similar services):
// Configure with security token
$adapter = new \LaravelFlysystemHuaweiObs\HuaweiObsAdapter(
'access_key_id',
'secret_access_key',
'bucket_name',
'endpoint',
null, // prefix
null, // http client
'security_token_here'
);
// Or refresh credentials during runtime
$adapter->refreshCredentials('new_access_key', 'new_secret_key', 'new_security_token');
The adapter supports both public URLs and signed URLs. For Laravel's Storage
facade:
The url()
method automatically handles both public and private objects:
// For public objects, returns a direct URL
$publicUrl = Storage::disk('huawei-obs')->url('public-file.txt');
// Returns: https://obs.example.com/bucket/public-file.txt
// For private objects, automatically returns a signed URL (1-hour expiration)
$privateUrl = Storage::disk('huawei-obs')->url('private-file.txt');
// Returns: https://obs.example.com/bucket/private-file.txt?signature=...
Generate temporary URLs with custom expiration times:
// Generate a temporary URL that expires in 2 hours
$tempUrl = Storage::disk('huawei-obs')->temporaryUrl(
'file.txt',
now()->addHours(2)
);
// Generate a temporary URL for uploads (PUT method)
$uploadUrl = Storage::disk('huawei-obs')->temporaryUploadUrl(
'file.txt',
now()->addHour()
);
Add and manage metadata tags on OBS objects:
// Set tags on an object
Storage::disk('huawei-obs')->setObjectTags('file.txt', [
'category' => 'images',
'processed' => 'true',
'user_id' => '123'
]);
// Get tags from an object
$tags = Storage::disk('huawei-obs')->getObjectTags('file.txt');
// Delete tags from an object
Storage::disk('huawei-obs')->deleteObjectTags('file.txt');
Create signatures for direct browser uploads to OBS:
// Generate a post signature for direct upload
$signature = Storage::disk('huawei-obs')->createPostSignature('uploads/file.txt', [
'success_action_status' => '201',
'x-amz-meta-category' => 'images'
], 3600); // 1 hour expiration
// Use the signature in an HTML form
echo '<form action="' . $signature['url'] . '" method="post" enctype="multipart/form-data">';
foreach ($signature['fields'] as $key => $value) {
echo '<input type="hidden" name="' . $key . '" value="' . $value . '">';
}
echo '<input type="file" name="file">';
echo '<input type="submit" value="Upload">';
echo '</form>';
Restore archived objects from OBS:
// Restore an archived object (default 1 day)
Storage::disk('huawei-obs')->restoreObject('archived-file.txt');
// Restore with custom restoration period (7 days)
Storage::disk('huawei-obs')->restoreObject('archived-file.txt', 7);
The package includes an Artisan command for testing connectivity and features:
php artisan huawei-obs:test
This command will:
- Validate credentials and bucket access
- Test reads/writes, signed URLs, post signatures, and tagging
- Output a concise pass/fail report
You can also test the adapter manually:
use LaravelFlysystemHuaweiObs\HuaweiObsAdapter;
$adapter = new HuaweiObsAdapter(
'your_access_key_id',
'your_secret_access_key',
'your_bucket_name',
'https://obs.cn-north-1.myhuaweicloud.com'
);
// Test basic operations
$adapter->write('test.txt', 'Hello World', new \League\Flysystem\Config());
$contents = $adapter->read('test.txt');
$adapter->delete('test.txt');
The package provides comprehensive error handling with clear, actionable error messages:
try {
Storage::disk('huawei-obs')->put('file.txt', 'content');
} catch (\RuntimeException $e) {
// Authentication errors
if (str_contains($e->getMessage(), 'Authentication failed')) {
// Check your credentials
}
// Bucket errors
if (str_contains($e->getMessage(), 'NoSuchBucket')) {
// Check your bucket name
}
// Permission errors
if (str_contains($e->getMessage(), 'AccessDenied')) {
// Check your IAM permissions
}
}
The package provides specific exceptions for different error types used by advanced features:
use LaravelFlysystemHuaweiObs\Exceptions\UnableToCreateSignedUrl;
use LaravelFlysystemHuaweiObs\Exceptions\UnableToSetObjectTags;
try {
$url = Storage::disk('huawei-obs')->temporaryUrl('file.txt', now()->addHour());
} catch (UnableToCreateSignedUrl $e) {
// Handle signed URL creation errors
}
try {
Storage::disk('huawei-obs')->setObjectTags('file.txt', ['tag' => 'value']);
} catch (UnableToSetObjectTags $e) {
// Handle object tagging errors
}
For applications with large numbers of files, use the optimized methods:
// Instead of allFiles() which might timeout
$allFiles = Storage::disk('huawei-obs')->allFiles();
// Use optimized method with limits
$files = Storage::disk('huawei-obs')->allFilesOptimized(10000, 60);
Consider caching frequently accessed data:
// Cache file listings
$files = Cache::remember('huawei-obs-files', 300, function () {
return Storage::disk('huawei-obs')->files('uploads');
});
For multiple operations, consider batching:
// Instead of multiple individual calls
foreach ($files as $file) {
Storage::disk('huawei-obs')->delete($file);
}
// Consider using background jobs for large batches
-
Authentication Errors
- Verify your access key and secret key
- Check if your security token is still valid
- Ensure your credentials have the necessary permissions
-
Bucket Access Errors
- Verify the bucket name is correct
- Check if the bucket exists in the specified region
- Ensure your credentials have access to the bucket
-
Timeout Errors
- Use optimized methods for large datasets
- Increase timeout values in configuration
- Consider using background processing for large operations
-
SSL Certificate Errors
- Verify your endpoint URL is correct
- Check if SSL verification is required in your environment
- Consider disabling SSL verification for testing (not recommended for production)
Enable debug mode to get more detailed error information:
'huawei-obs' => [
'logging_enabled' => true,
'log_operations' => true,
'log_errors' => true,
// ... other config
],
Contributions are welcome! Please feel free to submit a Pull Request. For major changes, please open an issue first to discuss what you would like to change.
- Clone the repository
- Install dependencies:
composer install
- Run tests:
composer test
- Static analysis:
composer phpstan
- Lint/format:
composer pint
This package follows strict code quality standards:
- PHPStan Level 8: Maximum type safety
- Laravel Pint: PSR-12 coding standards
- PHPUnit: Comprehensive test coverage
- GitHub Actions: Automated CI/CD pipeline
This package is open-sourced software licensed under the MIT license.
- Documentation: GitHub Wiki
- Issues: GitHub Issues
- Discussions: GitHub Discussions
Please see CHANGELOG.md for more information on what has changed recently.
- Author: Mubbasher Ahmed
- Maintainer: Mubbasher Ahmed
- License: MIT
Made with ❤️ by Mubbasher Ahmed