fn_b/app/Console/Commands/SyncIllustrationDeliveries.php

101 lines
4.2 KiB
PHP

<?php
namespace App\Console\Commands;
use App\Services\IllustrationPool;
use App\Services\LocalIllustrations;
use Illuminate\Console\Command;
final class SyncIllustrationDeliveries extends Command
{
protected $signature = 'illustrations:sync-deliveries';
protected $description = 'Synchronize pending illustration deliveries and acknowledge verified static copies';
public function handle(): int
{
if (! config('illustrations.delivery_enabled') || ! IllustrationPool::enabled() || ! LocalIllustrations::enabled()) {
$this->error('Illustration delivery is disabled');
return self::FAILURE;
}
$dir = dirname(config('illustrations.local_manifest'));
if (! is_dir($dir)) {
mkdir($dir, 0700, true);
}
$lock = fopen($dir.'/delivery.lock', 'c');
if (! $lock) {
return self::FAILURE;
}
if (! flock($lock, LOCK_EX | LOCK_NB)) {
fclose($lock);
return self::SUCCESS;
}
try {
$pending = array_slice(IllustrationPool::pendingDeliveries(), 0, 3); // Bound metadata calls below the consumer rate limit.
if (! $pending) {
$this->line('No pending illustration deliveries');
return self::SUCCESS;
}
$snapshots = [];
$images = [];
foreach ($pending as $batch) {
foreach ($batch['assets'] ?? [] as $asset) {
$pool = $asset['pool'] ?? '';
if (! in_array($pool, config('illustrations.categories'), true)) {
throw new \RuntimeException('invalid_pool');
}
if (! isset($snapshots[$pool])) {
$snapshots[$pool] = IllustrationPool::sourceSnapshot($pool);
}
$match = null;
foreach ($snapshots[$pool]['items'] as $image) {
if (($image['asset_id'] ?? '') === ($asset['asset_id'] ?? null)) {
$match = $image;
}
}
if (! $match) {
throw new \RuntimeException('delivery_asset_unavailable');
}
$images[$asset['asset_id']] = $match;
}
}
app(LocalIllustrations::class)->cacheDeliveryAssets(array_values($images));
$bytes = file_get_contents(config('illustrations.local_manifest'));
$manifest = json_decode($bytes, true, 512, JSON_THROW_ON_ERROR);
foreach ($pending as $batch) {
if (($batch['state'] ?? '') !== 'awaiting_sync' || empty($batch['assets'])) {
throw new \RuntimeException('invalid_delivery');
}
foreach ($batch['assets'] as $asset) {
$local = $manifest['assets'][$asset['asset_id']] ?? null;
$sha = $asset['sha256'] ?? '';
if (! preg_match('/\A[a-f0-9]{64}\z/D', $sha) || ! $local || ($local['sha256'] ?? '') !== $sha || ($local['mime'] ?? '') !== 'image/webp') {
throw new \RuntimeException('missing_copy');
}
$path = rtrim(config('illustrations.local_directory'), '/').'/'.$sha.'.webp';
if (! is_file($path) || is_link($path) || hash_file('sha256', $path) !== $sha) {
throw new \RuntimeException('invalid_copy');
}
}
$ack = IllustrationPool::acknowledgeDelivery($batch['id'], ['fingerprint' => $batch['fingerprint'], 'manifest_sha256' => hash('sha256', $bytes)]);
if (($ack['state'] ?? '') !== 'completed') {
throw new \RuntimeException('ack_failed');
}
$this->line('Completed illustration delivery '.$batch['id']);
}
return self::SUCCESS;
} catch (\Throwable) {
$this->error('Illustration delivery sync incomplete; will retry without a completion ACK.');
return self::FAILURE;
} finally {
flock($lock, LOCK_UN);
fclose($lock);
}
}
}