s3 storage

This commit is contained in:
Yuri Kuznetsov
2021-03-02 23:13:57 +02:00
parent 9b18ecf072
commit b729ec1a85
15 changed files with 695 additions and 41 deletions

View File

@@ -87,7 +87,7 @@ class Attachment extends Record
$response
->setHeader('Content-Type', $fileData->type)
->setHeader('Content-Disposition', 'attachment; filename="' . $fileData->name . '"')
->setHeader('Content-Length', $fileData->stream->getSize())
->setHeader('Content-Length', (string) $fileData->size)
->setBody($fileData->stream);
}
}

View File

@@ -61,6 +61,16 @@ class Manager
return $implementation->exists(self::wrapAttachmentEntity($attachment));
}
/**
* Get a file size.
*/
public function getSize(AttachmentEntity $attachment) : int
{
$implementation = $this->getImplementation($attachment);
return $implementation->getSize(self::wrapAttachmentEntity($attachment));
}
/**
* Get file contents.
*/

View File

@@ -55,4 +55,9 @@ interface Storage
* Delete a file.
*/
public function unlink(Attachment $attachment) : void;
/**
* Get a file size.
*/
public function getSize(Attachment $attachment) : int;
}

View File

@@ -0,0 +1,120 @@
<?php
/************************************************************************
* This file is part of EspoCRM.
*
* EspoCRM - Open Source CRM application.
* Copyright (C) 2014-2021 Yurii Kuznietsov, Taras Machyshyn, Oleksii Avramenko
* Website: https://www.espocrm.com
*
* EspoCRM is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* EspoCRM is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with EspoCRM. If not, see http://www.gnu.org/licenses/.
*
* The interactive user interfaces in modified source and object code versions
* of this program must display Appropriate Legal Notices, as required under
* Section 5 of the GNU General Public License version 3.
*
* In accordance with Section 7(b) of the GNU General Public License version 3,
* these Appropriate Legal Notices must retain the display of the "EspoCRM" word.
************************************************************************/
namespace Espo\Core\FileStorage\Storages;
use Espo\Core\{
Utils\Config,
FileStorage\Storage,
FileStorage\Attachment,
};
use Psr\Http\Message\StreamInterface;
use Aws\S3\S3Client;
use League\Flysystem\{
AwsS3V3\AwsS3V3Adapter,
Filesystem,
};
use GuzzleHttp\Psr7\Stream;
use RuntimeException;
class AwsS3 implements Storage
{
protected $filesystem;
public function __construct(Config $config)
{
$bucketName = $config->get('awsS3Storage.bucketName') ?? null;
$path = $config->get('awsS3Storage.path') ?? null;
$region = $config->get('awsS3Storage.region') ?? null;
$version = $config->get('awsS3Storage.version') ?? null;
$credentials = $config->get('awsS3Storage.credentials') ?? null;
if (!$bucketName) {
throw new RuntimeException("AWS S3 bucket name is not specified in config.");
}
$clientOptions = [
'region' => $region,
'version' => $version,
];
if ($credentials) {
$clientOptions['credentials'] = $credentials;
}
$client = new S3Client($clientOptions);
$adapter = new AwsS3V3Adapter($client, $bucketName, $path);
$this->filesystem = new Filesystem($adapter);
}
public function unlink(Attachment $attachment) : void
{
$this->filesystem->delete($attachment->getSourceId());
}
public function exists(Attachment $attachment) : bool
{
return $this->filesystem->fileExists($attachment->getSourceId());
}
public function getSize(Attachment $attachment) : int
{
return $this->filesystem->fileSize($attachment->getSourceId());
}
public function getStream(Attachment $attachment) : StreamInterface
{
$resource = $this->filesystem->readStream($attachment->getSourceId());
return new Stream($resource);
}
public function putStream(Attachment $attachment, StreamInterface $stream) : void
{
// League\Flysystem does not support StreamInterface.
// Need to pass a resource.
$resource = fopen('php://memory', 'r+');
fwrite($resource, $stream->getContents());
rewind($resource);
$this->filesystem->writeStream($attachment->getSourceId(), $resource);
fclose($resource);
}
}

View File

