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; } }