diff --git a/.env b/.env index 9a8bb987..9bafdaf6 100644 --- a/.env +++ b/.env @@ -73,6 +73,7 @@ GA_TRACKING= ###< google analytics ### ###> storage ### +STORAGE_SOURCE=storage.local PROXY_DIST_DIR=%kernel.project_dir%/var/proxy PACKAGES_DIST_DIR=%kernel.project_dir%/var/repo SECURITY_ADVISORIES_DB_DIR=%kernel.project_dir%/var/security-advisories diff --git a/.env.docker b/.env.docker index f7592966..452180e9 100644 --- a/.env.docker +++ b/.env.docker @@ -77,6 +77,7 @@ GA_TRACKING= ###< google analytics ### ###> storage ### +STORAGE_SOURCE=storage.local PROXY_DIST_DIR=%kernel.project_dir%/var/proxy PACKAGES_DIST_DIR=%kernel.project_dir%/var/repo SECURITY_ADVISORIES_DB_DIR=%kernel.project_dir%/var/security-advisories diff --git a/composer.json b/composer.json index 0c346d04..ab00d80d 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ "doctrine/orm": "^2.7", "knplabs/github-api": "^2.12", "knpuniversity/oauth2-client-bundle": "^2.0", + "league/flysystem-bundle": "^1.5", "league/oauth2-github": "^2.0", "m4tthumphrey/php-gitlab-api": "^9.17", "munusphp/munus": "^0.2.1", diff --git a/composer.lock b/composer.lock index 0da49f33..1f4cb082 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "0f679e9733bd2ec24aef029ff10233e9", + "content-hash": "1e09a95f4cde1b6e3c5a3b42ffd17002", "packages": [ { "name": "bitbucket/client", @@ -2776,6 +2776,180 @@ ], "time": "2020-05-20T16:45:56+00:00" }, + { + "name": "league/flysystem", + "version": "1.0.69", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem.git", + "reference": "7106f78428a344bc4f643c233a94e48795f10967" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem/zipball/7106f78428a344bc4f643c233a94e48795f10967", + "reference": "7106f78428a344bc4f643c233a94e48795f10967", + "shasum": "", + "mirrors": [ + { + "url": "https://repo.repman.io/dists/%package%/%version%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "ext-fileinfo": "*", + "php": ">=5.5.9" + }, + "conflict": { + "league/flysystem-sftp": "<1.0.6" + }, + "require-dev": { + "phpspec/phpspec": "^3.4", + "phpunit/phpunit": "^5.7.26" + }, + "suggest": { + "ext-fileinfo": "Required for MimeType", + "ext-ftp": "Allows you to use FTP server storage", + "ext-openssl": "Allows you to use FTPS server storage", + "league/flysystem-aws-s3-v2": "Allows you to use S3 storage with AWS SDK v2", + "league/flysystem-aws-s3-v3": "Allows you to use S3 storage with AWS SDK v3", + "league/flysystem-azure": "Allows you to use Windows Azure Blob storage", + "league/flysystem-cached-adapter": "Flysystem adapter decorator for metadata caching", + "league/flysystem-eventable-filesystem": "Allows you to use EventableFilesystem", + "league/flysystem-rackspace": "Allows you to use Rackspace Cloud Files", + "league/flysystem-sftp": "Allows you to use SFTP server storage via phpseclib", + "league/flysystem-webdav": "Allows you to use WebDAV storage", + "league/flysystem-ziparchive": "Allows you to use ZipArchive adapter", + "spatie/flysystem-dropbox": "Allows you to use Dropbox storage", + "srmklive/flysystem-dropbox-v2": "Allows you to use Dropbox storage for PHP 5 applications" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1-dev" + } + }, + "autoload": { + "psr-4": { + "League\\Flysystem\\": "src/" + } + }, + "notification-url": "https://repo.repman.io/downloads", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Frank de Jonge", + "email": "info@frenky.net" + } + ], + "description": "Filesystem abstraction: Many filesystems, one API.", + "keywords": [ + "Cloud Files", + "WebDAV", + "abstraction", + "aws", + "cloud", + "copy.com", + "dropbox", + "file systems", + "files", + "filesystem", + "filesystems", + "ftp", + "rackspace", + "remote", + "s3", + "sftp", + "storage" + ], + "support": { + "issues": "https://github.com/thephpleague/flysystem/issues", + "source": "https://github.com/thephpleague/flysystem/tree/1.0.69" + }, + "funding": [ + { + "url": "https://offset.earth/frankdejonge", + "type": "other" + } + ], + "time": "2020-05-18T15:13:39+00:00" + }, + { + "name": "league/flysystem-bundle", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/flysystem-bundle.git", + "reference": "d485109f8130d938bf08caf25933a94468becf60" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/flysystem-bundle/zipball/d485109f8130d938bf08caf25933a94468becf60", + "reference": "d485109f8130d938bf08caf25933a94468becf60", + "shasum": "", + "mirrors": [ + { + "url": "https://repo.repman.io/dists/%package%/%version%/%reference%.%type%", + "preferred": true + } + ] + }, + "require": { + "league/flysystem": "^1.0.40", + "php": ">=7.1", + "symfony/config": "^4.2|^5.0", + "symfony/dependency-injection": "^4.2|^5.0", + "symfony/http-kernel": "^4.2|^5.0", + "symfony/options-resolver": "^4.2|^5.0" + }, + "conflict": { + "league/flysystem-aws-s3-v3": "<1.0.22", + "league/flysystem-cached-adapter": "<1.0.9" + }, + "require-dev": { + "async-aws/flysystem-s3": "^0.3", + "league/flysystem-aws-s3-v3": "^1.0.22", + "league/flysystem-azure-blob-storage": "^0.1.5", + "league/flysystem-cached-adapter": "^1.0.9", + "league/flysystem-memory": "^1.0", + "league/flysystem-rackspace": "^1.0", + "league/flysystem-replicate-adapter": "^1.0", + "league/flysystem-sftp": "^1.0", + "league/flysystem-webdav": "^1.0", + "league/flysystem-ziparchive": "^1.0", + "phpunit/phpunit": "^7.4", + "spatie/flysystem-dropbox": "^1.0", + "superbalist/flysystem-google-storage": "^7.2", + "symfony/dotenv": "^4.2|^5.0", + "symfony/framework-bundle": "^4.2|^5.0", + "symfony/var-dumper": "^4.1|^5.0", + "symfony/yaml": "^4.2|^5.0" + }, + "type": "symfony-bundle", + "autoload": { + "psr-4": { + "League\\FlysystemBundle\\": "src" + } + }, + "notification-url": "https://repo.repman.io/downloads", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Titouan Galopin", + "email": "galopintitouan@gmail.com" + } + ], + "description": "Symfony bundle integrating Flysystem into Symfony 4.2+ applications", + "support": { + "issues": "https://github.com/thephpleague/flysystem-bundle/issues", + "source": "https://github.com/thephpleague/flysystem-bundle/tree/1.5.0" + }, + "time": "2020-04-04T22:09:59+00:00" + }, { "name": "league/oauth2-client", "version": "2.4.1", @@ -12206,5 +12380,6 @@ "ext-pdo_pgsql": "*", "ext-zip": "*" }, - "platform-dev": [] + "platform-dev": [], + "plugin-api-version": "2.0.0" } diff --git a/config/bundles.php b/config/bundles.php index a766ec96..f2e911c2 100644 --- a/config/bundles.php +++ b/config/bundles.php @@ -15,4 +15,5 @@ DAMA\DoctrineTestBundle\DAMADoctrineTestBundle::class => ['test' => true], Doctrine\Bundle\FixturesBundle\DoctrineFixturesBundle::class => ['dev' => true, 'test' => true], KnpU\OAuth2ClientBundle\KnpUOAuth2ClientBundle::class => ['all' => true], + League\FlysystemBundle\FlysystemBundle::class => ['all' => true], ]; diff --git a/config/packages/flysystem.yaml b/config/packages/flysystem.yaml new file mode 100644 index 00000000..ada549b2 --- /dev/null +++ b/config/packages/flysystem.yaml @@ -0,0 +1,12 @@ +# Read the documentation at https://github.com/thephpleague/flysystem-bundle/blob/master/docs/1-getting-started.md +flysystem: + storages: + storage.local.proxy: + adapter: 'local' + options: + directory: '%dists_dir%' + + proxy.storage: + adapter: 'lazy' + options: + source: '%env(STORAGE_SOURCE)%.proxy' diff --git a/config/services.yaml b/config/services.yaml index e09434af..0927a856 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -25,6 +25,7 @@ services: $distsDir: '%dists_dir%' $resetPasswordTokenTtl: 86400 # 24h Symfony\Component\HttpFoundation\Session\Session $session: '@session' + $proxyFilesystem: '@proxy.storage' # makes classes in src/ available to be used as services # this creates a service per class whose id is the fully-qualified class name diff --git a/src/Controller/OrganizationController.php b/src/Controller/OrganizationController.php index 1b5d9dde..8b49fc4d 100644 --- a/src/Controller/OrganizationController.php +++ b/src/Controller/OrganizationController.php @@ -342,14 +342,6 @@ public function packageScanResults(Organization $organization, Package $package, ]); } - protected function getUser(): User - { - /** @var User $user */ - $user = parent::getUser(); - - return $user; - } - private function tryToRemoveWebhook(Package $package): void { if ($package->webhookCreatedAt() !== null) { diff --git a/src/Controller/ProxyController.php b/src/Controller/ProxyController.php index 1ad9b135..6774bb6f 100644 --- a/src/Controller/ProxyController.php +++ b/src/Controller/ProxyController.php @@ -7,12 +7,15 @@ use Buddy\Repman\Message\Proxy\AddDownloads; use Buddy\Repman\Message\Proxy\AddDownloads\Package; use Buddy\Repman\Service\Proxy; +use Buddy\Repman\Service\Proxy\Metadata; use Buddy\Repman\Service\Proxy\ProxyRegister; +use Buddy\Repman\Service\Symfony\ResponseCallback; use Munus\Control\Option; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; -use Symfony\Component\HttpFoundation\BinaryFileResponse; use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpFoundation\Response; +use Symfony\Component\HttpFoundation\StreamedResponse; use Symfony\Component\HttpKernel\Exception\NotFoundHttpException; use Symfony\Component\Routing\Annotation\Route; use Symfony\Component\Routing\RouterInterface; @@ -47,34 +50,59 @@ public function packages(): JsonResponse } /** - * @Route("/p/{package}", name="package_provider", requirements={"package"="%package_name_pattern%"}, methods={"GET"}) + * @Route("/p/{package}", + * name="package_legacy_metadata", + * host="repo.{domain}", + * defaults={"domain"="%domain%"}, + * requirements={"package"="%package_name_pattern%","domain"="%domain%"}, + * methods={"GET"}) */ - public function provider(string $package): JsonResponse + public function legacyMetadata(string $package): Response { - return new JsonResponse($this->register->all() - ->map(fn (Proxy $proxy) => $proxy->providerData($package)) + /** @var Metadata $metadata */ + $metadata = $this->register->all() + ->map(fn (Proxy $proxy) => $proxy->legacyMetadata($package)) ->find(fn (Option $option) => !$option->isEmpty()) ->map(fn (Option $option) => $option->get()) - ->getOrElse(['packages' => new \stdClass()]) - ); + ->getOrElse(Metadata::fromString('{"packages": {}}')); + + return (new StreamedResponse(ResponseCallback::fromStream($metadata->stream()), 200, [ + 'Accept-Ranges' => 'bytes', + 'Content-Type' => 'application/json', + /* @phpstan-ignore-next-line */ + 'Content-Length' => fstat($metadata->stream())['size'], + ])) + ->setPublic() + ->setLastModified((new \DateTime())->setTimestamp($metadata->timestamp())) + ; } /** * @Route("/p2/{package}.json", - * name="package_provider_v2", + * name="package_metadata", * host="repo.{domain}", * defaults={"domain"="%domain%"}, * requirements={"package"="%package_name_pattern%","domain"="%domain%"}, * methods={"GET"}) */ - public function providerV2(string $package): JsonResponse + public function metadata(string $package): Response { - return new JsonResponse($this->register->all() - ->map(fn (Proxy $proxy) => $proxy->providerDataV2($package)) + /** @var Metadata $metadata */ + $metadata = $this->register->all() + ->map(fn (Proxy $proxy) => $proxy->metadata($package)) ->find(fn (Option $option) => !$option->isEmpty()) ->map(fn (Option $option) => $option->get()) - ->getOrElse(['packages' => new \stdClass()]) - ); + ->getOrElseThrow(new NotFoundHttpException()); + + return (new StreamedResponse(ResponseCallback::fromStream($metadata->stream()), 200, [ + 'Accept-Ranges' => 'bytes', + 'Content-Type' => 'application/json', + /* @phpstan-ignore-next-line */ + 'Content-Length' => fstat($metadata->stream())['size'], + ])) + ->setPublic() + ->setLastModified((new \DateTime())->setTimestamp($metadata->timestamp())) + ; } /** @@ -85,14 +113,24 @@ public function providerV2(string $package): JsonResponse * requirements={"package"="%package_name_pattern%","ref"="[a-f0-9]*?","type"="zip|tar","domain"="%domain%"}, * methods={"GET"}) */ - public function distribution(string $package, string $version, string $ref, string $type): BinaryFileResponse + public function distribution(string $package, string $version, string $ref, string $type): Response { - return new BinaryFileResponse($this->register->all() - ->map(fn (Proxy $proxy) => $proxy->distFilename($package, $version, $ref, $type)) + /** @var resource $stream */ + $stream = $this->register->all() + ->map(fn (Proxy $proxy) => $proxy->distribution($package, $version, $ref, $type)) ->find(fn (Option $option) => !$option->isEmpty()) ->map(fn (Option $option) => $option->get()) - ->getOrElseThrow(new NotFoundHttpException('This distribution file can not be found or downloaded from origin url.')) - ); + ->getOrElseThrow(new NotFoundHttpException('This distribution file can not be found or downloaded from origin url.')); + + return (new StreamedResponse(ResponseCallback::fromStream($stream), 200, [ + 'Accept-Ranges' => 'bytes', + 'Content-Type' => 'application/zip', + /* @phpstan-ignore-next-line */ + 'Content-Length' => fstat($stream)['size'], + ])) + ->setPublic() + ->setEtag($ref) + ; } /** diff --git a/src/Service/Cache.php b/src/Service/Cache.php deleted file mode 100644 index 041422ae..00000000 --- a/src/Service/Cache.php +++ /dev/null @@ -1,24 +0,0 @@ -> - */ - public function get(string $path, callable $supplier, int $expireTime = 0): Option; - - /** - * @return Option> - */ - public function find(string $path, int $expireTime = 0): Option; - - public function exists(string $path, int $expireTime = 0): bool; - - public function removeOld(string $path): void; -} diff --git a/src/Service/Cache/FileCache.php b/src/Service/Cache/FileCache.php deleted file mode 100644 index 2e82eaf8..00000000 --- a/src/Service/Cache/FileCache.php +++ /dev/null @@ -1,106 +0,0 @@ -basePath = $basePath; - $this->exceptionHandler = $exceptionHandler; - } - - public function get(string $path, callable $supplier, int $expireTime = 0): Option - { - $filename = $this->getPath($path); - if (is_readable($filename) && ($expireTime === 0 || filemtime($filename) > time() - $expireTime)) { - return Option::some(unserialize((string) file_get_contents($filename))); - } - - $this->ensureDirExist($filename); - - return TryTo::run($supplier) - ->onSuccess(function ($value) use ($filename): void {AtomicFile::write($filename, serialize($value)); }) - ->onFailure(function (\Throwable $throwable): void {$this->exceptionHandler->handle($throwable); }) - ->map(fn ($value) => Option::some($value)) - ->getOrElse(Option::none()); - } - - public function removeOld(string $path): void - { - $dir = $this->getPath(dirname($path)); - if (false === ($length = strpos(basename($path), '$')) || !is_dir($dir)) { - return; - } - - $pattern = substr(basename($path), 0, $length).'$*'; - $files = Finder::create()->files()->ignoreVCS(true)->name($pattern)->in($dir); - - foreach ($files as $file) { - /* @var SplFileInfo $file */ - @unlink($file->getPathname()); - } - } - - public function find(string $path, int $expireTime = 0): Option - { - $dir = $this->getPath(dirname($path)); - if (!is_dir($dir)) { - return Option::none(); - } - - $pattern = basename($path).'$*'; - /** @var SplFileInfo[] $files */ - $files = iterator_to_array(Finder::create()->files()->ignoreVCS(true)->ignoreUnreadableDirs(true)->name($pattern)->in($dir)->getIterator()); - if ($files === []) { - return Option::none(); - } - - $filename = current($files)->getPathname(); - if ($expireTime !== 0 && filemtime($filename) <= time() - $expireTime) { - return Option::none(); - } - - return Option::some(unserialize((string) file_get_contents($filename))); - } - - public function exists(string $path, int $expireTime = 0): bool - { - $filename = $this->getPath($path); - - return is_readable($filename) && ($expireTime === 0 || filemtime($filename) > time() - $expireTime); - } - - private function getPath(string $path): string - { - return sprintf('%s/%s', $this->basePath, $path); - } - - private function ensureDirExist(string $filename): void - { - $dirname = dirname($filename); - if (!is_dir($dirname)) { - mkdir($dirname, 0777, true); - } - } -} diff --git a/src/Service/Cache/InMemoryCache.php b/src/Service/Cache/InMemoryCache.php deleted file mode 100644 index 43181a76..00000000 --- a/src/Service/Cache/InMemoryCache.php +++ /dev/null @@ -1,40 +0,0 @@ - - */ - private array $cache; - - public function get(string $path, callable $supplier, int $expireTime = 0): Option - { - if (!isset($this->cache[$path])) { - $this->cache[$path] = $supplier(); - } - - return Option::some($this->cache[$path]); - } - - public function removeOld(string $path): void - { - // TODO: Implement remove() method. - } - - public function find(string $path, int $expireTime = 0): Option - { - return isset($this->cache[$path]) ? Option::some($this->cache[$path]) : Option::none(); - } - - public function exists(string $path, int $expireTime = 0): bool - { - return isset($this->cache[$path]); - } -} diff --git a/src/Service/Proxy.php b/src/Service/Proxy.php index d60913b4..ab45beda 100644 --- a/src/Service/Proxy.php +++ b/src/Service/Proxy.php @@ -4,78 +4,71 @@ namespace Buddy\Repman\Service; -use Buddy\Repman\Service\Dist\Storage; -use Buddy\Repman\Service\Proxy\MetadataProvider; -use Composer\Semver\VersionParser; +use Buddy\Repman\Service\Proxy\Metadata; +use League\Flysystem\FileNotFoundException; +use League\Flysystem\FilesystemInterface; use Munus\Collection\GenericList; use Munus\Control\Option; final class Proxy { - public const PACKAGES_PATH = 'packages.json'; - public const PACKAGES_EXPIRE_TIME = 60; - private string $url; private string $name; - private MetadataProvider $metadataProvider; - private Storage $distStorage; - private VersionParser $versionParser; - - public function __construct(string $name, string $url, MetadataProvider $metadataProvider, Storage $distStorage) - { + private FilesystemInterface $filesystem; + private Downloader $downloader; + + public function __construct( + string $name, + string $url, + FilesystemInterface $proxyFilesystem, + Downloader $downloader + ) { $this->name = $name; $this->url = rtrim($url, '/'); - $this->metadataProvider = $metadataProvider; - $this->distStorage = $distStorage; - $this->versionParser = new VersionParser(); + $this->filesystem = $proxyFilesystem; + $this->downloader = $downloader; } /** - * @return Option + * @return Option */ - public function distFilename(string $package, string $version, string $ref, string $format): Option + public function metadata(string $package): Option { - $dist = new Dist($this->name, $package, $version, $ref, $format); - if (!$this->distStorage->has($dist)) { - $this->tryToDownload($package, $version, $dist); - } - $distFilename = $this->distStorage->filename($dist); - - return Option::when(file_exists($distFilename), $distFilename); + return $this->fetchMetadata(sprintf('%s/p2/%s.json', $this->url, $package)); } /** - * @return Option> + * @return Option */ - public function providerData(string $package, int $expireTime = self::PACKAGES_EXPIRE_TIME): Option + public function distribution(string $package, string $version, string $ref, string $format): Option { - if (!($fromPath = $this->metadataProvider->fromPath('p/'.$package, $this->url, $expireTime))->isEmpty()) { - return $fromPath; + $path = $this->distPath($package, $ref, $format); + if (!$this->filesystem->has($path)) { + foreach ($this->decodeMetadata($package) as $packageData) { + if (($packageData['dist']['reference'] ?? '') === $ref) { + $this->filesystem->write($path, $this->downloader->getContents($packageData['dist']['url']) + ->getOrElseThrow(new \RuntimeException(sprintf('Failed to download file from %s', $packageData['dist']['url']))) + ); + break; + } + } } - $providerPath = $this->getProviderPath($package); - if ($providerPath->isEmpty()) { + try { + $stream = $this->filesystem->readStream($path); + + return $stream !== false ? Option::some($stream) : Option::none(); + } catch (FileNotFoundException $exception) { return Option::none(); } - - return $this->metadataProvider->fromUrl($this->getUrl($providerPath->get())); } /** - * @return Option> + * @return Option */ - public function providerDataV2(string $package, int $expireTime = self::PACKAGES_EXPIRE_TIME): Option + public function legacyMetadata(string $package): Option { - if (!($fromPath = $this->metadataProvider->fromPath('p2/'.$package, $this->url, $expireTime))->isEmpty()) { - return $fromPath; - } - - $providerPath = $this->getProviderPathV2($package); - if ($providerPath->isEmpty()) { - return Option::none(); - } - - return $this->metadataProvider->fromUrl($this->getUrl($providerPath->get())); + return $this->fetchMetadata(sprintf('%s/p/%s.json', $this->url, $package)); } /** @@ -83,101 +76,81 @@ public function providerDataV2(string $package, int $expireTime = self::PACKAGES */ public function syncedPackages(): GenericList { - return $this->distStorage->packages($this->name); + $packages = GenericList::empty(); + foreach ($this->filesystem->listContents(sprintf('%s/dist', $this->name)) as $vendor) { + foreach ($this->filesystem->listContents($vendor['path']) as $package) { + $packages = $packages->append($vendor['basename'].'/'.$package['basename']); + } + } + + return $packages; } - public function downloadByVersion(string $package, string $version, bool $fromCache = true): void + public function downloadByVersion(string $package, string $version): void { - $normalizedVersion = $this->versionParser->normalize($version); - $providerData = $this->providerData($package, $fromCache ? 0 : -60)->getOrElse([]); - - foreach ($providerData['packages'][$package] ?? [] as $packageData) { - $packageVersion = $packageData['version_normalized'] ?? $this->versionParser->normalize($packageData['version']); - $packageDist = $packageData['dist']; - - if ($packageVersion !== $normalizedVersion && isset($packageDist['url'], $packageDist['reference'])) { - $this->distStorage->download($packageDist['url'], new Dist( - $this->name, - $package, - $normalizedVersion, - $packageDist['reference'], - $packageDist['type'] - )); + $lastDist = null; + + foreach ($this->decodeMetadata($package) as $packageData) { + $lastDist = $packageData['dist'] ?? $lastDist; + if ($version === $packageData['version']) { + $this->filesystem->write($this->distPath($package, $lastDist['reference'], $lastDist['type']), $this->downloader->getContents($lastDist['url']) + ->getOrElseThrow(new \RuntimeException(sprintf('Failed to download file from %s', $lastDist['url']))) + ); break; } } } /** - * @return Option + * @return mixed[] */ - private function getProviderPath(string $packageName): Option + private function decodeMetadata(string $package): array { - $root = $this->getRootPackages(); - if (isset($root['provider-includes'])) { - foreach ($root['provider-includes'] as $url => $meta) { - $data = $this->metadataProvider->fromUrl($this->getUrl(str_replace('%hash%', $meta['sha256'], $url)))->getOrElse([]); - if (isset($data['providers'][$packageName])) { - return Option::some( - (string) str_replace( - ['%package%', '%hash%'], - [$packageName, $data['providers'][$packageName]['sha256']], - $root['providers-url'] - ) - ); - } - } - } + /** @var Metadata $metadata */ + $metadata = $this->metadata($package)->getOrElse(Metadata::fromString('[]')); + $metadata = json_decode((string) stream_get_contents($metadata->stream()), true); - return Option::none(); + return is_array($metadata) ? ($metadata['packages'][$package] ?? []) : []; } /** - * @return Option + * @return Option */ - private function getProviderPathV2(string $packageName): Option + private function fetchMetadata(string $url): Option { - $root = $this->getRootPackages(); - if (isset($root['metadata-url'])) { - return Option::some( - (string) str_replace( - ['%package%'], - [$packageName], - $root['metadata-url'] - ) - ); + $path = $this->metadataPath($url); + if (!$this->filesystem->has($path)) { + $metadata = $this->downloader->getContents($url)->getOrNull(); + if ($metadata === null) { + return Option::none(); + } + $this->filesystem->write($path, $metadata); } - return Option::none(); - } + $stream = $this->filesystem->readStream($path); + if ($stream === false) { + return Option::none(); + } - /** - * @return array - */ - private function getRootPackages(): array - { - return $this->metadataProvider->fromUrl($this->getUrl(self::PACKAGES_PATH), self::PACKAGES_EXPIRE_TIME)->getOrElse([]); + return Option::some(new Metadata( + (int) $this->filesystem->getTimestamp($path), + $stream + )); } - private function getUrl(string $path): string + private function distPath(string $package, string $ref, string $format): string { - return sprintf('%s/%s', $this->url, $path); + return sprintf( + '%s/dist/%s/%s.%s', + (string) parse_url($this->url, PHP_URL_HOST), + $package, + $ref, + $format + ); } - private function tryToDownload(string $package, string $version, Dist $dist, bool $fromCache = true): void + private function metadataPath(string $url): string { - $providerData = $this->providerData($package, $fromCache ? 0 : -60)->getOrElse([]); - foreach ($providerData['packages'][$package] ?? [] as $packageData) { - $packageVersion = $packageData['version_normalized'] ?? $this->versionParser->normalize($packageData['version']); - if (($packageVersion === $version || md5($packageVersion) === $version) && isset($packageData['dist']['url'])) { - $this->distStorage->download($packageData['dist']['url'], $dist); - - return; - } - } - - if ($fromCache) { - $this->tryToDownload($package, $version, $dist, false); - } + return (string) parse_url($url, PHP_URL_HOST).'/'.ltrim((string) parse_url($url, PHP_URL_PATH), '/'); } } diff --git a/src/Service/Proxy/Metadata.php b/src/Service/Proxy/Metadata.php new file mode 100644 index 00000000..c077d9a3 --- /dev/null +++ b/src/Service/Proxy/Metadata.php @@ -0,0 +1,49 @@ +timestamp = $timestamp; + $this->stream = $stream; + } + + public static function fromString(string $string): self + { + $stream = fopen('php://memory', 'r+'); + if ($stream === false) { + throw new \RuntimeException('Failed to open in-memory stream'); + } + fwrite($stream, $string); + rewind($stream); + + return new self(time(), $stream); + } + + public function timestamp(): int + { + return $this->timestamp; + } + + /** + * @return resource + */ + public function stream() + { + return $this->stream; + } +} diff --git a/src/Service/Proxy/MetadataProvider.php b/src/Service/Proxy/MetadataProvider.php deleted file mode 100644 index 52b9f5d5..00000000 --- a/src/Service/Proxy/MetadataProvider.php +++ /dev/null @@ -1,20 +0,0 @@ -> - */ - public function fromUrl(string $url, int $expireTime = 0): Option; - - /** - * @return Option> - */ - public function fromPath(string $package, string $repoUrl, int $expireTime = 0): Option; -} diff --git a/src/Service/Proxy/MetadataProvider/CacheableMetadataProvider.php b/src/Service/Proxy/MetadataProvider/CacheableMetadataProvider.php deleted file mode 100644 index 2547367c..00000000 --- a/src/Service/Proxy/MetadataProvider/CacheableMetadataProvider.php +++ /dev/null @@ -1,61 +0,0 @@ -downloader = $downloader; - $this->cache = $cache; - } - - /** - * @return Option> - */ - public function fromUrl(string $url, int $expireTime = 0): Option - { - $path = $this->getPath($url); - - return $this->cache->get($path, function () use ($url, $path, $expireTime): array { - $content = $this->downloader->getContents($url)->getOrElseThrow( - new \RuntimeException(sprintf('Failed to download metadata from %s', $url)) - ); - - if ($expireTime === 0) { - $this->cache->removeOld($path); - } - - return Json::decode($content); - }, $expireTime); - } - - /** - * @return Option> - */ - public function fromPath(string $package, string $repoUrl, int $expireTime = 0): Option - { - if (!$this->cache->exists($this->getPath($repoUrl.'/'.Proxy::PACKAGES_PATH), $expireTime)) { - return Option::none(); - } - - return $this->cache->find((string) parse_url($repoUrl, PHP_URL_HOST).'/'.$package, $expireTime); - } - - private function getPath(string $url): string - { - return (string) parse_url($url, PHP_URL_HOST).'/'.ltrim((string) parse_url($url, PHP_URL_PATH), '/'); - } -} diff --git a/src/Service/Proxy/ProxyFactory.php b/src/Service/Proxy/ProxyFactory.php index ba78a43d..3131d0f4 100644 --- a/src/Service/Proxy/ProxyFactory.php +++ b/src/Service/Proxy/ProxyFactory.php @@ -4,18 +4,19 @@ namespace Buddy\Repman\Service\Proxy; -use Buddy\Repman\Service\Dist\Storage; +use Buddy\Repman\Service\Downloader; use Buddy\Repman\Service\Proxy; +use League\Flysystem\FilesystemInterface; final class ProxyFactory { - private MetadataProvider $metadataProvider; - private Storage $distStorage; + private Downloader $downloader; + private FilesystemInterface $filesystem; - public function __construct(MetadataProvider $metadataProvider, Storage $distStorage) + public function __construct(Downloader $downloader, FilesystemInterface $proxyFilesystem) { - $this->metadataProvider = $metadataProvider; - $this->distStorage = $distStorage; + $this->downloader = $downloader; + $this->filesystem = $proxyFilesystem; } public function create(string $url): Proxy @@ -23,8 +24,8 @@ public function create(string $url): Proxy return new Proxy( (string) parse_url($url, PHP_URL_HOST), $url, - $this->metadataProvider, - $this->distStorage + $this->filesystem, + $this->downloader ); } } diff --git a/src/Service/Symfony/ResponseCallback.php b/src/Service/Symfony/ResponseCallback.php new file mode 100644 index 00000000..5e830894 --- /dev/null +++ b/src/Service/Symfony/ResponseCallback.php @@ -0,0 +1,22 @@ +basePath.parse_url($url, PHP_URL_PATH); + if (file_exists($path)) { return Option::some((string) file_get_contents($path)); } diff --git a/tests/Doubles/FakeMetadataProvider.php b/tests/Doubles/FakeMetadataProvider.php deleted file mode 100644 index 28562096..00000000 --- a/tests/Doubles/FakeMetadataProvider.php +++ /dev/null @@ -1,38 +0,0 @@ - - */ - private array $metadata = []; - - public function fromUrl(string $url, int $expireTime = 0): Option - { - if (!isset($this->metadata[$url])) { - return Option::none(); - } - - return Option::some($this->metadata[$url]); - } - - public function fromPath(string $package, string $repoUrl, int $expireTime = 0): Option - { - return Option::none(); - } - - /** - * @param array $metadata - */ - public function setMetadata(string $url, array $metadata): void - { - $this->metadata[$url] = $metadata; - } -} diff --git a/tests/Functional/Command/ProxySyncReleasesCommandTest.php b/tests/Functional/Command/ProxySyncReleasesCommandTest.php index 8b99d995..beca2ac5 100644 --- a/tests/Functional/Command/ProxySyncReleasesCommandTest.php +++ b/tests/Functional/Command/ProxySyncReleasesCommandTest.php @@ -5,13 +5,8 @@ namespace Buddy\Repman\Tests\Functional\Command; use Buddy\Repman\Command\ProxySyncReleasesCommand; -use Buddy\Repman\Service\Cache\InMemoryCache; -use Buddy\Repman\Service\Dist\Storage\FileStorage; use Buddy\Repman\Service\Downloader; -use Buddy\Repman\Service\Proxy\MetadataProvider\CacheableMetadataProvider; -use Buddy\Repman\Service\Proxy\ProxyFactory; use Buddy\Repman\Service\Proxy\ProxyRegister; -use Buddy\Repman\Tests\Doubles\FakeDownloader; use Buddy\Repman\Tests\Functional\FunctionalTestCase; use Doctrine\DBAL\Connection; use Munus\Control\Option; @@ -25,7 +20,7 @@ final class ProxySyncReleasesCommandTest extends FunctionalTestCase { private string $basePath = __DIR__.'/../../Resources'; private FilesystemAdapter $cache; - private string $newDistPath = '/packagist.org/dist/buddy-works/repman/1.2.3.0_5e77ad71826b9411cb873c0947a7d541d822dff1.zip'; + private string $newDistPath = '/packagist.org/dist/buddy-works/repman/61e39aa8197cf1bc7fcb16a6f727b0c291bc9b76.zip'; private string $feedPath = '/packagist.org/feed/releases.rss'; public function testSyncReleases(): void @@ -87,16 +82,8 @@ private function prepareCommand(string $feed, bool $fromCache = false, bool $loc $feedDownloader = $this->createMock(Downloader::class); $feedDownloader->method('getContents')->willReturn(Option::of($feed)); - $storageDownloader = $this->createMock(Downloader::class); - $storageDownloader->method('getContents')->willReturn(Option::of('test')); - return new ProxySyncReleasesCommand( - new ProxyRegister( - new ProxyFactory( - new CacheableMetadataProvider(new FakeDownloader(), new InMemoryCache()), - new FileStorage($this->basePath, $storageDownloader) - ) - ), + $this->container()->get(ProxyRegister::class), $feedDownloader, $this->cache(), $lockFactory diff --git a/tests/Functional/Controller/Admin/ConfigControllerTest.php b/tests/Functional/Controller/Admin/ConfigControllerTest.php index 904c8683..565a5df3 100644 --- a/tests/Functional/Controller/Admin/ConfigControllerTest.php +++ b/tests/Functional/Controller/Admin/ConfigControllerTest.php @@ -83,6 +83,9 @@ public function testToggleAuthenticationOptions(): void $this->client->request('GET', $this->urlTo('register_buddy_start')); self::assertEquals(Response::HTTP_NOT_FOUND, $this->client->getResponse()->getStatusCode()); + $this->client->request('GET', $this->urlTo('app_register_confirm', ['token' => '825f33c5-2311-41ec-ba18-e967027b3f6f'])); + self::assertEquals(Response::HTTP_NOT_FOUND, $this->client->getResponse()->getStatusCode()); + $this->client->request('GET', $this->urlTo('admin_config')); $this->client->submitForm('save', [ 'local_authentication' => 'login_and_registration', diff --git a/tests/Functional/Controller/ProxyControllerTest.php b/tests/Functional/Controller/ProxyControllerTest.php index aa0fbb95..bbf7589b 100644 --- a/tests/Functional/Controller/ProxyControllerTest.php +++ b/tests/Functional/Controller/ProxyControllerTest.php @@ -35,9 +35,9 @@ public function testPackagesAction(): void public function testProviderAction(): void { - $this->client->request('GET', '/p/buddy-works/repman', [], [], [ + $response = $this->contentFromStream(fn () => $this->client->request('GET', '/p/buddy-works/repman', [], [], [ 'HTTP_HOST' => 'repo.repman.wip', - ]); + ])); self::assertMatchesPattern(' { @@ -46,27 +46,28 @@ public function testProviderAction(): void "buddy-works/repman": "@array@" } } - ', $this->client->getResponse()->getContent()); + ', $response); + self::assertTrue($this->client->getResponse()->isCacheable()); } public function testProviderActionEmptyPackagesWhenNotExist(): void { - $this->client->request('GET', '/p/buddy-works/example-app', [], [], [ + $response = $this->contentFromStream(fn () => $this->client->request('GET', '/p/buddy-works/example-app', [], [], [ 'HTTP_HOST' => 'repo.repman.wip', - ]); + ])); self::assertMatchesPattern(' { "packages": {} } - ', $this->client->getResponse()->getContent()); + ', $response); } public function testProviderV2Action(): void { - $this->client->request('GET', '/p2/buddy-works/repman.json', [], [], [ + $response = $this->contentFromStream(fn () => $this->client->request('GET', '/p2/buddy-works/repman.json', [], [], [ 'HTTP_HOST' => 'repo.repman.wip', - ]); + ])); self::assertMatchesPattern(' { @@ -75,16 +76,27 @@ public function testProviderV2Action(): void "buddy-works/repman": "@array@" } } - ', $this->client->getResponse()->getContent()); + ', $response); + self::assertTrue($this->client->getResponse()->isCacheable()); } - public function testDistributionAction(): void + public function testProviderV2ActionWhenPackageNotExist(): void { - $this->client->request('GET', '/dists/buddy-works/repman/0.1.2.0/f0c896a759d4e2e1eff57978318e841911796305.zip', [], [], [ + $this->client->request('GET', '/p2/buddy-works/example-app.json', [], [], [ 'HTTP_HOST' => 'repo.repman.wip', ]); + self::assertTrue($this->client->getResponse()->isNotFound()); + } + + public function testDistributionAction(): void + { + $file = $this->contentFromStream(fn () => $this->client->request('GET', '/dists/buddy-works/repman/0.1.2.0/f0c896a759d4e2e1eff57978318e841911796305.zip', [], [], [ + 'HTTP_HOST' => 'repo.repman.wip', + ])); + self::assertTrue($this->client->getResponse()->isOk()); + self::assertTrue($this->client->getResponse()->isCacheable()); } public function testDistributionNotFoundAction(): void diff --git a/tests/Functional/FunctionalTestCase.php b/tests/Functional/FunctionalTestCase.php index fe550e66..83f5b23f 100644 --- a/tests/Functional/FunctionalTestCase.php +++ b/tests/Functional/FunctionalTestCase.php @@ -24,6 +24,16 @@ protected function setUp(): void $this->fixtures = new FixturesManager(self::$kernel->getContainer()->get('test.service_container')); } + public function contentFromStream(callable $request): string + { + ob_start(); + $request(); + $content = (string) ob_get_contents(); + ob_end_clean(); + + return $content; + } + /** * @param array $parameters */ diff --git a/tests/Resources/p/buddy-works/repman$cd34b123d88f963b6ccdce310612bf6ae337fb50cd8d83f4412ae591443231d0.json b/tests/Resources/p/buddy-works/repman.json similarity index 100% rename from tests/Resources/p/buddy-works/repman$cd34b123d88f963b6ccdce310612bf6ae337fb50cd8d83f4412ae591443231d0.json rename to tests/Resources/p/buddy-works/repman.json diff --git a/tests/Resources/packagist.org/dist/buddy-works/repman/0.1.2.0_f0c896a759d4e2e1eff57978318e841911796305.zip b/tests/Resources/packagist.org/dist/buddy-works/repman/f0c896a759d4e2e1eff57978318e841911796305.zip similarity index 100% rename from tests/Resources/packagist.org/dist/buddy-works/repman/0.1.2.0_f0c896a759d4e2e1eff57978318e841911796305.zip rename to tests/Resources/packagist.org/dist/buddy-works/repman/f0c896a759d4e2e1eff57978318e841911796305.zip diff --git a/tests/Resources/packagist.org/p/buddy-works/repman.json b/tests/Resources/packagist.org/p/buddy-works/repman.json new file mode 100644 index 00000000..ff5f8e30 --- /dev/null +++ b/tests/Resources/packagist.org/p/buddy-works/repman.json @@ -0,0 +1,202 @@ +{ + "packages": { + "buddy-works\/repman": { + "0.1.0": { + "name": "buddy-works\/repman", + "description": "Power of object-oriented programming with the elegance of functional programming.", + "keywords": [], + "homepage": "", + "version": "0.1.0", + "version_normalized": "0.1.0.0-RC1", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Arkadiusz Kondas", + "email": "arkadiusz.kondas@gmail.com" + } + ], + "source": { + "type": "git", + "url": "https:\/\/github.com\/munusphp\/munus.git", + "reference": "5e77ad71826b9411cb873c0947a7d541d822dff1" + }, + "dist": { + "type": "zip", + "url": "https:\/\/api.github.com\/repos\/munusphp\/munus\/zipball\/5e77ad71826b9411cb873c0947a7d541d822dff1", + "reference": "5e77ad71826b9411cb873c0947a7d541d822dff1", + "shasum": "" + }, + "type": "library", + "time": "2019-11-29T21:18:39+00:00", + "autoload": { + "psr-4": { + "Munus\\": "src\/" + } + }, + "require": { + "php": "^7.2" + }, + "require-dev": { + "friendsofphp\/php-cs-fixer": "^2.15", + "phpunit\/phpunit": "^8.4", + "vimeo\/psalm": "^3.6", + "psalm\/plugin-phpunit": "^0.7.0", + "phpstan\/phpstan": "^0.11.19" + }, + "uid": 3420230, + "notification-url": "https:\/\/packagist.org\/downloads\/" + }, + "2.0.0": { + "version": "2.0.0", + "dist":{ + "type": "zip", + "url": "https:\/\/api.github.com\/repos\/buddy-works\/repman\/zipball\/not-found", + "reference": "not-found", + "shasum": "" + } + }, + "0.1.2": { + "name": "buddy-works\/repman", + "description": "Power of object-oriented programming with the elegance of functional programming.", + "keywords": [], + "homepage": "", + "version": "0.1.2", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Arkadiusz Kondas", + "email": "arkadiusz.kondas@gmail.com" + } + ], + "source": { + "type": "git", + "url": "https:\/\/github.com\/munusphp\/munus.git", + "reference": "f0c896a759d4e2e1eff57978318e841911796305" + }, + "dist": { + "type": "zip", + "url": "https:\/\/api.github.com\/repos\/munusphp\/munus\/zipball\/f0c896a759d4e2e1eff57978318e841911796305", + "reference": "f0c896a759d4e2e1eff57978318e841911796305", + "shasum": "" + }, + "type": "library", + "time": "2019-11-29T21:18:39+00:00", + "autoload": { + "psr-4": { + "Munus\\": "src\/" + } + }, + "require": { + "php": "^7.2" + }, + "require-dev": { + "friendsofphp\/php-cs-fixer": "^2.15", + "phpunit\/phpunit": "^8.4", + "vimeo\/psalm": "^3.6", + "psalm\/plugin-phpunit": "^0.7.0", + "phpstan\/phpstan": "^0.11.19" + }, + "uid": 3420230, + "notification-url": "https:\/\/packagist.org\/downloads\/" + }, + "dev-feature/awesome": { + "name": "buddy-works\/repman", + "description": "Power of object-oriented programming with the elegance of functional programming.", + "keywords": [], + "homepage": "", + "version": "dev-feature/awesome", + "version_normalized": "dev-feature/awesome", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Arkadiusz Kondas", + "email": "arkadiusz.kondas@gmail.com" + } + ], + "source": { + "type": "git", + "url": "https:\/\/github.com\/munusphp\/munus.git", + "reference": "e738ed3634a11f6b5e23aca3d1c3f9be4efd8cfb" + }, + "dist": { + "type": "zip", + "url": "https:\/\/api.github.com\/repos\/munusphp\/munus\/zipball\/e738ed3634a11f6b5e23aca3d1c3f9be4efd8cfb", + "reference": "e738ed3634a11f6b5e23aca3d1c3f9be4efd8cfb", + "shasum": "" + }, + "type": "library", + "time": "2019-12-08T08:35:40+00:00", + "autoload": { + "psr-4": { + "Munus\\": "src\/" + } + }, + "require": { + "php": "^7.2" + }, + "require-dev": { + "friendsofphp\/php-cs-fixer": "^2.15", + "phpunit\/phpunit": "^8.4", + "vimeo\/psalm": "^3.6", + "psalm\/plugin-phpunit": "^0.7.0", + "phpstan\/phpstan": "^0.11.19" + }, + "uid": 3386500, + "notification-url": "https:\/\/packagist.org\/downloads\/" + }, + "dev-master": { + "name": "buddy-works\/repman", + "description": "Power of object-oriented programming with the elegance of functional programming.", + "keywords": [], + "homepage": "", + "version": "dev-master", + "version_normalized": "9999999-dev", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Arkadiusz Kondas", + "email": "arkadiusz.kondas@gmail.com" + } + ], + "source": { + "type": "git", + "url": "https:\/\/github.com\/munusphp\/munus.git", + "reference": "e738ed3634a11f6b5e23aca3d1c3f9be4efd8cfb" + }, + "dist": { + "type": "zip", + "url": "https:\/\/api.github.com\/repos\/munusphp\/munus\/zipball\/e738ed3634a11f6b5e23aca3d1c3f9be4efd8cfb", + "reference": "e738ed3634a11f6b5e23aca3d1c3f9be4efd8cfb", + "shasum": "" + }, + "type": "library", + "time": "2019-12-08T08:35:40+00:00", + "autoload": { + "psr-4": { + "Munus\\": "src\/" + } + }, + "require": { + "php": "^7.2" + }, + "require-dev": { + "friendsofphp\/php-cs-fixer": "^2.15", + "phpunit\/phpunit": "^8.4", + "vimeo\/psalm": "^3.6", + "psalm\/plugin-phpunit": "^0.7.0", + "phpstan\/phpstan": "^0.11.19" + }, + "uid": 3386500, + "notification-url": "https:\/\/packagist.org\/downloads\/" + } + } + } +} diff --git a/tests/Resources/packagist.org/p2/buddy-works/repman.json b/tests/Resources/packagist.org/p2/buddy-works/repman.json new file mode 100644 index 00000000..1728cdc3 --- /dev/null +++ b/tests/Resources/packagist.org/p2/buddy-works/repman.json @@ -0,0 +1,211 @@ +{ + "packages": { + "buddy-works\/repman": { + "0.1.0": { + "name": "buddy-works\/repman", + "description": "Power of object-oriented programming with the elegance of functional programming.", + "keywords": [], + "homepage": "", + "version": "0.1.0", + "version_normalized": "0.1.0.0-RC1", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Arkadiusz Kondas", + "email": "arkadiusz.kondas@gmail.com" + } + ], + "source": { + "type": "git", + "url": "https:\/\/github.com\/munusphp\/munus.git", + "reference": "5e77ad71826b9411cb873c0947a7d541d822dff1" + }, + "dist": { + "type": "zip", + "url": "https:\/\/api.github.com\/repos\/munusphp\/munus\/zipball\/5e77ad71826b9411cb873c0947a7d541d822dff1", + "reference": "5e77ad71826b9411cb873c0947a7d541d822dff1", + "shasum": "" + }, + "type": "library", + "time": "2019-11-29T21:18:39+00:00", + "autoload": { + "psr-4": { + "Munus\\": "src\/" + } + }, + "require": { + "php": "^7.2" + }, + "require-dev": { + "friendsofphp\/php-cs-fixer": "^2.15", + "phpunit\/phpunit": "^8.4", + "vimeo\/psalm": "^3.6", + "psalm\/plugin-phpunit": "^0.7.0", + "phpstan\/phpstan": "^0.11.19" + }, + "uid": 3420230, + "notification-url": "https:\/\/packagist.org\/downloads\/" + }, + "2.0.0": { + "version": "2.0.0", + "dist":{ + "type": "zip", + "url": "https:\/\/api.github.com\/repos\/buddy-works\/repman\/zipball\/not-found", + "reference": "not-found", + "shasum": "" + } + }, + "1.2.3": { + "version": "1.2.3", + "dist": { + "type": "zip", + "url": "https:\/\/api.github.com\/zipball\/61e39aa8197cf1bc7fcb16a6f727b0c291bc9b76", + "reference": "61e39aa8197cf1bc7fcb16a6f727b0c291bc9b76", + "shasum": "" + } + }, + "0.1.2": { + "name": "buddy-works\/repman", + "description": "Power of object-oriented programming with the elegance of functional programming.", + "keywords": [], + "homepage": "", + "version": "0.1.2", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Arkadiusz Kondas", + "email": "arkadiusz.kondas@gmail.com" + } + ], + "source": { + "type": "git", + "url": "https:\/\/github.com\/munusphp\/munus.git", + "reference": "f0c896a759d4e2e1eff57978318e841911796305" + }, + "dist": { + "type": "zip", + "url": "https:\/\/api.github.com\/repos\/munusphp\/munus\/zipball\/f0c896a759d4e2e1eff57978318e841911796305", + "reference": "f0c896a759d4e2e1eff57978318e841911796305", + "shasum": "" + }, + "type": "library", + "time": "2019-11-29T21:18:39+00:00", + "autoload": { + "psr-4": { + "Munus\\": "src\/" + } + }, + "require": { + "php": "^7.2" + }, + "require-dev": { + "friendsofphp\/php-cs-fixer": "^2.15", + "phpunit\/phpunit": "^8.4", + "vimeo\/psalm": "^3.6", + "psalm\/plugin-phpunit": "^0.7.0", + "phpstan\/phpstan": "^0.11.19" + }, + "uid": 3420230, + "notification-url": "https:\/\/packagist.org\/downloads\/" + }, + "dev-feature/awesome": { + "name": "buddy-works\/repman", + "description": "Power of object-oriented programming with the elegance of functional programming.", + "keywords": [], + "homepage": "", + "version": "dev-feature/awesome", + "version_normalized": "dev-feature/awesome", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Arkadiusz Kondas", + "email": "arkadiusz.kondas@gmail.com" + } + ], + "source": { + "type": "git", + "url": "https:\/\/github.com\/munusphp\/munus.git", + "reference": "e738ed3634a11f6b5e23aca3d1c3f9be4efd8cfb" + }, + "dist": { + "type": "zip", + "url": "https:\/\/api.github.com\/repos\/munusphp\/munus\/zipball\/e738ed3634a11f6b5e23aca3d1c3f9be4efd8cfb", + "reference": "e738ed3634a11f6b5e23aca3d1c3f9be4efd8cfb", + "shasum": "" + }, + "type": "library", + "time": "2019-12-08T08:35:40+00:00", + "autoload": { + "psr-4": { + "Munus\\": "src\/" + } + }, + "require": { + "php": "^7.2" + }, + "require-dev": { + "friendsofphp\/php-cs-fixer": "^2.15", + "phpunit\/phpunit": "^8.4", + "vimeo\/psalm": "^3.6", + "psalm\/plugin-phpunit": "^0.7.0", + "phpstan\/phpstan": "^0.11.19" + }, + "uid": 3386500, + "notification-url": "https:\/\/packagist.org\/downloads\/" + }, + "dev-master": { + "name": "buddy-works\/repman", + "description": "Power of object-oriented programming with the elegance of functional programming.", + "keywords": [], + "homepage": "", + "version": "dev-master", + "version_normalized": "9999999-dev", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Arkadiusz Kondas", + "email": "arkadiusz.kondas@gmail.com" + } + ], + "source": { + "type": "git", + "url": "https:\/\/github.com\/munusphp\/munus.git", + "reference": "e738ed3634a11f6b5e23aca3d1c3f9be4efd8cfb" + }, + "dist": { + "type": "zip", + "url": "https:\/\/api.github.com\/repos\/munusphp\/munus\/zipball\/e738ed3634a11f6b5e23aca3d1c3f9be4efd8cfb", + "reference": "e738ed3634a11f6b5e23aca3d1c3f9be4efd8cfb", + "shasum": "" + }, + "type": "library", + "time": "2019-12-08T08:35:40+00:00", + "autoload": { + "psr-4": { + "Munus\\": "src\/" + } + }, + "require": { + "php": "^7.2" + }, + "require-dev": { + "friendsofphp\/php-cs-fixer": "^2.15", + "phpunit\/phpunit": "^8.4", + "vimeo\/psalm": "^3.6", + "psalm\/plugin-phpunit": "^0.7.0", + "phpstan\/phpstan": "^0.11.19" + }, + "uid": 3386500, + "notification-url": "https:\/\/packagist.org\/downloads\/" + } + } + } +} diff --git a/tests/Resources/zipball/61e39aa8197cf1bc7fcb16a6f727b0c291bc9b76 b/tests/Resources/zipball/61e39aa8197cf1bc7fcb16a6f727b0c291bc9b76 new file mode 100644 index 00000000..411e74f6 Binary files /dev/null and b/tests/Resources/zipball/61e39aa8197cf1bc7fcb16a6f727b0c291bc9b76 differ diff --git a/tests/Unit/Service/Cache/FileCacheTest.php b/tests/Unit/Service/Cache/FileCacheTest.php deleted file mode 100644 index 485a484b..00000000 --- a/tests/Unit/Service/Cache/FileCacheTest.php +++ /dev/null @@ -1,127 +0,0 @@ -basePath = sys_get_temp_dir().'/repman'; - $this->exceptionHandler = new InMemoryExceptionHandler(); - $this->cache = new FileCache($this->basePath, $this->exceptionHandler); - $this->packagesPath = $this->basePath.'/packagist/packages.json'; - } - - protected function tearDown(): void - { - $filesystem = new Filesystem(); - $filesystem->remove($this->basePath); - } - - public function testThrowExceptionWhenCacheDirIsNotWritable(): void - { - $this->expectException(\InvalidArgumentException::class); - - new FileCache('/proc/new', new InMemoryExceptionHandler()); - } - - public function testCacheHitAndExists(): void - { - $content = '{"some":"json"}'; - @mkdir(dirname($this->packagesPath)); - file_put_contents($this->packagesPath, serialize($content)); - - self::assertTrue(Option::some($content)->equals( - $this->cache->get('packagist/packages.json', function (): void { - throw new \RuntimeException('This should not happen'); - }) - )); - self::assertTrue($this->cache->exists('packagist/packages.json')); - } - - public function testHandleExceptionOnFailure(): void - { - $exception = new \LogicException('something goes wrong'); - - $this->cache->get('some.file', function () use ($exception): void { - throw $exception; - }); - - self::assertTrue($this->exceptionHandler->exist($exception)); - } - - public function testCacheFind(): void - { - $file = '/p/buddy-works/repman$d1392374.json'; - @mkdir(dirname($this->basePath.$file), 0777, true); - file_put_contents($this->basePath.$file, 'a:1:{s:4:"some";s:4:"json";}'); - - self::assertTrue(Option::some(['some' => 'json'])->equals($this->cache->find('/p/buddy-works/repman'))); - self::assertTrue(Option::none()->equals($this->cache->find('/path/to/not-exist-dir'))); - self::assertTrue(Option::none()->equals($this->cache->find('/p/buddy-works/missing-package'))); - } - - public function testCacheFindExpire(): void - { - $file = '/p/buddy-works/repman$d1392374.json'; - @mkdir(dirname($this->basePath.$file), 0777, true); - file_put_contents($this->basePath.$file, 'a:1:{s:4:"some";s:4:"json";}'); - - self::assertTrue(Option::none()->equals($this->cache->find('/p/buddy-works/repman', -2))); - } - - public function testCacheHitExpire(): void - { - $cache = new FileCache(__DIR__.'/../../../Resources', new InMemoryExceptionHandler()); - - self::assertTrue(Option::none()->equals($cache->get('packages.json', function (): void { - // to prevent overwrite packages.json - throw new \LogicException(); - }, 60))); - } - - public function testCacheMiss(): void - { - $content = '{"some":"json"}'; - - self::assertTrue(!file_exists($this->packagesPath)); - self::assertTrue(Option::some($content)->equals( - $this->cache->get('packagist/packages.json', fn () => $content) - )); - self::assertTrue(file_exists($this->packagesPath)); - } - - public function testCacheRemoveByPattern(): void - { - $file = '/p/buddy-works/repman$d1392374.json'; - @mkdir(dirname($this->basePath.$file), 0777, true); - file_put_contents($this->basePath.$file, '{}'); - - $this->cache->removeOld($file); - self::assertTrue(!file_exists($this->basePath.$file)); - } - - public function testCacheNotRemoveWhenDollarSignIsMissing(): void - { - $file = '/p/buddy-works/repman.json'; - @mkdir(dirname($this->basePath.$file), 0777, true); - file_put_contents($this->basePath.$file, '{}'); - - $this->cache->removeOld($file); - self::assertTrue(file_exists($this->basePath.$file)); - @unlink($this->basePath.$file); - } -} diff --git a/tests/Unit/Service/Dist/Storage/FileStorageTest.php b/tests/Unit/Service/Dist/Storage/FileStorageTest.php index a167aacf..6876853f 100644 --- a/tests/Unit/Service/Dist/Storage/FileStorageTest.php +++ b/tests/Unit/Service/Dist/Storage/FileStorageTest.php @@ -34,7 +34,7 @@ public function testDownloadPackage(): void $packagePath = $this->basePath.'/packagist.org/dist/buddy-works/repman/0.1.2.0_f0c896.zip'; self::assertFileNotExists($packagePath); - $this->storage->download('https://some.domain/packagist.org/dist/buddy-works/repman/0.1.2.0_f0c896a759d4e2e1eff57978318e841911796305.zip', new Dist( + $this->storage->download('https://some.domain/packagist.org/dist/buddy-works/repman/f0c896a759d4e2e1eff57978318e841911796305.zip', new Dist( 'packagist.org', 'buddy-works/repman', '0.1.2.0', diff --git a/tests/Unit/Service/Proxy/MetadataProvider/CacheableMetadataProviderTest.php b/tests/Unit/Service/Proxy/MetadataProvider/CacheableMetadataProviderTest.php deleted file mode 100644 index 94cdb65a..00000000 --- a/tests/Unit/Service/Proxy/MetadataProvider/CacheableMetadataProviderTest.php +++ /dev/null @@ -1,61 +0,0 @@ -cache->exists(Argument::cetera())->willReturn(false); - - self::assertTrue(Option::none()->equals( - $this->provider->fromPath('buddy-works/repman', 'https://repman.buddy.works', 60) - )); - } - - public function testFromPathWithCache(): void - { - $metadata = ['metadata']; - $this->cache->exists(Argument::cetera())->willReturn(true); - $this->cache->find(Argument::type('string'), Proxy::PACKAGES_EXPIRE_TIME)->willReturn(Option::some($metadata)); - - self::assertTrue(Option::some($metadata)->equals( - $this->provider->fromPath('buddy-works/repman', 'https://repman.buddy.works', 60) - )); - } - - protected function setUp(): void - { - $this->cache = $this->prophesize(Cache::class); - $this->downloader = $this->prophesize(Downloader::class); - - /** @var Downloader $downloader */ - $downloader = $this->downloader->reveal(); - /** @var Cache $cache */ - $cache = $this->cache->reveal(); - - $this->provider = new CacheableMetadataProvider($downloader, $cache); - } -} diff --git a/tests/Unit/Service/ProxyTest.php b/tests/Unit/Service/ProxyTest.php index 9189ba63..aa6a1f5f 100644 --- a/tests/Unit/Service/ProxyTest.php +++ b/tests/Unit/Service/ProxyTest.php @@ -4,122 +4,42 @@ namespace Buddy\Repman\Tests\Unit\Service; -use Buddy\Repman\Service\Cache\InMemoryCache; -use Buddy\Repman\Service\Dist; -use Buddy\Repman\Service\Dist\Storage; -use Buddy\Repman\Service\Dist\Storage\InMemoryStorage; use Buddy\Repman\Service\Proxy; -use Buddy\Repman\Service\Proxy\MetadataProvider\CacheableMetadataProvider; use Buddy\Repman\Tests\Doubles\FakeDownloader; -use Buddy\Repman\Tests\Doubles\FakeMetadataProvider; -use Munus\Control\Option; +use League\Flysystem\Adapter\Local; +use League\Flysystem\Filesystem; use PHPUnit\Framework\TestCase; -use Prophecy\Argument; final class ProxyTest extends TestCase { - public function testPackageProvider(): void - { - $proxy = new Proxy('packagist.org', 'https://packagist.org', new CacheableMetadataProvider(new FakeDownloader(), new InMemoryCache()), new InMemoryStorage()); - $provider = $proxy->providerData('buddy-works/repman')->get(); - - self::assertEquals('0.1.0', $provider['packages']['buddy-works/repman']['0.1.0']['version']); - } - - public function testPackageProviderFromCache(): void - { - $cache = new InMemoryCache(); - $cache->get('packagist.org/packages.json', function (): array {return ['metadata']; }); - $cache->get('packagist.org/p/buddy-works/repman', function (): array {return ['package-metadata']; }); - $proxy = new Proxy('packagist.org', 'https://packagist.org', new CacheableMetadataProvider(new FakeDownloader(), $cache), new InMemoryStorage()); - - self::assertEquals(['package-metadata'], $proxy->providerData('buddy-works/repman')->get()); - } + private Proxy $proxy; - public function testPackageProviderV2FromCache(): void + protected function setUp(): void { - $cache = new InMemoryCache(); - $cache->get('packagist.org/packages.json', fn () => ['metadata-url' => '/p2/%package%.json']); - $cache->get('packagist.org/p2/buddy-works/repman', fn () => ['package-metadata']); - $proxy = new Proxy('packagist.org', 'https://packagist.org', new CacheableMetadataProvider(new FakeDownloader(), $cache), new InMemoryStorage()); - - self::assertEquals(['package-metadata'], $proxy->providerDataV2('buddy-works/repman')->get()); - } - - public function testPackageProviderV2NotFound(): void - { - $cache = new InMemoryCache(); - $cache->get('packagist.org/packages.json', function (): array {return []; }); - $proxy = new Proxy('packagist.org', 'https://packagist.org', new CacheableMetadataProvider(new FakeDownloader(), $cache), new InMemoryStorage()); - - self::assertTrue($proxy->providerDataV2('buddy-works/repman')->isEmpty()); - } - - public function testStorageDownloadDistWhenNotExists(): void - { - $distFilepath = __DIR__.'/../../Resources/packagist.org/dist/buddy-works/repman/0.1.2.0_f0c896a759d4e2e1eff57978318e841911796305.zip'; - /** @phpstan-var mixed $storage */ - $storage = $this->prophesize(Storage::class); - $storage->has(Argument::type(Dist::class))->willReturn(false); - $storage->filename(Argument::type(Dist::class))->willReturn($distFilepath); - $storage->download('https://api.github.com/repos/munusphp/munus/zipball/f0c896a759d4e2e1eff57978318e841911796305', Argument::type(Dist::class)) - ->shouldBeCalledOnce(); - - $proxy = new Proxy('packagist.org', 'https://packagist.org', new CacheableMetadataProvider(new FakeDownloader(), new InMemoryCache()), $storage->reveal()); - - self::assertStringContainsString( - '0.1.2.0_f0c896a759d4e2e1eff57978318e841911796305.zip', - $proxy->distFilename('buddy-works/repman', '0.1.2.0', 'f0c896a759d4e2e1eff57978318e841911796305', 'zip')->get() + $this->proxy = new Proxy( + 'packagist.org', + 'https://packagist.org', + new Filesystem(new Local(__DIR__.'/../../Resources')), + new FakeDownloader() ); } - public function testReturnNoneWhenDistPackageNotExists(): void - { - /** @phpstan-var mixed $storage */ - $storage = $this->prophesize(Storage::class); - $storage->has(Argument::type(Dist::class))->willReturn(false); - $storage->filename(Argument::type(Dist::class))->willReturn('/not/exist'); - $proxy = new Proxy('packagist.org', 'https://packagist.org', new CacheableMetadataProvider(new FakeDownloader(), new InMemoryCache()), $storage->reveal()); - - self::assertTrue(Option::none()->equals( - $proxy->distFilename('not-exist-vendor/not-exist-package', '0.1.2.0', 'f0c896a759d4e2e1eff57978318e841911796305', 'zip') - )); - } - - public function testStorageNotForceToDownloadWhenDistExists(): void + public function testPackageMetadata(): void { - /** @phpstan-var mixed $storage */ - $storage = $this->prophesize(Storage::class); - $storage->has(Argument::type(Dist::class))->willReturn(true); - $storage->download(Argument::cetera())->shouldNotBeCalled(); - $storage->filename(Argument::type(Dist::class))->willReturn( - __DIR__.'/../../Resources/packagist.org/dist/buddy-works/repman/0.1.2.0_f0c896a759d4e2e1eff57978318e841911796305.zip' - ); + $metadata = $this->proxy->metadata('buddy-works/repman'); - $proxy = new Proxy('packagist.org', 'https://packagist.org', new FakeMetadataProvider(), $storage->reveal()); - - self::assertStringContainsString( - '0.1.2.0_f0c896a759d4e2e1eff57978318e841911796305.zip', - $proxy->distFilename('buddy-works/repman', '0.1.2.0', 'f0c896a759d4e2e1eff57978318e841911796305', 'zip')->get() - ); + self::assertTrue($metadata->isPresent()); } - public function testStorageHandleDistWithSlashInVersion(): void + public function testDownloadDistWhenNotExists(): void { - $distFilepath = __DIR__.'/../../Resources/packagist.org/dist/buddy-works/repman/0cdaa0ab95de9fcf94ad9b1d2f80e15d_e738ed3634a11f6b5e23aca3d1c3f9be4efd8cfb.zip'; - /** @phpstan-var mixed $storage */ - $storage = $this->prophesize(Storage::class); - $storage->has(Argument::type(Dist::class))->willReturn(false); - $storage->filename(Argument::type(Dist::class))->willReturn($distFilepath); - $storage->download('https://api.github.com/repos/munusphp/munus/zipball/e738ed3634a11f6b5e23aca3d1c3f9be4efd8cfb', Argument::type(Dist::class)) - ->shouldBeCalledOnce(); + $distPath = __DIR__.'/../../Resources/packagist.org/dist/buddy-works/repman/61e39aa8197cf1bc7fcb16a6f727b0c291bc9b76.zip'; - $proxy = new Proxy('packagist.org', 'https://packagist.org', new CacheableMetadataProvider(new FakeDownloader(), new InMemoryCache()), $storage->reveal()); + self::assertFileNotExists($distPath); + $distribution = $this->proxy->distribution('buddy-works/repman', '1.2.3', '61e39aa8197cf1bc7fcb16a6f727b0c291bc9b76', 'zip'); + self::assertTrue($distribution->isPresent()); - self::assertStringContainsString( - '0cdaa0ab95de9fcf94ad9b1d2f80e15d_e738ed3634a11f6b5e23aca3d1c3f9be4efd8cfb.zip', - $proxy->distFilename('buddy-works/repman', 'dev-feature/awesome', 'e738ed3634a11f6b5e23aca3d1c3f9be4efd8cfb', 'zip')->get() - ); - self::assertEquals('0cdaa0ab95de9fcf94ad9b1d2f80e15d', (new Dist('repo', 'package', 'dev-feature/awesome', 'ref', 'format'))->version()); + fclose($distribution->get()); + unlink($distPath); } }