54178f6d70
實作 cloud-ready-s3-storage 的程式碼部分(tasks 1-3):
- config/filesystems.php:public disk 的 driver 改吃 FILESYSTEM_DISK env,
切換為 s3 時不需改動既有 6 個 Storage::disk('public') 呼叫點
- .env.example:FILESYSTEM_DISK 維持 local 預設(本機無 S3 服務),補齊
AWS_ENDPOINT/AWS_URL/AWS_USE_PATH_STYLE_ENDPOINT 供 R2 等服務使用
- 新增 storage:migrate-to-s3 指令:一次性把 storage/app/public 既有檔案
上傳到 S3,來源固定讀本機磁碟(不受 FILESYSTEM_DISK 影響),支援
--dry-run,失敗不中斷且不刪除本機檔案
- 新增測試:遷移指令(上傳/dry-run/部分失敗)+ public disk 在 local
driver 下零 AWS credentials 也能正常運作
239 tests passed / 578 assertions,容器內驗證無回歸。
R2 環境準備、VPS 部署與實際切換(tasks 4-6)待 Hank 自行處理。
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011LpcY9b7y9x4fusHBTXciQ
84 lines
2.5 KiB
PHP
84 lines
2.5 KiB
PHP
<?php
|
||
|
||
namespace App\Console\Commands;
|
||
|
||
use Illuminate\Console\Command;
|
||
use Illuminate\Support\Facades\Storage;
|
||
use Throwable;
|
||
|
||
class MigrateStorageToS3 extends Command
|
||
{
|
||
protected $signature = 'storage:migrate-to-s3
|
||
{--dry-run : 只列出將上傳的檔案與總數,不實際執行}';
|
||
|
||
protected $description = '將本機 storage/app/public 下既有上傳檔案搬遷至 S3 相容物件儲存(public disk 切換前的一次性遷移,不刪除本機檔案)';
|
||
|
||
public function handle(): int
|
||
{
|
||
// 直接指定 local 磁碟為來源,不透過會依 FILESYSTEM_DISK 變動的 'public' disk,
|
||
// 避免這支指令在 FILESYSTEM_DISK 已切成 s3 後誤把來源當成目的地。
|
||
$source = Storage::build([
|
||
'driver' => 'local',
|
||
'root' => storage_path('app/public'),
|
||
]);
|
||
|
||
$files = $source->allFiles();
|
||
|
||
if (empty($files)) {
|
||
$this->info('storage/app/public 底下沒有檔案,無需遷移。');
|
||
|
||
return self::SUCCESS;
|
||
}
|
||
|
||
if ($this->option('dry-run')) {
|
||
$this->info(sprintf('[dry-run] 將上傳 %d 個檔案:', count($files)));
|
||
|
||
foreach ($files as $file) {
|
||
$this->line(" - {$file}");
|
||
}
|
||
|
||
return self::SUCCESS;
|
||
}
|
||
|
||
$destination = Storage::disk('s3');
|
||
$failed = [];
|
||
|
||
$this->info(sprintf('開始上傳 %d 個檔案至 S3...', count($files)));
|
||
$bar = $this->output->createProgressBar(count($files));
|
||
|
||
foreach ($files as $file) {
|
||
try {
|
||
$destination->put($file, $source->get($file));
|
||
|
||
if (! $destination->exists($file)) {
|
||
$failed[] = $file;
|
||
}
|
||
} catch (Throwable $e) {
|
||
$failed[] = $file;
|
||
$this->newLine();
|
||
$this->warn("上傳失敗:{$file}({$e->getMessage()})");
|
||
}
|
||
|
||
$bar->advance();
|
||
}
|
||
|
||
$bar->finish();
|
||
$this->newLine(2);
|
||
|
||
$succeeded = count($files) - count($failed);
|
||
$this->info(sprintf('完成:%d/%d 個檔案上傳成功。', $succeeded, count($files)));
|
||
|
||
if (! empty($failed)) {
|
||
$this->error('以下檔案上傳失敗,本機檔案未刪除,可重新執行本指令補上傳:');
|
||
|
||
foreach ($failed as $file) {
|
||
$this->line(" - {$file}");
|
||
}
|
||
|
||
return self::FAILURE;
|
||
}
|
||
|
||
return self::SUCCESS;
|
||
}
|
||
}
|