feat: extract storage providers from app/Plug/Storage
Bunny Storage (Browse, Delete, Download, Upload, VBucket), StorageManager, domain contracts (Browseable, Deletable, Downloadable, Uploadable) with Core\Plug\Storage namespace alignment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
619b53135d
commit
373b94efab
11 changed files with 1191 additions and 0 deletions
17
composer.json
Normal file
17
composer.json
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{
|
||||
"name": "core/php-plug-storage",
|
||||
"description": "Storage provider integrations for the Plug framework",
|
||||
"type": "library",
|
||||
"license": "EUPL-1.2",
|
||||
"require": {
|
||||
"php": "^8.2",
|
||||
"core/php": "^1.0"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"Core\\Plug\\Storage\\": "src/"
|
||||
}
|
||||
},
|
||||
"minimum-stability": "dev",
|
||||
"prefer-stable": true
|
||||
}
|
||||
144
src/Bunny/Browse.php
Normal file
144
src/Bunny/Browse.php
Normal file
|
|
@ -0,0 +1,144 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Storage\Bunny;
|
||||
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Response;
|
||||
use Core\Plug\Storage\Contract\Browseable;
|
||||
use Bunny\Storage\Client;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Bunny Storage browsing operations.
|
||||
*
|
||||
* Supports:
|
||||
* - List files in directory
|
||||
* - Check file existence
|
||||
* - Get file size
|
||||
* - Public and private storage zones
|
||||
*/
|
||||
class Browse implements Browseable
|
||||
{
|
||||
use BuildsResponse;
|
||||
|
||||
protected string $apiKey;
|
||||
|
||||
protected string $storageZone;
|
||||
|
||||
protected string $region;
|
||||
|
||||
protected ?Client $client = null;
|
||||
|
||||
public function __construct(
|
||||
?string $apiKey = null,
|
||||
?string $storageZone = null,
|
||||
?string $region = null,
|
||||
string $zone = 'public'
|
||||
) {
|
||||
$this->apiKey = $apiKey ?? config("cdn.bunny.{$zone}.api_key", '');
|
||||
$this->storageZone = $storageZone ?? config("cdn.bunny.{$zone}.storage_zone", '');
|
||||
$this->region = $region ?? config("cdn.bunny.{$zone}.region", Client::STORAGE_ZONE_FS_EU);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the service is configured.
|
||||
*/
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return ! empty($this->apiKey) && ! empty($this->storageZone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Bunny Storage client.
|
||||
*/
|
||||
protected function client(): ?Client
|
||||
{
|
||||
if ($this->client === null && $this->isConfigured()) {
|
||||
$this->client = new Client($this->apiKey, $this->storageZone, $this->region);
|
||||
}
|
||||
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
/**
|
||||
* List files in a path.
|
||||
*/
|
||||
public function list(string $path = '/'): Response
|
||||
{
|
||||
if (! $this->isConfigured()) {
|
||||
return $this->error('Bunny Storage not configured');
|
||||
}
|
||||
|
||||
try {
|
||||
$files = $this->client()->listFiles($path);
|
||||
|
||||
return $this->ok([
|
||||
'path' => $path,
|
||||
'files' => $files,
|
||||
'count' => count($files),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Bunny Storage: List failed', [
|
||||
'path' => $path,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a file exists.
|
||||
*/
|
||||
public function exists(string $path): bool
|
||||
{
|
||||
if (! $this->isConfigured()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
// Try to get the file contents - if it fails, file doesn't exist
|
||||
$this->client()->getContents($path);
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file size in bytes.
|
||||
*/
|
||||
public function size(string $path): ?int
|
||||
{
|
||||
if (! $this->isConfigured()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$contents = $this->client()->getContents($path);
|
||||
|
||||
return strlen($contents);
|
||||
} catch (\Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a browser for the public storage zone.
|
||||
*/
|
||||
public static function public(): self
|
||||
{
|
||||
return new self(zone: 'public');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a browser for the private storage zone.
|
||||
*/
|
||||
public static function private(): self
|
||||
{
|
||||
return new self(zone: 'private');
|
||||
}
|
||||
}
|
||||
146
src/Bunny/Delete.php
Normal file
146
src/Bunny/Delete.php
Normal file
|
|
@ -0,0 +1,146 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Storage\Bunny;
|
||||
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Response;
|
||||
use Core\Plug\Storage\Contract\Deletable;
|
||||
use Bunny\Storage\Client;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Bunny Storage delete operations.
|
||||
*
|
||||
* Supports:
|
||||
* - Single file deletion
|
||||
* - Bulk file deletion
|
||||
* - Public and private storage zones
|
||||
*/
|
||||
class Delete implements Deletable
|
||||
{
|
||||
use BuildsResponse;
|
||||
|
||||
protected string $apiKey;
|
||||
|
||||
protected string $storageZone;
|
||||
|
||||
protected string $region;
|
||||
|
||||
protected ?Client $client = null;
|
||||
|
||||
public function __construct(
|
||||
?string $apiKey = null,
|
||||
?string $storageZone = null,
|
||||
?string $region = null,
|
||||
string $zone = 'public'
|
||||
) {
|
||||
$this->apiKey = $apiKey ?? config("cdn.bunny.{$zone}.api_key", '');
|
||||
$this->storageZone = $storageZone ?? config("cdn.bunny.{$zone}.storage_zone", '');
|
||||
$this->region = $region ?? config("cdn.bunny.{$zone}.region", Client::STORAGE_ZONE_FS_EU);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the service is configured.
|
||||
*/
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return ! empty($this->apiKey) && ! empty($this->storageZone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Bunny Storage client.
|
||||
*/
|
||||
protected function client(): ?Client
|
||||
{
|
||||
if ($this->client === null && $this->isConfigured()) {
|
||||
$this->client = new Client($this->apiKey, $this->storageZone, $this->region);
|
||||
}
|
||||
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a single file.
|
||||
*/
|
||||
public function path(string $remotePath): Response
|
||||
{
|
||||
if (! $this->isConfigured()) {
|
||||
return $this->error('Bunny Storage not configured');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->client()->delete($remotePath);
|
||||
|
||||
return $this->ok([
|
||||
'deleted' => true,
|
||||
'path' => $remotePath,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Bunny Storage: Delete failed', [
|
||||
'remote' => $remotePath,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple files.
|
||||
*
|
||||
* @param array<string> $remotePaths
|
||||
*/
|
||||
public function paths(array $remotePaths): Response
|
||||
{
|
||||
if (! $this->isConfigured()) {
|
||||
return $this->error('Bunny Storage not configured');
|
||||
}
|
||||
|
||||
$failed = [];
|
||||
$deleted = [];
|
||||
|
||||
foreach ($remotePaths as $path) {
|
||||
try {
|
||||
$this->client()->delete($path);
|
||||
$deleted[] = $path;
|
||||
} catch (\Exception $e) {
|
||||
$failed[] = $path;
|
||||
Log::error('Bunny Storage: Delete failed', [
|
||||
'remote' => $path,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
if (! empty($failed)) {
|
||||
return $this->error('Some files failed to delete', [
|
||||
'deleted' => $deleted,
|
||||
'failed' => $failed,
|
||||
'total' => count($remotePaths),
|
||||
]);
|
||||
}
|
||||
|
||||
return $this->ok([
|
||||
'deleted' => count($deleted),
|
||||
'paths' => $deleted,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a deleter for the public storage zone.
|
||||
*/
|
||||
public static function public(): self
|
||||
{
|
||||
return new self(zone: 'public');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a deleter for the private storage zone.
|
||||
*/
|
||||
public static function private(): self
|
||||
{
|
||||
return new self(zone: 'private');
|
||||
}
|
||||
}
|
||||
142
src/Bunny/Download.php
Normal file
142
src/Bunny/Download.php
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Storage\Bunny;
|
||||
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Response;
|
||||
use Core\Plug\Storage\Contract\Downloadable;
|
||||
use Bunny\Storage\Client;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Bunny Storage download operations.
|
||||
*
|
||||
* Supports:
|
||||
* - Download file contents as string
|
||||
* - Download file to local path
|
||||
* - Public and private storage zones
|
||||
*/
|
||||
class Download implements Downloadable
|
||||
{
|
||||
use BuildsResponse;
|
||||
|
||||
protected string $apiKey;
|
||||
|
||||
protected string $storageZone;
|
||||
|
||||
protected string $region;
|
||||
|
||||
protected ?Client $client = null;
|
||||
|
||||
public function __construct(
|
||||
?string $apiKey = null,
|
||||
?string $storageZone = null,
|
||||
?string $region = null,
|
||||
string $zone = 'public'
|
||||
) {
|
||||
$this->apiKey = $apiKey ?? config("cdn.bunny.{$zone}.api_key", '');
|
||||
$this->storageZone = $storageZone ?? config("cdn.bunny.{$zone}.storage_zone", '');
|
||||
$this->region = $region ?? config("cdn.bunny.{$zone}.region", Client::STORAGE_ZONE_FS_EU);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the service is configured.
|
||||
*/
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return ! empty($this->apiKey) && ! empty($this->storageZone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Bunny Storage client.
|
||||
*/
|
||||
protected function client(): ?Client
|
||||
{
|
||||
if ($this->client === null && $this->isConfigured()) {
|
||||
$this->client = new Client($this->apiKey, $this->storageZone, $this->region);
|
||||
}
|
||||
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file contents as string.
|
||||
*/
|
||||
public function contents(string $remotePath): Response
|
||||
{
|
||||
if (! $this->isConfigured()) {
|
||||
return $this->error('Bunny Storage not configured');
|
||||
}
|
||||
|
||||
try {
|
||||
$contents = $this->client()->getContents($remotePath);
|
||||
|
||||
return $this->ok([
|
||||
'contents' => $contents,
|
||||
'remote_path' => $remotePath,
|
||||
'size' => strlen($contents),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Bunny Storage: getContents failed', [
|
||||
'remote' => $remotePath,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Download file to local path.
|
||||
*/
|
||||
public function toFile(string $remotePath, string $localPath): Response
|
||||
{
|
||||
if (! $this->isConfigured()) {
|
||||
return $this->error('Bunny Storage not configured');
|
||||
}
|
||||
|
||||
try {
|
||||
$contents = $this->client()->getContents($remotePath);
|
||||
|
||||
$directory = dirname($localPath);
|
||||
if (! is_dir($directory)) {
|
||||
mkdir($directory, 0755, true);
|
||||
}
|
||||
|
||||
file_put_contents($localPath, $contents);
|
||||
|
||||
return $this->ok([
|
||||
'downloaded' => true,
|
||||
'remote_path' => $remotePath,
|
||||
'local_path' => $localPath,
|
||||
'size' => strlen($contents),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Bunny Storage: Download to file failed', [
|
||||
'remote' => $remotePath,
|
||||
'local' => $localPath,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a downloader for the public storage zone.
|
||||
*/
|
||||
public static function public(): self
|
||||
{
|
||||
return new self(zone: 'public');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a downloader for the private storage zone.
|
||||
*/
|
||||
public static function private(): self
|
||||
{
|
||||
return new self(zone: 'private');
|
||||
}
|
||||
}
|
||||
139
src/Bunny/Upload.php
Normal file
139
src/Bunny/Upload.php
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Storage\Bunny;
|
||||
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Response;
|
||||
use Core\Plug\Storage\Contract\Uploadable;
|
||||
use Bunny\Storage\Client;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Bunny Storage upload operations.
|
||||
*
|
||||
* Supports:
|
||||
* - File uploads from local path
|
||||
* - Direct content uploads
|
||||
* - Public and private storage zones
|
||||
*/
|
||||
class Upload implements Uploadable
|
||||
{
|
||||
use BuildsResponse;
|
||||
|
||||
protected string $apiKey;
|
||||
|
||||
protected string $storageZone;
|
||||
|
||||
protected string $region;
|
||||
|
||||
protected ?Client $client = null;
|
||||
|
||||
public function __construct(
|
||||
?string $apiKey = null,
|
||||
?string $storageZone = null,
|
||||
?string $region = null,
|
||||
string $zone = 'public'
|
||||
) {
|
||||
$this->apiKey = $apiKey ?? config("cdn.bunny.{$zone}.api_key", '');
|
||||
$this->storageZone = $storageZone ?? config("cdn.bunny.{$zone}.storage_zone", '');
|
||||
$this->region = $region ?? config("cdn.bunny.{$zone}.region", Client::STORAGE_ZONE_FS_EU);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the service is configured.
|
||||
*/
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return ! empty($this->apiKey) && ! empty($this->storageZone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Bunny Storage client.
|
||||
*/
|
||||
protected function client(): ?Client
|
||||
{
|
||||
if ($this->client === null && $this->isConfigured()) {
|
||||
$this->client = new Client($this->apiKey, $this->storageZone, $this->region);
|
||||
}
|
||||
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file from local path.
|
||||
*/
|
||||
public function file(string $localPath, string $remotePath): Response
|
||||
{
|
||||
if (! $this->isConfigured()) {
|
||||
return $this->error('Bunny Storage not configured');
|
||||
}
|
||||
|
||||
if (! file_exists($localPath)) {
|
||||
return $this->error('Local file not found', ['local_path' => $localPath]);
|
||||
}
|
||||
|
||||
try {
|
||||
$this->client()->upload($localPath, $remotePath);
|
||||
|
||||
return $this->ok([
|
||||
'uploaded' => true,
|
||||
'local_path' => $localPath,
|
||||
'remote_path' => $remotePath,
|
||||
'size' => filesize($localPath),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Bunny Storage: Upload failed', [
|
||||
'local' => $localPath,
|
||||
'remote' => $remotePath,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload content directly.
|
||||
*/
|
||||
public function contents(string $remotePath, string $contents): Response
|
||||
{
|
||||
if (! $this->isConfigured()) {
|
||||
return $this->error('Bunny Storage not configured');
|
||||
}
|
||||
|
||||
try {
|
||||
$this->client()->putContents($remotePath, $contents);
|
||||
|
||||
return $this->ok([
|
||||
'uploaded' => true,
|
||||
'remote_path' => $remotePath,
|
||||
'size' => strlen($contents),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Bunny Storage: putContents failed', [
|
||||
'remote' => $remotePath,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an uploader for the public storage zone.
|
||||
*/
|
||||
public static function public(): self
|
||||
{
|
||||
return new self(zone: 'public');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an uploader for the private storage zone.
|
||||
*/
|
||||
public static function private(): self
|
||||
{
|
||||
return new self(zone: 'private');
|
||||
}
|
||||
}
|
||||
308
src/Bunny/VBucket.php
Normal file
308
src/Bunny/VBucket.php
Normal file
|
|
@ -0,0 +1,308 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Storage\Bunny;
|
||||
|
||||
use Core\Crypt\LthnHash;
|
||||
use Core\Plug\Concern\BuildsResponse;
|
||||
use Core\Plug\Response;
|
||||
use Bunny\Storage\Client;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
/**
|
||||
* Bunny Storage vBucket operations for workspace-isolated paths.
|
||||
*
|
||||
* vBuckets provide workspace isolation on CDN:
|
||||
* - Each workspace gets a unique vBucket ID
|
||||
* - All paths are automatically scoped to the vBucket
|
||||
* - Prevents cross-workspace data access
|
||||
*
|
||||
* @example
|
||||
* $vbucket = new VBucket('example.com');
|
||||
* $vbucket->upload('/local/file.jpg', 'images/hero.jpg');
|
||||
* // Uploads to: /{vbucket_id}/images/hero.jpg
|
||||
*/
|
||||
class VBucket
|
||||
{
|
||||
use BuildsResponse;
|
||||
|
||||
protected string $domain;
|
||||
|
||||
protected string $vBucketId;
|
||||
|
||||
protected string $apiKey;
|
||||
|
||||
protected string $storageZone;
|
||||
|
||||
protected string $region;
|
||||
|
||||
protected ?Client $client = null;
|
||||
|
||||
public function __construct(
|
||||
string $domain,
|
||||
?string $apiKey = null,
|
||||
?string $storageZone = null,
|
||||
?string $region = null,
|
||||
string $zone = 'public'
|
||||
) {
|
||||
$this->domain = $domain;
|
||||
$this->vBucketId = LthnHash::vBucketId($domain);
|
||||
$this->apiKey = $apiKey ?? config("cdn.bunny.{$zone}.api_key", '');
|
||||
$this->storageZone = $storageZone ?? config("cdn.bunny.{$zone}.storage_zone", '');
|
||||
$this->region = $region ?? config("cdn.bunny.{$zone}.region", Client::STORAGE_ZONE_FS_EU);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the service is configured.
|
||||
*/
|
||||
public function isConfigured(): bool
|
||||
{
|
||||
return ! empty($this->apiKey) && ! empty($this->storageZone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Bunny Storage client.
|
||||
*/
|
||||
protected function client(): ?Client
|
||||
{
|
||||
if ($this->client === null && $this->isConfigured()) {
|
||||
$this->client = new Client($this->apiKey, $this->storageZone, $this->region);
|
||||
}
|
||||
|
||||
return $this->client;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the vBucket ID for this domain.
|
||||
*/
|
||||
public function id(): string
|
||||
{
|
||||
return $this->vBucketId;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the domain for this vBucket.
|
||||
*/
|
||||
public function domain(): string
|
||||
{
|
||||
return $this->domain;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a vBucket-scoped path.
|
||||
*/
|
||||
public function path(string $path): string
|
||||
{
|
||||
return $this->vBucketId.'/'.ltrim($path, '/');
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload a file to vBucket-scoped path.
|
||||
*/
|
||||
public function upload(string $localPath, string $remotePath): Response
|
||||
{
|
||||
if (! $this->isConfigured()) {
|
||||
return $this->error('Bunny Storage not configured');
|
||||
}
|
||||
|
||||
if (! file_exists($localPath)) {
|
||||
return $this->error('Local file not found', ['local_path' => $localPath]);
|
||||
}
|
||||
|
||||
$scopedPath = $this->path($remotePath);
|
||||
|
||||
try {
|
||||
$this->client()->upload($localPath, $scopedPath);
|
||||
|
||||
return $this->ok([
|
||||
'uploaded' => true,
|
||||
'domain' => $this->domain,
|
||||
'vbucket_id' => $this->vBucketId,
|
||||
'local_path' => $localPath,
|
||||
'remote_path' => $scopedPath,
|
||||
'size' => filesize($localPath),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Bunny Storage vBucket: Upload failed', [
|
||||
'domain' => $this->domain,
|
||||
'local' => $localPath,
|
||||
'remote' => $scopedPath,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Upload content directly to vBucket-scoped path.
|
||||
*/
|
||||
public function putContents(string $remotePath, string $contents): Response
|
||||
{
|
||||
if (! $this->isConfigured()) {
|
||||
return $this->error('Bunny Storage not configured');
|
||||
}
|
||||
|
||||
$scopedPath = $this->path($remotePath);
|
||||
|
||||
try {
|
||||
$this->client()->putContents($scopedPath, $contents);
|
||||
|
||||
return $this->ok([
|
||||
'uploaded' => true,
|
||||
'domain' => $this->domain,
|
||||
'vbucket_id' => $this->vBucketId,
|
||||
'remote_path' => $scopedPath,
|
||||
'size' => strlen($contents),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Bunny Storage vBucket: putContents failed', [
|
||||
'domain' => $this->domain,
|
||||
'remote' => $scopedPath,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file contents from vBucket-scoped path.
|
||||
*/
|
||||
public function getContents(string $remotePath): Response
|
||||
{
|
||||
if (! $this->isConfigured()) {
|
||||
return $this->error('Bunny Storage not configured');
|
||||
}
|
||||
|
||||
$scopedPath = $this->path($remotePath);
|
||||
|
||||
try {
|
||||
$contents = $this->client()->getContents($scopedPath);
|
||||
|
||||
return $this->ok([
|
||||
'contents' => $contents,
|
||||
'domain' => $this->domain,
|
||||
'vbucket_id' => $this->vBucketId,
|
||||
'remote_path' => $scopedPath,
|
||||
'size' => strlen($contents),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Bunny Storage vBucket: getContents failed', [
|
||||
'domain' => $this->domain,
|
||||
'remote' => $scopedPath,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete file from vBucket-scoped path.
|
||||
*/
|
||||
public function delete(string $remotePath): Response
|
||||
{
|
||||
if (! $this->isConfigured()) {
|
||||
return $this->error('Bunny Storage not configured');
|
||||
}
|
||||
|
||||
$scopedPath = $this->path($remotePath);
|
||||
|
||||
try {
|
||||
$this->client()->delete($scopedPath);
|
||||
|
||||
return $this->ok([
|
||||
'deleted' => true,
|
||||
'domain' => $this->domain,
|
||||
'vbucket_id' => $this->vBucketId,
|
||||
'path' => $scopedPath,
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Bunny Storage vBucket: Delete failed', [
|
||||
'domain' => $this->domain,
|
||||
'remote' => $scopedPath,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* List files in vBucket-scoped path.
|
||||
*/
|
||||
public function list(string $path = ''): Response
|
||||
{
|
||||
if (! $this->isConfigured()) {
|
||||
return $this->error('Bunny Storage not configured');
|
||||
}
|
||||
|
||||
$scopedPath = $this->path($path);
|
||||
|
||||
try {
|
||||
$files = $this->client()->listFiles($scopedPath);
|
||||
|
||||
return $this->ok([
|
||||
'path' => $scopedPath,
|
||||
'domain' => $this->domain,
|
||||
'vbucket_id' => $this->vBucketId,
|
||||
'files' => $files,
|
||||
'count' => count($files),
|
||||
]);
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Bunny Storage vBucket: List failed', [
|
||||
'domain' => $this->domain,
|
||||
'path' => $scopedPath,
|
||||
'error' => $e->getMessage(),
|
||||
]);
|
||||
|
||||
return $this->error($e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if file exists in vBucket-scoped path.
|
||||
*/
|
||||
public function exists(string $path): bool
|
||||
{
|
||||
if (! $this->isConfigured()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$scopedPath = $this->path($path);
|
||||
|
||||
try {
|
||||
$this->client()->getContents($scopedPath);
|
||||
|
||||
return true;
|
||||
} catch (\Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a vBucket for a workspace (convenience method).
|
||||
*/
|
||||
public static function forWorkspace(string $workspaceUuid, string $zone = 'public'): self
|
||||
{
|
||||
return new self($workspaceUuid, zone: $zone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a public zone vBucket.
|
||||
*/
|
||||
public static function public(string $domain): self
|
||||
{
|
||||
return new self($domain, zone: 'public');
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a private zone vBucket.
|
||||
*/
|
||||
public static function private(string $domain): self
|
||||
{
|
||||
return new self($domain, zone: 'private');
|
||||
}
|
||||
}
|
||||
28
src/Contract/Browseable.php
Normal file
28
src/Contract/Browseable.php
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Storage\Contract;
|
||||
|
||||
use Core\Plug\Response;
|
||||
|
||||
/**
|
||||
* File browsing and inspection operations.
|
||||
*/
|
||||
interface Browseable
|
||||
{
|
||||
/**
|
||||
* List files in a path.
|
||||
*/
|
||||
public function list(string $path = '/'): Response;
|
||||
|
||||
/**
|
||||
* Check if a file exists.
|
||||
*/
|
||||
public function exists(string $path): bool;
|
||||
|
||||
/**
|
||||
* Get file size in bytes.
|
||||
*/
|
||||
public function size(string $path): ?int;
|
||||
}
|
||||
25
src/Contract/Deletable.php
Normal file
25
src/Contract/Deletable.php
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Storage\Contract;
|
||||
|
||||
use Core\Plug\Response;
|
||||
|
||||
/**
|
||||
* File deletion operations.
|
||||
*/
|
||||
interface Deletable
|
||||
{
|
||||
/**
|
||||
* Delete a single file.
|
||||
*/
|
||||
public function path(string $remotePath): Response;
|
||||
|
||||
/**
|
||||
* Delete multiple files.
|
||||
*
|
||||
* @param array<string> $remotePaths
|
||||
*/
|
||||
public function paths(array $remotePaths): Response;
|
||||
}
|
||||
23
src/Contract/Downloadable.php
Normal file
23
src/Contract/Downloadable.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Storage\Contract;
|
||||
|
||||
use Core\Plug\Response;
|
||||
|
||||
/**
|
||||
* File download operations.
|
||||
*/
|
||||
interface Downloadable
|
||||
{
|
||||
/**
|
||||
* Get file contents as string.
|
||||
*/
|
||||
public function contents(string $remotePath): Response;
|
||||
|
||||
/**
|
||||
* Download file to local path.
|
||||
*/
|
||||
public function toFile(string $remotePath, string $localPath): Response;
|
||||
}
|
||||
23
src/Contract/Uploadable.php
Normal file
23
src/Contract/Uploadable.php
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Storage\Contract;
|
||||
|
||||
use Core\Plug\Response;
|
||||
|
||||
/**
|
||||
* File upload operations.
|
||||
*/
|
||||
interface Uploadable
|
||||
{
|
||||
/**
|
||||
* Upload a file from local path to remote path.
|
||||
*/
|
||||
public function file(string $localPath, string $remotePath): Response;
|
||||
|
||||
/**
|
||||
* Upload string contents directly to remote path.
|
||||
*/
|
||||
public function contents(string $remotePath, string $contents): Response;
|
||||
}
|
||||
196
src/StorageManager.php
Normal file
196
src/StorageManager.php
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Core\Plug\Storage;
|
||||
|
||||
use Core\Plug\Storage\Contract\Browseable;
|
||||
use Core\Plug\Storage\Contract\Deletable;
|
||||
use Core\Plug\Storage\Contract\Downloadable;
|
||||
use Core\Plug\Storage\Contract\Uploadable;
|
||||
use InvalidArgumentException;
|
||||
|
||||
/**
|
||||
* Storage Manager - Factory for storage provider operations.
|
||||
*
|
||||
* Resolves storage providers from config and provides type-safe access to operations.
|
||||
* Supports zone switching for providers with multiple storage zones (public/private).
|
||||
*
|
||||
* @example
|
||||
* $storage = app(StorageManager::class);
|
||||
*
|
||||
* // Get operations
|
||||
* $uploader = $storage->upload(); // Returns Uploadable for default driver
|
||||
* $downloader = $storage->download(); // Returns Downloadable
|
||||
*
|
||||
* // Specific driver
|
||||
* $uploader = $storage->driver('bunny')->upload();
|
||||
*
|
||||
* // Zone switching (for Bunny)
|
||||
* $uploader = $storage->zone('private')->upload();
|
||||
*
|
||||
* // vBucket for workspace isolation
|
||||
* $vbucket = $storage->vbucket('example.com');
|
||||
*/
|
||||
class StorageManager
|
||||
{
|
||||
protected string $defaultDriver;
|
||||
|
||||
protected string $zone = 'public';
|
||||
|
||||
/**
|
||||
* @var array<string, array{upload: class-string<Uploadable>, download: class-string<Downloadable>, delete: class-string<Deletable>, browse: class-string<Browseable>, vbucket?: class-string}>
|
||||
*/
|
||||
protected array $drivers = [
|
||||
'bunny' => [
|
||||
'upload' => Bunny\Upload::class,
|
||||
'download' => Bunny\Download::class,
|
||||
'delete' => Bunny\Delete::class,
|
||||
'browse' => Bunny\Browse::class,
|
||||
'vbucket' => Bunny\VBucket::class,
|
||||
],
|
||||
];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->defaultDriver = config('cdn.storage_driver', 'bunny');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the default driver name.
|
||||
*/
|
||||
public function getDefaultDriver(): string
|
||||
{
|
||||
return $this->defaultDriver;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the default driver.
|
||||
*/
|
||||
public function setDefaultDriver(string $driver): self
|
||||
{
|
||||
$this->defaultDriver = $driver;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a driver instance.
|
||||
*/
|
||||
public function driver(?string $driver = null): self
|
||||
{
|
||||
if ($driver !== null) {
|
||||
$this->defaultDriver = $driver;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the storage zone (public/private).
|
||||
*/
|
||||
public function zone(string $zone): self
|
||||
{
|
||||
$this->zone = $zone;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the upload operation for the current driver.
|
||||
*/
|
||||
public function upload(): Uploadable
|
||||
{
|
||||
return $this->resolve('upload');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the download operation for the current driver.
|
||||
*/
|
||||
public function download(): Downloadable
|
||||
{
|
||||
return $this->resolve('download');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the delete operation for the current driver.
|
||||
*/
|
||||
public function delete(): Deletable
|
||||
{
|
||||
return $this->resolve('delete');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the browse operation for the current driver.
|
||||
*/
|
||||
public function browse(): Browseable
|
||||
{
|
||||
return $this->resolve('browse');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a vBucket for workspace-isolated storage.
|
||||
*/
|
||||
public function vbucket(string $domain): Bunny\VBucket
|
||||
{
|
||||
if (! isset($this->drivers[$this->defaultDriver]['vbucket'])) {
|
||||
throw new InvalidArgumentException(
|
||||
"vBucket not available for storage driver [{$this->defaultDriver}]."
|
||||
);
|
||||
}
|
||||
|
||||
$class = $this->drivers[$this->defaultDriver]['vbucket'];
|
||||
|
||||
return new $class($domain, zone: $this->zone);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a driver is registered.
|
||||
*/
|
||||
public function hasDriver(string $driver): bool
|
||||
{
|
||||
return isset($this->drivers[$driver]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all registered driver names.
|
||||
*
|
||||
* @return array<string>
|
||||
*/
|
||||
public function drivers(): array
|
||||
{
|
||||
return array_keys($this->drivers);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a custom driver.
|
||||
*
|
||||
* @param array{upload?: class-string<Uploadable>, download?: class-string<Downloadable>, delete?: class-string<Deletable>, browse?: class-string<Browseable>, vbucket?: class-string} $operations
|
||||
*/
|
||||
public function extend(string $driver, array $operations): self
|
||||
{
|
||||
$this->drivers[$driver] = array_merge($this->drivers[$driver] ?? [], $operations);
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve an operation class for the current driver.
|
||||
*/
|
||||
protected function resolve(string $operation): object
|
||||
{
|
||||
if (! isset($this->drivers[$this->defaultDriver])) {
|
||||
throw new InvalidArgumentException("Storage driver [{$this->defaultDriver}] not registered.");
|
||||
}
|
||||
|
||||
if (! isset($this->drivers[$this->defaultDriver][$operation])) {
|
||||
throw new InvalidArgumentException(
|
||||
"Operation [{$operation}] not available for storage driver [{$this->defaultDriver}]."
|
||||
);
|
||||
}
|
||||
|
||||
$class = $this->drivers[$this->defaultDriver][$operation];
|
||||
|
||||
return new $class(zone: $this->zone);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue