aspirecloud/app/Console/Commands/PackageRepoIndexCommand.php
2025-10-25 13:15:51 -06:00

128 lines
4.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Values\Packages\FairMetadata;
use App\Values\Packages\PackageData;
use Closure;
use Exception;
use Illuminate\Pipeline\Pipeline;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Http;
use function Safe\ini_set;
class PackageRepoIndexCommand extends Command
{
protected $signature = 'package:repo-index {--stop-on-first-error}';
protected $description = 'Import packages from FAIR repositories';
private string $currentRepo = '';
private int $errors = 0;
private int $loaded = 0;
public function handle(Pipeline $pipeline): void
{
ini_set('memory_limit', '-1');
$repos = config('fair.repos', []);
if (empty($repos)) {
$this->fail('No FAIR repositories configured. Update the FAIR_REPOS environment variable.');
}
$stages = [
$this->readPackageMetadata(...),
$this->createPackage(...),
];
assert(is_iterable($repos));
foreach ($repos as $repo) {
assert(is_string($repo));
$this->currentRepo = $repo;
try {
$packages = $this->getRepoPackages($repo);
foreach ($packages as $did) {
try {
DB::transaction(
fn() => $pipeline
->send($did)
->through($stages)
->thenReturn(),
);
} catch (Exception $e) {
$this->errors++;
$this->error("Package $did: {$e->getMessage()}");
$this->option('stop-on-first-error') and $this->fail('Errors encountered -- aborting.');
}
}
} catch (Exception $e) {
$this->errors++;
$this->error("Repo $this->currentRepo: {$e->getMessage()}");
$this->option('stop-on-first-error') and $this->fail('Errors encountered -- aborting.');
}
}
if ($this->errors > 0) {
$this->fail("Indexed $this->loaded packages; $this->errors errors");
}
$this->info("Indexed $this->loaded packages.");
}
/** @return array<string, string> */
private function getRepoPackages(string $repoUrl): array
{
$this->info("Fetching packages from $repoUrl");
$response = HTTP::withUrlParameters([
'repoUrl' => rtrim($repoUrl, '/'),
'path' => trim(config('fair.paths.packages', '/wp-json/minifair/v1/packages'), '/'),
])->withHeaders(['Accept' => 'application/json'])
->get('{+repoUrl}/{+path}');
if ($response->failed()) {
throw new Exception("Failed to fetch $repoUrl");
}
$data = $response->json();
if (!is_array($data)) {
throw new Exception("Invalid JSON from $repoUrl");
}
return $data;
}
private function readPackageMetadata(string $did, Closure $next): void
{
$this->info("Fetching package $did metadata from $this->currentRepo");
$response = HTTP::withUrlParameters([
'repoUrl' => rtrim($this->currentRepo, '/'),
'path' => trim(config('fair.paths.packages', '/wp-json/minifair/v1/packages'), '/'),
'did' => $did,
])->withHeaders(['Accept' => 'application/json'])
->get('{+repoUrl}/{+path}/{+did}');
if ($response->failed()) {
throw new Exception("Failed to fetch package metadata from $this->currentRepo");
}
$metadata = $response->json();
if (!is_array($metadata)) {
throw new Exception("Invalid JSON from $this->currentRepo");
}
$next($metadata);
}
/** @param array<string, mixed> $metadata */
private function createPackage(array $metadata, Closure $next): void
{
$fairMetadata = FairMetadata::from($metadata);
$package = PackageData::from($fairMetadata);
$this->loaded++;
$next($package);
}
}