101 lines
4.2 KiB
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);
|
|
}
|
|
}
|
|
}
|