@@ -64,12 +64,23 @@ class EspoUploadDir implements Storage, Local
return $this->fileManager->isFile($filePath);
}
public function getSize(Attachment $attachment) : int
{
$filePath = $this->getFilePath($attachment);
if (!$this->exists($attachment)) {
throw new Error("Could not get size for non-existing file '{$filePath}'.");
}
return filesize($filePath);
}
public function getStream(Attachment $attachment) : StreamInterface
{
$filePath = $this->getFilePath($attachment);
if (!$this->exists($attachment)) {
throw new Error("Could not get stream for the file '{$filePath}'.");
throw new Error("Could not get stream for non-existing '{$filePath}'.");
}
$resouce = fopen($filePath, 'r');

View File

@@ -97,9 +97,11 @@ class Attachment implements EntryPoint
$stream = $this->fileStorageManager->getStream($attachment);
$size = $stream->getSize() ?? $this->fileStorageManager->getSize($attachment);
$response
->setHeader('Pragma', 'public')
->setHeader('Content-Length', (string) $stream->getSize())
->setHeader('Content-Length', (string) $size)
->setBody($stream);
}
}

View File

@@ -107,12 +107,14 @@ class Download implements EntryPoint
$response->setHeader('Content-Type', $type);
}
$size = $stream->getSize() ?? $this->fileStorageManager->getSize($attachment);
$response
->setHeader('Content-Disposition', $disposition . ";filename=\"" . $outputFileName . "\"")
->setHeader('Expires', '0')
->setHeader('Cache-Control', 'must-revalidate')
->setHeader('Pragma', 'public')
->setHeader('Content-Length', $stream->getSize())
->setHeader('Content-Length', (string) $size)
->setBody($stream);
}
}

View File

@@ -199,7 +199,7 @@ class Image implements EntryPoint,
$stream = $this->fileStorageManager->getStream($attachment);
$fileSize = $stream->getSize();
$fileSize = $stream->getSize() ?? $this->fileStorageManager->getSize($attachment);
$response->setBody($stream);
}
@@ -212,7 +212,7 @@ class Image implements EntryPoint,
->setHeader('Content-Disposition', 'inline;filename="' . $fileName . '"')
->setHeader('Pragma', 'public')
->setHeader('Cache-Control', 'max-age=360000, must-revalidate')
->setHeader('Content-Length', $fileSize);
->setHeader('Content-Length', (string) $fileSize);
}
protected function getThumbImage($filePath, $fileType, $size)

View File

@@ -66,47 +66,34 @@ class Attachment extends \Espo\Core\Repositories\Database implements
'xx-large',
];
public function beforeSave(Entity $entity, array $options = [])
protected function beforeSave(Entity $entity, array $options = [])
{
parent::beforeSave($entity, $options);
$storage = $entity->get('storage');
if (!$storage) {
$entity->set('storage', $this->config->get('defaultFileStorage', null));
}
if ($entity->isNew()) {
if (!$entity->has('size') && $entity->has('contents')) {
$contents = $entity->get('contents');
$entity->set(
'size',
mb_strlen($contents, '8bit')
);
}
$this->processBeforeSaveNew($entity);
}
}
public function save(Entity $entity, array $options = [])
protected function processBeforeSaveNew(Entity $entity) : void
{
$isNew = $entity->isNew();
if (!$entity->get('storage')) {
$defaultStorage = $this->config->get('defaultFileStorage');
if ($isNew) {
$entity->id = Util::generateId();
if ($entity->has('contents')) {
$contents = $entity->get('contents');
if (is_string($contents)) {
$this->fileStorageManager->putContents($entity, $contents);
}
}
$entity->set('storage', $defaultStorage);
}
$result = parent::save($entity, $options);
$entity->id = Util::generateId();
return $result;
$contents = $entity->get('contents');
if (is_null($contents)) {
return;
}
$entity->set('size', strlen($contents));
$this->fileStorageManager->putContents($entity, $contents);
}
protected function afterRemove(Entity $entity, array $options = [])
@@ -175,6 +162,11 @@ class Attachment extends \Espo\Core\Repositories\Database implements
return $this->fileStorageManager->getStream($entity);
}
public function getSize(AttachmentEntity $entity) : int
{
return $this->fileStorageManager->getSize($entity);
}
public function getFilePath(AttachmentEntity $entity) : string
{
return $this->fileStorageManager->getLocalFilePath($entity);

View File

@@ -157,5 +157,6 @@ return [
'personNameFormat' => 'firstLast',
'newNotificationCountInTitle' => false,
'pdfEngine' => 'Tcpdf',
'defaultFileStorage' => 'EspoUploadDir',
'isInstalled' => false,
];

View File

@@ -1,5 +1,6 @@
{
"implementationClassNameMap": {
"EspoUploadDir": "Espo\\Core\\FileStorage\\Storages\\EspoUploadDir"
"EspoUploadDir": "Espo\\Core\\FileStorage\\Storages\\EspoUploadDir",
"AwsS3": "Espo\\Core\\FileStorage\\Storages\\AwsS3"
}
}

View File

@@ -635,6 +635,10 @@
"useWebSocket": {
"type": "bool",
"tooltip": true
},
"awsS3Storage": {
"type": "jsonObject",
"onlySystem": true
}
}
}

