Files
ladill-transfer/app/Console/Commands/MigrateTransferStorageCommand.php
T
isaaccladandCursor aa2db7186a
Deploy Ladill Transfer / deploy (push) Successful in 50s
Store transfer files on Contabo Object Storage like Ladill Meet.
Add transfer-files S3 disk, config-driven uploads, and an artisan command to migrate existing local blobs to object storage.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-04 20:01:49 +00:00

132 lines
4.1 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Models\TransferFile;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Storage;
class MigrateTransferStorageCommand extends Command
{
protected $signature = 'transfer:migrate-storage
{--from=qr : Source disk for legacy transfer blobs}
{--to= : Target disk (defaults to transfer.files_disk config)}
{--dry-run : List files without copying or updating records}
{--delete-source : Remove local blobs after a successful upload}';
protected $description = 'Copy transfer file blobs from local disk to Contabo object storage and update transfer_files.disk';
public function handle(): int
{
$fromDisk = (string) $this->option('from');
$toDisk = (string) ($this->option('to') ?: config('transfer.files_disk', 'transfer-files'));
$dryRun = (bool) $this->option('dry-run');
$deleteSource = (bool) $this->option('delete-source');
if ($fromDisk === $toDisk) {
$this->error('Source and target disks must differ.');
return self::FAILURE;
}
if (! config("filesystems.disks.{$fromDisk}") || ! config("filesystems.disks.{$toDisk}")) {
$this->error('Unknown filesystem disk configured for migration.');
return self::FAILURE;
}
$files = TransferFile::query()
->where('disk', $fromDisk)
->orderBy('id')
->get();
if ($files->isEmpty()) {
$this->info("No transfer files on disk [{$fromDisk}].");
return self::SUCCESS;
}
$this->info(sprintf(
'%s migrating %d file(s) from [%s] to [%s].',
$dryRun ? 'Would migrate' : 'Migrating',
$files->count(),
$fromDisk,
$toDisk,
));
$migrated = 0;
$skipped = 0;
$failed = 0;
foreach ($files as $file) {
$label = "#{$file->id} {$file->path}";
if (! Storage::disk($fromDisk)->exists($file->path)) {
$this->warn("Missing source blob: {$label}");
$failed++;
continue;
}
if (Storage::disk($toDisk)->exists($file->path)) {
if (! $dryRun) {
$file->update(['disk' => $toDisk]);
if ($deleteSource) {
Storage::disk($fromDisk)->delete($file->path);
}
}
$this->line("Already on target, updated record: {$label}");
$skipped++;
continue;
}
if ($dryRun) {
$this->line("Would migrate: {$label}");
$migrated++;
continue;
}
try {
$stream = Storage::disk($fromDisk)->readStream($file->path);
if ($stream === false) {
throw new \RuntimeException('Could not open source stream.');
}
$written = Storage::disk($toDisk)->writeStream($file->path, $stream);
if (is_resource($stream)) {
fclose($stream);
}
if ($written === false) {
throw new \RuntimeException('Upload to target disk failed.');
}
if (! Storage::disk($toDisk)->exists($file->path)) {
throw new \RuntimeException('Uploaded blob not found on target disk.');
}
$file->update(['disk' => $toDisk]);
if ($deleteSource) {
Storage::disk($fromDisk)->delete($file->path);
}
$this->line("Migrated: {$label}");
$migrated++;
} catch (\Throwable $e) {
$this->error("Failed {$label}: {$e->getMessage()}");
$failed++;
}
}
$this->newLine();
$this->info("Done. migrated={$migrated} skipped={$skipped} failed={$failed}");
return $failed > 0 ? self::FAILURE : self::SUCCESS;
}
}