Skip to content

Commit

Permalink
Added Archive module; add code to download and extract files
Browse files Browse the repository at this point in the history
  • Loading branch information
roxblnfk committed Jul 18, 2024
1 parent 4a47a2e commit 3abc193
Show file tree
Hide file tree
Showing 18 changed files with 418 additions and 41 deletions.
2 changes: 2 additions & 0 deletions bin/dload
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
declare(strict_types=1);

use Internal\DLoad\Command;
use Internal\DLoad\Info;
use Symfony\Component\Console\Application;
use Symfony\Component\Console\CommandLoader\FactoryCommandLoader;

Expand Down Expand Up @@ -48,5 +49,6 @@ if ('cli' !== PHP_SAPI) {
]),
);
$application->setDefaultCommand(Command\Get::getDefaultName(), true);
$application->setVersion(Info::version());
$application->run();
})();
6 changes: 3 additions & 3 deletions dload.xml
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,15 @@
<registry>
<software name="RoadRunner" alias="rr" description="High performant Application server">
<repository type="github" uri="roadrunner-server/roadrunner" asset-pattern="/^roadrunner-.*/"/>
<file rename="rr" pattern="/^(roadrunner|rr)(?:\.exe)?/" />
<file rename="rr" pattern="/^(roadrunner|rr)(?:\.exe)?$/" />
</software>
<software name="Dolt" description="Dolt is a SQL database that you can fork, clone, branch, merge, push and pull just like a Git repository">
<repository type="github" uri="dolthub/dolt" asset-pattern="/^dolt-.*/"/>
<file pattern="/^(dolt)(?:\.exe)?/" />
<file pattern="/^(dolt)(?:\.exe)?$/" />
</software>
<software name="Temporal" description="Temporal command-line interface and development server">
<repository type="github" uri="temporalio/cli" asset-pattern="/^temporal_cli_.*/"/>
<file pattern="/^(temporal)(?:\.exe)?/" />
<file pattern="/^(temporal)(?:\.exe)?$/" />
</software>
</registry>
</dload>
3 changes: 3 additions & 0 deletions resources/version.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
".": "0.1.0"
}
116 changes: 100 additions & 16 deletions src/Command/Get.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
namespace Internal\DLoad\Command;

use Internal\DLoad\Bootstrap;
use Internal\DLoad\Module\Archive\ArchiveFactory;
use Internal\DLoad\Module\Common\Architecture;
use Internal\DLoad\Module\Common\Config\Destination;
use Internal\DLoad\Module\Common\Config\Embed\File;
use Internal\DLoad\Module\Common\OperatingSystem;
use Internal\DLoad\Module\Common\Stability;
use Internal\DLoad\Module\Downloader\Downloader;
use Internal\DLoad\Module\Downloader\SoftwareCollection;
use Internal\DLoad\Module\Repository\Internal\GitHub\GitHubRepository;
use Internal\DLoad\Module\Downloader\Task\DownloadResult;
use Internal\DLoad\Service\Container;
use Internal\DLoad\Service\Logger;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
Expand All @@ -20,6 +23,8 @@
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\StyleInterface;
use Symfony\Component\Console\Style\SymfonyStyle;

/**
* @internal
Expand All @@ -34,6 +39,8 @@ final class Get extends Command implements SignalableCommandInterface

private Logger $logger;

private Container $container;

public function configure(): void
{
$this->addArgument('binary', InputArgument::REQUIRED, 'Binary name, e.g. "rr", "dolt", "temporal" etc.');
Expand Down Expand Up @@ -76,43 +83,120 @@ protected function execute(
$output->writeln('Binary to load: ' . $input->getArgument('binary'));
$output->writeln('Path to store the binary: ' . $input->getOption('path'));

$container = Bootstrap::init()->withConfig(
$this->container = $container = Bootstrap::init()->withConfig(
xml: \dirname(__DIR__, 2) . '/dload.xml',
inputOptions: $input->getOptions(),
inputArguments: $input->getArguments(),
environment: \getenv(),
)->finish();
$container->set($input, InputInterface::class);
$container->set(new SymfonyStyle($input, $output), StyleInterface::class);
$container->set($this->logger);

$output->writeln('Architecture: ' . $container->get(Architecture::class)->name);
$output->writeln('Op. system: ' . $container->get(OperatingSystem::class)->name);
$output->writeln('Stability: ' . $container->get(Stability::class)->name);

$output->writeln(' Op. system: ' . $container->get(OperatingSystem::class)->name);
$output->writeln(' Stability: ' . $container->get(Stability::class)->name);

/** @var SoftwareCollection $softwareCollection */
$softwareCollection = $container->get(SoftwareCollection::class);