View File

@@ -448,7 +448,7 @@ class Attachment extends Record
'name' => $attachment->get('name'),
'type' => $attachment->get('type'),
'stream' => $this->getRepository()->getStream($attachment),
'size' => $attachment->get('size'),
'size' => $this->getRepository()->getSize($attachment),
];
return $data;

View File

@@ -40,7 +40,9 @@
"nesbot/carbon": "^2.26",
"zbateson/mail-mime-parser": "1.3.*",
"phpoffice/phpspreadsheet": "^1.16",
"doctrine/dbal": "^3.0"
"doctrine/dbal": "^3.0",
"league/flysystem": "^2.0",
"league/flysystem-aws-s3-v3": "^2.0"
},
"require-dev": {
"phpunit/phpunit": "^9"

510
composer.lock generated
View File

@@ -1,11 +1,101 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "dc02d5c59ae017fcfae1bb229746e0c2",
"content-hash": "2be325991598c4526c7d1350a463c397",
"packages": [
{
"name": "aws/aws-sdk-php",
"version": "3.173.19",
"source": {
"type": "git",
"url": "https://github.com/aws/aws-sdk-php.git",
"reference": "63c6feca49bf4083f33bf250401c0c286759e101"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/63c6feca49bf4083f33bf250401c0c286759e101",
"reference": "63c6feca49bf4083f33bf250401c0c286759e101",
"shasum": ""
},
"require": {
"ext-json": "*",
"ext-pcre": "*",
"ext-simplexml": "*",
"guzzlehttp/guzzle": "^5.3.3|^6.2.1|^7.0",
"guzzlehttp/promises": "^1.0",
"guzzlehttp/psr7": "^1.4.1",
"mtdowling/jmespath.php": "^2.5",
"php": ">=5.5"
},
"require-dev": {
"andrewsville/php-token-reflection": "^1.4",
"aws/aws-php-sns-message-validator": "~1.0",
"behat/behat": "~3.0",
"doctrine/cache": "~1.4",
"ext-dom": "*",
"ext-openssl": "*",
"ext-pcntl": "*",
"ext-sockets": "*",
"nette/neon": "^2.3",
"paragonie/random_compat": ">= 2",
"phpunit/phpunit": "^4.8.35|^5.4.3",
"psr/cache": "^1.0",
"psr/simple-cache": "^1.0",
"sebastian/comparator": "^1.2.3"
},
"suggest": {
"aws/aws-php-sns-message-validator": "To validate incoming SNS notifications",
"doctrine/cache": "To use the DoctrineCacheAdapter",
"ext-curl": "To send requests using cURL",
"ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages",
"ext-sockets": "To use client-side monitoring"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "3.0-dev"
}
},
"autoload": {
"psr-4": {
"Aws\\": "src/"
},
"files": [
"src/functions.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"Apache-2.0"
],
"authors": [
{
"name": "Amazon Web Services",
"homepage": "http://aws.amazon.com"
}
],
"description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project",
"homepage": "http://aws.amazon.com/sdkforphp",
"keywords": [
"amazon",
"aws",
"cloud",
"dynamodb",
"ec2",
"glacier",
"s3",
"sdk"
],
"support": {
"forum": "https://forums.aws.amazon.com/forum.jspa?forumID=80",
"issues": "https://github.com/aws/aws-sdk-php/issues",
"source": "https://github.com/aws/aws-sdk-php/tree/3.173.19"
},
"time": "2021-03-01T19:15:59+00:00"
},
{
"name": "cboden/ratchet",
"version": "v0.4.1",
@@ -652,6 +742,163 @@
],
"time": "2020-02-05T20:36:27+00:00"
},
{
"name": "guzzlehttp/guzzle",
"version": "7.1.1",
"source": {
"type": "git",
"url": "https://github.com/guzzle/guzzle.git",
"reference": "7427d6f99df41cc01f33cd59832f721c150ffdf3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/guzzle/zipball/7427d6f99df41cc01f33cd59832f721c150ffdf3",
"reference": "7427d6f99df41cc01f33cd59832f721c150ffdf3",
"shasum": ""
},
"require": {
"ext-json": "*",
"guzzlehttp/promises": "^1.0",
"guzzlehttp/psr7": "^1.6.1",
"php": "^7.2.5",
"psr/http-client": "^1.0"
},
"provide": {
"psr/http-client-implementation": "1.0"
},
"require-dev": {
"ext-curl": "*",
"php-http/client-integration-tests": "dev-phpunit8",
"phpunit/phpunit": "^8.5.5",
"psr/log": "^1.1"
},
"suggest": {
"ext-curl": "Required for CURL handler support",
"ext-intl": "Required for Internationalized Domain Name (IDN) support",
"psr/log": "Required for using the Log middleware"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "7.1-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\": "src/"
},
"files": [
"src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
},
{
"name": "Márk Sági-Kazár",
"email": "mark.sagikazar@gmail.com",
"homepage": "https://sagikazarmark.hu"
}
],
"description": "Guzzle is a PHP HTTP client library",
"homepage": "http://guzzlephp.org/",
"keywords": [
"client",
"curl",
"framework",
"http",
"http client",
"psr-18",
"psr-7",
"rest",
"web service"
],
"support": {
"issues": "https://github.com/guzzle/guzzle/issues",
"source": "https://github.com/guzzle/guzzle/tree/7.1.1"
},
"funding": [
{
"url": "https://github.com/GrahamCampbell",
"type": "github"
},
{
"url": "https://github.com/Nyholm",
"type": "github"
},
{
"url": "https://github.com/alexeyshockov",
"type": "github"
},
{
"url": "https://github.com/gmponos",
"type": "github"
}
],
"time": "2020-09-30T08:51:17+00:00"
},
{
"name": "guzzlehttp/promises",
"version": "1.4.0",
"source": {
"type": "git",
"url": "https://github.com/guzzle/promises.git",
"reference": "60d379c243457e073cff02bc323a2a86cb355631"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/guzzle/promises/zipball/60d379c243457e073cff02bc323a2a86cb355631",
"reference": "60d379c243457e073cff02bc323a2a86cb355631",
"shasum": ""
},
"require": {
"php": ">=5.5"
},
"require-dev": {
"symfony/phpunit-bridge": "^4.4 || ^5.1"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.4-dev"
}
},
"autoload": {
"psr-4": {
"GuzzleHttp\\Promise\\": "src/"
},
"files": [
"src/functions_include.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Guzzle promises library",
"keywords": [
"promise"
],
"support": {
"issues": "https://github.com/guzzle/promises/issues",
"source": "https://github.com/guzzle/promises/tree/1.4.0"
},
"time": "2020-09-30T07:37:28+00:00"
},
{
"name": "guzzlehttp/psr7",
"version": "1.6.1",
@@ -1198,6 +1445,201 @@
],
"time": "2020-09-14T14:23:00+00:00"
},
{
"name": "league/flysystem",
"version": "2.0.4",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem.git",
"reference": "7cbbb7222e8d8a34e71273d243dfcf383ed6779f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem/zipball/7cbbb7222e8d8a34e71273d243dfcf383ed6779f",
"reference": "7cbbb7222e8d8a34e71273d243dfcf383ed6779f",
"shasum": ""
},
"require": {
"ext-json": "*",
"league/mime-type-detection": "^1.0.0",
"php": "^7.2 || ^8.0"
},
"conflict": {
"guzzlehttp/ringphp": "<1.1.1"
},
"require-dev": {
"async-aws/s3": "^1.5",
"async-aws/simple-s3": "^1.0",
"aws/aws-sdk-php": "^3.132.4",
"composer/semver": "^3.0",
"ext-fileinfo": "*",
"friendsofphp/php-cs-fixer": "^2.16",
"google/cloud-storage": "^1.23",
"phpseclib/phpseclib": "^2.0",
"phpstan/phpstan": "^0.12.26",
"phpunit/phpunit": "^8.5 || ^9.4"
},
"type": "library",
"autoload": {
"psr-4": {
"League\\Flysystem\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Frank de Jonge",
"email": "info@frankdejonge.nl"
}
],
"description": "File storage abstraction for PHP",
"keywords": [
"WebDAV",
"aws",
"cloud",
"file",
"files",
"filesystem",
"filesystems",
"ftp",
"s3",
"sftp",
"storage"
],
"support": {
"issues": "https://github.com/thephpleague/flysystem/issues",
"source": "https://github.com/thephpleague/flysystem/tree/2.0.4"
},
"funding": [
{
"url": "https://offset.earth/frankdejonge",
"type": "custom"
},
{
"url": "https://github.com/frankdejonge",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/league/flysystem",
"type": "tidelift"
}
],
"time": "2021-02-12T19:37:50+00:00"
},
{
"name": "league/flysystem-aws-s3-v3",
"version": "2.0.4",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/flysystem-aws-s3-v3.git",
"reference": "c89931e9c4b294493234798564cc814ef478fbc6"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/flysystem-aws-s3-v3/zipball/c89931e9c4b294493234798564cc814ef478fbc6",
"reference": "c89931e9c4b294493234798564cc814ef478fbc6",
"shasum": ""
},
"require": {
"aws/aws-sdk-php": "^3.132.4",
"league/flysystem": "^2.0.0",
"league/mime-type-detection": "^1.0.0",
"php": "^7.2 || ^8.0"
},
"conflict": {
"guzzlehttp/ringphp": "<1.1.1"
},
"type": "library",
"autoload": {
"psr-4": {
"League\\Flysystem\\AwsS3V3\\": ""
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Frank de Jonge",
"email": "info@frankdejonge.nl"
}
],
"description": "AWS S3 filesystem adapter for Flysystem.",
"keywords": [
"Flysystem",
"aws",
"file",
"files",
"filesystem",
"s3",
"storage"
],
"support": {
"issues": "https://github.com/thephpleague/flysystem-aws-s3-v3/issues",
"source": "https://github.com/thephpleague/flysystem-aws-s3-v3/tree/2.0.4"
},
"time": "2021-02-09T21:10:56+00:00"
},
{
"name": "league/mime-type-detection",
"version": "1.7.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/mime-type-detection.git",
"reference": "3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/mime-type-detection/zipball/3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3",
"reference": "3b9dff8aaf7323590c1d2e443db701eb1f9aa0d3",
"shasum": ""
},
"require": {
"ext-fileinfo": "*",
"php": "^7.2 || ^8.0"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.18",
"phpstan/phpstan": "^0.12.68",
"phpunit/phpunit": "^8.5.8 || ^9.3"
},
"type": "library",
"autoload": {
"psr-4": {
"League\\MimeTypeDetection\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Frank de Jonge",
"email": "info@frankdejonge.nl"
}
],
"description": "Mime-type detection for Flysystem",
"support": {
"issues": "https://github.com/thephpleague/mime-type-detection/issues",
"source": "https://github.com/thephpleague/mime-type-detection/tree/1.7.0"
},
"funding": [
{
"url": "https://github.com/frankdejonge",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/league/flysystem",
"type": "tidelift"
}
],
"time": "2021-01-18T20:58:21+00:00"
},
{
"name": "maennchen/zipstream-php",
"version": "2.1.0",
@@ -1551,6 +1993,67 @@
],
"time": "2020-07-23T08:41:23+00:00"
},
{
"name": "mtdowling/jmespath.php",
"version": "2.6.0",
"source": {
"type": "git",
"url": "https://github.com/jmespath/jmespath.php.git",
"reference": "42dae2cbd13154083ca6d70099692fef8ca84bfb"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/jmespath/jmespath.php/zipball/42dae2cbd13154083ca6d70099692fef8ca84bfb",
"reference": "42dae2cbd13154083ca6d70099692fef8ca84bfb",
"shasum": ""
},
"require": {
"php": "^5.4 || ^7.0 || ^8.0",
"symfony/polyfill-mbstring": "^1.17"
},
"require-dev": {
"composer/xdebug-handler": "^1.4",
"phpunit/phpunit": "^4.8.36 || ^7.5.15"
},
"bin": [
"bin/jp.php"
],
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "2.6-dev"
}
},
"autoload": {
"psr-4": {
"JmesPath\\": "src/"
},
"files": [
"src/JmesPath.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Michael Dowling",
"email": "mtdowling@gmail.com",
"homepage": "https://github.com/mtdowling"
}
],
"description": "Declaratively specify how to extract elements from a JSON document",
"keywords": [
"json",
"jsonpath"
],
"support": {
"issues": "https://github.com/jmespath/jmespath.php/issues",
"source": "https://github.com/jmespath/jmespath.php/tree/2.6.0"
},
"time": "2020-07-31T21:01:56+00:00"
},
{
"name": "myclabs/php-enum",
"version": "1.7.7",
@@ -5784,5 +6287,6 @@
"ext-curl": "*",
"ext-exif": "*"
},
"platform-dev": []
"platform-dev": [],
"plugin-api-version": "2.0.0"
}