feat(ai-edits): add output storage backfill flow and coverage
This commit is contained in:
144
app/Console/Commands/AiEditsBackfillStorageCommand.php
Normal file
144
app/Console/Commands/AiEditsBackfillStorageCommand.php
Normal file
@@ -0,0 +1,144 @@
|
||||
<?php
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Models\AiEditOutput;
|
||||
use App\Models\AiEditRequest;
|
||||
use App\Services\AiEditing\AiEditOutputStorageService;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
|
||||
class AiEditsBackfillStorageCommand extends Command
|
||||
{
|
||||
protected $signature = 'ai-edits:backfill-storage
|
||||
{--request-id= : Restrict backfill to one AI edit request id}
|
||||
{--limit=200 : Maximum outputs to process}
|
||||
{--pretend : Dry run without writing changes}';
|
||||
|
||||
protected $description = 'Backfill local storage paths for AI outputs that only have provider URLs.';
|
||||
|
||||
public function __construct(private readonly AiEditOutputStorageService $outputStorage)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function handle(): int
|
||||
{
|
||||
$limit = max(1, (int) $this->option('limit'));
|
||||
$requestId = $this->normalizeRequestId($this->option('request-id'));
|
||||
$pretend = (bool) $this->option('pretend');
|
||||
|
||||
$query = AiEditOutput::query()
|
||||
->with('request')
|
||||
->whereNotNull('provider_url')
|
||||
->where(function (Builder $builder): void {
|
||||
$builder
|
||||
->whereNull('storage_path')
|
||||
->orWhere('storage_path', '');
|
||||
})
|
||||
->orderBy('id');
|
||||
|
||||
if ($requestId !== null) {
|
||||
$query->where('request_id', $requestId);
|
||||
}
|
||||
|
||||
$candidateCount = (clone $query)->count();
|
||||
$outputs = $query->limit($limit)->get();
|
||||
|
||||
if ($outputs->isEmpty()) {
|
||||
$this->info('No AI outputs require storage backfill.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$this->line(sprintf(
|
||||
'AI output backfill candidates: %d (processing up to %d).',
|
||||
$candidateCount,
|
||||
$limit
|
||||
));
|
||||
|
||||
if ($pretend) {
|
||||
$this->table(
|
||||
['Output ID', 'Request ID', 'Provider URL'],
|
||||
$outputs->map(static fn (AiEditOutput $output): array => [
|
||||
(string) $output->id,
|
||||
(string) $output->request_id,
|
||||
(string) $output->provider_url,
|
||||
])->all()
|
||||
);
|
||||
|
||||
$this->info('Pretend mode enabled. No records were changed.');
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$processed = 0;
|
||||
$stored = 0;
|
||||
$failed = 0;
|
||||
|
||||
foreach ($outputs as $output) {
|
||||
$processed++;
|
||||
|
||||
$request = $output->request;
|
||||
if (! $request instanceof AiEditRequest) {
|
||||
$failed++;
|
||||
$this->warn(sprintf('Output %d skipped: missing request relation.', $output->id));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$persisted = $this->outputStorage->persist($request, [
|
||||
'provider_url' => $output->provider_url,
|
||||
'provider_asset_id' => $output->provider_asset_id,
|
||||
'storage_disk' => $output->storage_disk,
|
||||
'storage_path' => $output->storage_path,
|
||||
'mime_type' => $output->mime_type,
|
||||
'width' => $output->width,
|
||||
'height' => $output->height,
|
||||
'bytes' => $output->bytes,
|
||||
'checksum' => $output->checksum,
|
||||
'metadata' => $output->metadata,
|
||||
]);
|
||||
|
||||
$output->forceFill([
|
||||
'provider_url' => $persisted['provider_url'] ?? $output->provider_url,
|
||||
'storage_disk' => $persisted['storage_disk'] ?? $output->storage_disk,
|
||||
'storage_path' => $persisted['storage_path'] ?? $output->storage_path,
|
||||
'mime_type' => $persisted['mime_type'] ?? $output->mime_type,
|
||||
'width' => array_key_exists('width', $persisted) ? $persisted['width'] : $output->width,
|
||||
'height' => array_key_exists('height', $persisted) ? $persisted['height'] : $output->height,
|
||||
'bytes' => array_key_exists('bytes', $persisted) ? $persisted['bytes'] : $output->bytes,
|
||||
'checksum' => $persisted['checksum'] ?? $output->checksum,
|
||||
'metadata' => is_array($persisted['metadata'] ?? null) ? $persisted['metadata'] : $output->metadata,
|
||||
])->save();
|
||||
|
||||
$storagePath = trim((string) ($output->storage_path ?? ''));
|
||||
if ($storagePath !== '') {
|
||||
$stored++;
|
||||
} else {
|
||||
$failed++;
|
||||
$this->warn(sprintf('Output %d could not be persisted locally.', $output->id));
|
||||
}
|
||||
}
|
||||
|
||||
$this->info(sprintf(
|
||||
'AI output backfill complete: processed=%d stored=%d failed=%d.',
|
||||
$processed,
|
||||
$stored,
|
||||
$failed
|
||||
));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
private function normalizeRequestId(mixed $value): ?int
|
||||
{
|
||||
if (! is_numeric($value)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$requestId = (int) $value;
|
||||
|
||||
return $requestId > 0 ? $requestId : null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user