/** @var ArchiveFactory $archiveFactory */
$archiveFactory = $container->get(ArchiveFactory::class);
/** @var Downloader $downloader */
$downloader = $container->get(Downloader::class);

// / /*
$task = $downloader->download(
$softwareCollection->findSoftware('rr') ?? throw new \RuntimeException('Software not found.'),
// trap(...),
static fn() => null,
);
/*/
$task = new \Internal\DLoad\Module\Downloader\Task\DownloadTask(
$softwareCollection->findSoftware('rr') ?? throw new \RuntimeException('Software not found.'),
static fn() => null,
fn(): \React\Promise\PromiseInterface => \React\Promise\resolve(new DownloadResult(
new \SplFileInfo('C:\Users\test\AppData\Local\Temp\roadrunner-2024.1.5-windows-amd64.zip'),
'2024.1.5'
)),
);
//*/

($task->handler)()->then(
function (DownloadResult $downloadResult) use ($task, $archiveFactory, $output): void {
$fileInfo = $downloadResult->file;
$archive = $archiveFactory->create($fileInfo);
$extractor = $archive->extract();

while ($extractor->valid()) {
$file = $extractor->current();
\assert($file instanceof \SplFileInfo);

$to = $this->shouldBeExtracted($file, $task->software->files);

if ($to === null || !$this->checkExisting($to)) {
$extractor->next();
continue;
}

$extractor->send($to);

// Success
$path = $to->getRealPath() ?: $to->getPathname();
$output->writeln(\sprintf(
'%s (<comment>%s</comment>) has been installed into <info>%s</info>',
$to->getFilename(),
$downloadResult->version,
$path,
));

$to->isExecutable() or @\chmod($path, 0755);
}
},
);

return Command::SUCCESS;
}

// $container->get(Destination::class),
/**
* @return bool True if the file should be extracted, false otherwise.
*/
private function checkExisting(\SplFileInfo $bin): bool
{
if (! \is_file($bin->getPathname())) {
return true;
}

($task->handler)();
/** @var StyleInterface $io */
$io = $this->container->get(StyleInterface::class);
$io->warning('File already exists: ' . $bin->getPathname());
if (!$io->confirm('Do you want overwrite it?', false)) {
$io->note('Skipping ' . $bin->getFilename() . ' installation...');
return false;
}

// $repo = 'roadrunner-server/roadrunner';
// trap(
// GitHubRepository::fromDsn($repo)->getReleases()->first()->getAssets()
// ->whereArchitecture($container->get(Architecture::class))
// ->whereOperatingSystem($container->get(OperatingSystem::class)),
// );
return true;
}

/**
* @param array<File> $mapping
*/
private function shouldBeExtracted(\SplFileInfo $source, array $mapping): ?\SplFileInfo
{
/** @var Destination $destination */
$destination = $this->container->get(Destination::class);
$path = $destination->path ?? \getcwd();

foreach ($mapping as $conf) {
if (\preg_match($conf->pattern, $source->getFilename())) {
$newName = match(true) {
$conf->rename === null => $source->getFilename(),
$source->getExtension() === '' => $conf->rename,
default => $conf->rename . '.' . $source->getExtension(),
};

return new \SplFileInfo($path . DIRECTORY_SEPARATOR . $newName);
}
}

return Command::SUCCESS;
return null;
}
}
47 changes: 47 additions & 0 deletions src/Info.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
<?php

declare(strict_types=1);

namespace Internal\DLoad;

/**
* @internal
*/
class Info
{
public const NAME = 'DLoad';

public const LOGO_CLI_COLOR = '';

public const ROOT_DIR = __DIR__ . '/..';

private const VERSION = 'experimental';

/**
* Returns the version of the Trap.
*
* @return non-empty-string
*/
public static function version(): string
{
/** @var non-empty-string|null $cache */
static $cache = null;

if ($cache !== null) {
return $cache;
}

$fileContent = \file_get_contents(self::ROOT_DIR . '/resources/version.json');

if ($fileContent === false) {
return $cache = self::VERSION;
}

/** @var mixed $version */
$version = \json_decode($fileContent, true)['.'] ?? null;

return $cache = \is_string($version) && $version !== ''
? $version
: self::VERSION;
}
}
16 changes: 16 additions & 0 deletions src/Module/Archive/Archive.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

namespace Internal\DLoad\Module\Archive;

interface Archive
{
/**
* Iterate archive files. If a {@see \SplFileInfo} is backed into the generator, the file will be
* extracted to the given location.
*
* @return \Generator<non-empty-string, \SplFileInfo, \SplFileInfo|null, void>
*/
public function extract(): \Generator;
}
84 changes: 84 additions & 0 deletions src/Module/Archive/ArchiveFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

declare(strict_types=1);

namespace Internal\DLoad\Module\Archive;

use Closure as ArchiveMatcher;
use Internal\DLoad\Module\Archive\Internal\PharArchive;
use Internal\DLoad\Module\Archive\Internal\TarPharArchive;
use Internal\DLoad\Module\Archive\Internal\ZipPharArchive;

/**
* @psalm-type ArchiveMatcher = \Closure(\SplFileInfo): ?Archive
*/
final class ArchiveFactory
{
/**
* @var array<ArchiveMatcher>
*/
private array $matchers = [];

/**
* FactoryTrait constructor.
*/
public function __construct()
{
$this->bootDefaultMatchers();
}

public function extend(\Closure $matcher): void
{
\array_unshift($this->matchers, $matcher);
}

public function create(\SplFileInfo $file): Archive
{
$errors = [];

foreach ($this->matchers as $matcher) {
try {
if ($archive = $matcher($file)) {
return $archive;
}
} catch (\Throwable $e) {
$errors[] = ' - ' . $e->getMessage();
continue;
}
}

$error = \sprintf("Can not open the archive \"%s\":\n%s", $file->getFilename(), \implode(\PHP_EOL, $errors));

throw new \InvalidArgumentException($error);
}

private function bootDefaultMatchers(): void
{
$this->extend($this->matcher(
'zip',
static fn(\SplFileInfo $info): Archive => new ZipPharArchive($info),
));

$this->extend($this->matcher(
'tar.gz',
static fn(\SplFileInfo $info): Archive => new TarPharArchive($info),
));

$this->extend($this->matcher(
'phar',
static fn(\SplFileInfo $info): Archive => new PharArchive($info),
));
}

/**
* @param string $extension
* @param ArchiveMatcher $then
*
* @return ArchiveMatcher
*/
private function matcher(string $extension, \Closure $then): \Closure
{
return static fn(\SplFileInfo $info): ?Archive =>
\str_ends_with(\strtolower($info->getFilename()), '.' . $extension) ? $then($info) : null;
}
}
36 changes: 36 additions & 0 deletions src/Module/Archive/Internal/Archive.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
<?php

declare(strict_types=1);

namespace Internal\DLoad\Module\Archive\Internal;

use Internal\DLoad\Module\Archive\Archive as ArchiveInterface;

abstract class Archive implements ArchiveInterface
{
/**
* @param \SplFileInfo $archive
*/
public function __construct(\SplFileInfo $archive)
{
$this->assertArchiveValid($archive);
}

/**
* @param \SplFileInfo $archive
*/
private function assertArchiveValid(\SplFileInfo $archive): void
{
if (! $archive->isFile()) {
throw new \InvalidArgumentException(
\sprintf('Archive "%s" is not a file.', $archive->getFilename()),
);
}

if (! $archive->isReadable()) {
throw new \InvalidArgumentException(
\sprintf('Archive file "%s" is not readable.', $archive->getFilename()),
);
}
}
}
Loading

0 comments on commit 3abc193

Please sign in to comment.