Subiendo proyecto completo sin restricciones de git ignore

This commit is contained in:
Jose Sanchez
2023-08-17 11:44:02 -04:00
parent a0d4f5ba3b
commit 20f1c60600
19921 changed files with 2509159 additions and 45 deletions

View File

@@ -0,0 +1,508 @@
<?php
declare(strict_types=1);
namespace League\Flysystem\AwsS3V3;
use Aws\Api\DateTimeResult;
use Aws\S3\S3ClientInterface;
use DateTimeInterface;
use Generator;
use League\Flysystem\ChecksumAlgoIsNotSupported;
use League\Flysystem\ChecksumProvider;
use League\Flysystem\Config;
use League\Flysystem\DirectoryAttributes;
use League\Flysystem\FileAttributes;
use League\Flysystem\FilesystemAdapter;
use League\Flysystem\FilesystemOperationFailed;
use League\Flysystem\PathPrefixer;
use League\Flysystem\StorageAttributes;
use League\Flysystem\UnableToCheckDirectoryExistence;
use League\Flysystem\UnableToCheckFileExistence;
use League\Flysystem\UnableToCopyFile;
use League\Flysystem\UnableToDeleteDirectory;
use League\Flysystem\UnableToDeleteFile;
use League\Flysystem\UnableToGeneratePublicUrl;
use League\Flysystem\UnableToGenerateTemporaryUrl;
use League\Flysystem\UnableToMoveFile;
use League\Flysystem\UnableToProvideChecksum;
use League\Flysystem\UnableToReadFile;
use League\Flysystem\UnableToRetrieveMetadata;
use League\Flysystem\UnableToSetVisibility;
use League\Flysystem\UnableToWriteFile;
use League\Flysystem\UrlGeneration\PublicUrlGenerator;
use League\Flysystem\UrlGeneration\TemporaryUrlGenerator;
use League\Flysystem\Visibility;
use League\MimeTypeDetection\FinfoMimeTypeDetector;
use League\MimeTypeDetection\MimeTypeDetector;
use Psr\Http\Message\StreamInterface;
use Throwable;
use function trim;
class AwsS3V3Adapter implements FilesystemAdapter, PublicUrlGenerator, ChecksumProvider, TemporaryUrlGenerator
{
/**
* @var string[]
*/
public const AVAILABLE_OPTIONS = [
'ACL',
'CacheControl',
'ContentDisposition',
'ContentEncoding',
'ContentLength',
'ContentType',
'ContentMD5',
'Expires',
'GrantFullControl',
'GrantRead',
'GrantReadACP',
'GrantWriteACP',
'Metadata',
'MetadataDirective',
'RequestPayer',
'SSECustomerAlgorithm',
'SSECustomerKey',
'SSECustomerKeyMD5',
'SSEKMSKeyId',
'ServerSideEncryption',
'StorageClass',
'Tagging',
'WebsiteRedirectLocation',
'ChecksumAlgorithm',
];
/**
* @var string[]
*/
public const MUP_AVAILABLE_OPTIONS = [
'before_upload',
'concurrency',
'mup_threshold',
'params',
'part_size',
];
/**
* @var string[]
*/
private const EXTRA_METADATA_FIELDS = [
'Metadata',
'StorageClass',
'ETag',
'VersionId',
];
private PathPrefixer $prefixer;
private VisibilityConverter $visibility;
private MimeTypeDetector $mimeTypeDetector;
public function __construct(
private S3ClientInterface $client,
private string $bucket,
string $prefix = '',
VisibilityConverter $visibility = null,
MimeTypeDetector $mimeTypeDetector = null,
private array $options = [],
private bool $streamReads = true,
private array $forwardedOptions = self::AVAILABLE_OPTIONS,
private array $metadataFields = self::EXTRA_METADATA_FIELDS,
private array $multipartUploadOptions = self::MUP_AVAILABLE_OPTIONS,
) {
$this->prefixer = new PathPrefixer($prefix);
$this->visibility = $visibility ?: new PortableVisibilityConverter();
$this->mimeTypeDetector = $mimeTypeDetector ?: new FinfoMimeTypeDetector();
}
public function fileExists(string $path): bool
{
try {
return $this->client->doesObjectExistV2($this->bucket, $this->prefixer->prefixPath($path), false, $this->options);
} catch (Throwable $exception) {
throw UnableToCheckFileExistence::forLocation($path, $exception);
}
}
public function directoryExists(string $path): bool
{
try {
$prefix = $this->prefixer->prefixDirectoryPath($path);
$options = ['Bucket' => $this->bucket, 'Prefix' => $prefix, 'MaxKeys' => 1, 'Delimiter' => '/'];
$command = $this->client->getCommand('ListObjectsV2', $options);
$result = $this->client->execute($command);
return $result->hasKey('Contents') || $result->hasKey('CommonPrefixes');
} catch (Throwable $exception) {
throw UnableToCheckDirectoryExistence::forLocation($path, $exception);
}
}
public function write(string $path, string $contents, Config $config): void
{
$this->upload($path, $contents, $config);
}
/**
* @param string $path
* @param string|resource $body
* @param Config $config
*/
private function upload(string $path, $body, Config $config): void
{
$key = $this->prefixer->prefixPath($path);
$options = $this->createOptionsFromConfig($config);
$acl = $options['params']['ACL'] ?? $this->determineAcl($config);
$shouldDetermineMimetype = ! array_key_exists('ContentType', $options['params']);
if ($shouldDetermineMimetype && $mimeType = $this->mimeTypeDetector->detectMimeType($key, $body)) {
$options['params']['ContentType'] = $mimeType;
}
try {
$this->client->upload($this->bucket, $key, $body, $acl, $options);
} catch (Throwable $exception) {
throw UnableToWriteFile::atLocation($path, $exception->getMessage(), $exception);
}
}
private function determineAcl(Config $config): string
{
$visibility = (string) $config->get(Config::OPTION_VISIBILITY, Visibility::PRIVATE);
return $this->visibility->visibilityToAcl($visibility);
}
private function createOptionsFromConfig(Config $config): array
{
$config = $config->withDefaults($this->options);
$options = ['params' => []];
if ($mimetype = $config->get('mimetype')) {
$options['params']['ContentType'] = $mimetype;
}
foreach ($this->forwardedOptions as $option) {
$value = $config->get($option, '__NOT_SET__');
if ($value !== '__NOT_SET__') {
$options['params'][$option] = $value;
}
}
foreach ($this->multipartUploadOptions as $option) {
$value = $config->get($option, '__NOT_SET__');
if ($value !== '__NOT_SET__') {
$options[$option] = $value;
}
}
return $options;
}
public function writeStream(string $path, $contents, Config $config): void
{
$this->upload($path, $contents, $config);
}
public function read(string $path): string
{
$body = $this->readObject($path, false);
return (string) $body->getContents();
}
public function readStream(string $path)
{
/** @var resource $resource */
$resource = $this->readObject($path, true)->detach();
return $resource;
}
public function delete(string $path): void
{
$arguments = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)];
$command = $this->client->getCommand('DeleteObject', $arguments);
try {
$this->client->execute($command);
} catch (Throwable $exception) {
throw UnableToDeleteFile::atLocation($path, '', $exception);
}
}
public function deleteDirectory(string $path): void
{
$prefix = $this->prefixer->prefixPath($path);
$prefix = ltrim(rtrim($prefix, '/') . '/', '/');
try {
$this->client->deleteMatchingObjects($this->bucket, $prefix);
} catch (Throwable $exception) {
throw UnableToDeleteDirectory::atLocation($path, '', $exception);
}
}
public function createDirectory(string $path, Config $config): void
{
$defaultVisibility = $config->get('directory_visibility', $this->visibility->defaultForDirectories());
$config = $config->withDefaults(['visibility' => $defaultVisibility]);
$this->upload(rtrim($path, '/') . '/', '', $config);
}
public function setVisibility(string $path, string $visibility): void
{
$arguments = [
'Bucket' => $this->bucket,
'Key' => $this->prefixer->prefixPath($path),
'ACL' => $this->visibility->visibilityToAcl($visibility),
];
$command = $this->client->getCommand('PutObjectAcl', $arguments);
try {
$this->client->execute($command);
} catch (Throwable $exception) {
throw UnableToSetVisibility::atLocation($path, '', $exception);
}
}
public function visibility(string $path): FileAttributes
{
$arguments = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)];
$command = $this->client->getCommand('GetObjectAcl', $arguments);
try {
$result = $this->client->execute($command);
} catch (Throwable $exception) {
throw UnableToRetrieveMetadata::visibility($path, '', $exception);
}
$visibility = $this->visibility->aclToVisibility((array) $result->get('Grants'));
return new FileAttributes($path, null, $visibility);
}
private function fetchFileMetadata(string $path, string $type): FileAttributes
{
$arguments = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)];
$command = $this->client->getCommand('HeadObject', $arguments);
try {
$result = $this->client->execute($command);
} catch (Throwable $exception) {
throw UnableToRetrieveMetadata::create($path, $type, '', $exception);
}
$attributes = $this->mapS3ObjectMetadata($result->toArray(), $path);
if ( ! $attributes instanceof FileAttributes) {
throw UnableToRetrieveMetadata::create($path, $type, '');
}
return $attributes;
}
private function mapS3ObjectMetadata(array $metadata, string $path): StorageAttributes
{
if (substr($path, -1) === '/') {
return new DirectoryAttributes(rtrim($path, '/'));
}
$mimetype = $metadata['ContentType'] ?? null;
$fileSize = $metadata['ContentLength'] ?? $metadata['Size'] ?? null;
$fileSize = $fileSize === null ? null : (int) $fileSize;
$dateTime = $metadata['LastModified'] ?? null;
$lastModified = $dateTime instanceof DateTimeResult ? $dateTime->getTimeStamp() : null;
return new FileAttributes(
$path,
$fileSize,
null,
$lastModified,
$mimetype,
$this->extractExtraMetadata($metadata)
);
}
private function extractExtraMetadata(array $metadata): array
{
$extracted = [];
foreach ($this->metadataFields as $field) {
if (isset($metadata[$field]) && $metadata[$field] !== '') {
$extracted[$field] = $metadata[$field];
}
}
return $extracted;
}
public function mimeType(string $path): FileAttributes
{
$attributes = $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_MIME_TYPE);
if ($attributes->mimeType() === null) {
throw UnableToRetrieveMetadata::mimeType($path);
}
return $attributes;
}
public function lastModified(string $path): FileAttributes
{
$attributes = $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_LAST_MODIFIED);
if ($attributes->lastModified() === null) {
throw UnableToRetrieveMetadata::lastModified($path);
}
return $attributes;
}
public function fileSize(string $path): FileAttributes
{
$attributes = $this->fetchFileMetadata($path, FileAttributes::ATTRIBUTE_FILE_SIZE);
if ($attributes->fileSize() === null) {
throw UnableToRetrieveMetadata::fileSize($path);
}
return $attributes;
}
public function listContents(string $path, bool $deep): iterable
{
$prefix = trim($this->prefixer->prefixPath($path), '/');
$prefix = empty($prefix) ? '' : $prefix . '/';
$options = ['Bucket' => $this->bucket, 'Prefix' => $prefix];
if ($deep === false) {
$options['Delimiter'] = '/';
}
$listing = $this->retrievePaginatedListing($options);
foreach ($listing as $item) {
$key = $item['Key'] ?? $item['Prefix'];
if ($key === $prefix) {
continue;
}
yield $this->mapS3ObjectMetadata($item, $this->prefixer->stripPrefix($key));
}
}
private function retrievePaginatedListing(array $options): Generator
{
$resultPaginator = $this->client->getPaginator('ListObjectsV2', $options + $this->options);
foreach ($resultPaginator as $result) {
yield from ($result->get('CommonPrefixes') ?: []);
yield from ($result->get('Contents') ?: []);
}
}
public function move(string $source, string $destination, Config $config): void
{
try {
$this->copy($source, $destination, $config);
$this->delete($source);
} catch (FilesystemOperationFailed $exception) {
throw UnableToMoveFile::fromLocationTo($source, $destination, $exception);
}
}
public function copy(string $source, string $destination, Config $config): void
{
try {
/** @var string $visibility */
$visibility = $config->get(Config::OPTION_VISIBILITY) ?: $this->visibility($source)->visibility();
} catch (Throwable $exception) {
throw UnableToCopyFile::fromLocationTo(
$source,
$destination,
$exception
);
}
try {
$this->client->copy(
$this->bucket,
$this->prefixer->prefixPath($source),
$this->bucket,
$this->prefixer->prefixPath($destination),
$this->visibility->visibilityToAcl($visibility),
$this->createOptionsFromConfig($config)['params']
);
} catch (Throwable $exception) {
throw UnableToCopyFile::fromLocationTo($source, $destination, $exception);
}
}
private function readObject(string $path, bool $wantsStream): StreamInterface
{
$options = ['Bucket' => $this->bucket, 'Key' => $this->prefixer->prefixPath($path)];
if ($wantsStream && $this->streamReads && ! isset($this->options['@http']['stream'])) {
$options['@http']['stream'] = true;
}
$command = $this->client->getCommand('GetObject', $options + $this->options);
try {
return $this->client->execute($command)->get('Body');
} catch (Throwable $exception) {
throw UnableToReadFile::fromLocation($path, '', $exception);
}
}
public function publicUrl(string $path, Config $config): string
{
$location = $this->prefixer->prefixPath($path);
try {
return $this->client->getObjectUrl($this->bucket, $location);
} catch (Throwable $exception) {
throw UnableToGeneratePublicUrl::dueToError($path, $exception);
}
}
public function checksum(string $path, Config $config): string
{
$algo = $config->get('checksum_algo', 'etag');
if ($algo !== 'etag') {
throw new ChecksumAlgoIsNotSupported();
}
try {
$metadata = $this->fetchFileMetadata($path, 'checksum')->extraMetadata();
} catch (UnableToRetrieveMetadata $exception) {
throw new UnableToProvideChecksum($exception->reason(), $path, $exception);
}
if ( ! isset($metadata['ETag'])) {
throw new UnableToProvideChecksum('ETag header not available.', $path);
}
return trim($metadata['ETag'], '"');
}
public function temporaryUrl(string $path, DateTimeInterface $expiresAt, Config $config): string
{
try {
$options = $config->get('get_object_options', []);
$command = $this->client->getCommand('GetObject', [
'Bucket' => $this->bucket,
'Key' => $this->prefixer->prefixPath($path),
] + $options);
$presignedRequestOptions = $config->get('presigned_request_options', []);
$request = $this->client->createPresignedRequest($command, $expiresAt, $presignedRequestOptions);
return (string)$request->getUri();
} catch (Throwable $exception) {
throw UnableToGenerateTemporaryUrl::dueToError($path, $exception);
}
}
}

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2014-2019 Frank de Jonge
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace League\Flysystem\AwsS3V3;
use League\Flysystem\Visibility;
class PortableVisibilityConverter implements VisibilityConverter
{
private const PUBLIC_GRANTEE_URI = 'http://acs.amazonaws.com/groups/global/AllUsers';
private const PUBLIC_GRANTS_PERMISSION = 'READ';
private const PUBLIC_ACL = 'public-read';
private const PRIVATE_ACL = 'private';
public function __construct(private string $defaultForDirectories = Visibility::PUBLIC)
{
}
public function visibilityToAcl(string $visibility): string
{
if ($visibility === Visibility::PUBLIC) {
return self::PUBLIC_ACL;
}
return self::PRIVATE_ACL;
}
public function aclToVisibility(array $grants): string
{
foreach ($grants as $grant) {
$granteeUri = $grant['Grantee']['URI'] ?? null;
$permission = $grant['Permission'] ?? null;
if ($granteeUri === self::PUBLIC_GRANTEE_URI && $permission === self::PUBLIC_GRANTS_PERMISSION) {
return Visibility::PUBLIC;
}
}
return Visibility::PRIVATE;
}
public function defaultForDirectories(): string
{
return $this->defaultForDirectories;
}
}

View File

@@ -0,0 +1,12 @@
<?php
declare(strict_types=1);
namespace League\Flysystem\AwsS3V3;
interface VisibilityConverter
{
public function visibilityToAcl(string $visibility): string;
public function aclToVisibility(array $grants): string;
public function defaultForDirectories(): string;
}

View File

@@ -0,0 +1,161 @@
# Changelog
## 1.0.30 - 2022-07-02
* upgrade to list objects v2
## 1.0.29 - 2020-10-08
* copies now switch to multipart copy for large files.
## 1.0.28 - 2020-08-22
* __Allow streamed read by default.__<br/>
This change prevents the stream from being seekable (func
calls like rewind have no effect). Need to seek through the stream?
Check out the docs to see how to disable streaming read: https://flysystem.thephpleague.com/v1/docs/adapter/aws-s3-v3/#streamed-reads
## 1.0.27 - 2020-08-22
* Revert always streaming reads (degraded functionality).
## 1.0.26 - 2020-08-18
* Always stream reads (#211)
## 1.0.25 - 2020-06-02
* Use `S3Client::encodeKey` for key encoding.
## 1.0.24 - 2020-02-23
* Depend on S3ClientInterface rather than the concrete client.
## 1.0.23 - 2019-06-05
* Prevent content type detection for directory creation.
* Use `rawurlencode` instead of `urlencode` to treat url encoding in a spec compliant way.
## 1.0.22 - 2019-01-31
* Invert type check where string/resource difference is determined for ContentLength option.
## 1.0.21 - 2018-10-08
* Catch multipart upload errors.
## 1.0.20 - 2018-09-25
* Fixed prefix handling for uploads (writes and updates).
## 1.0.19 - 2018-03-27
* Added ETAG to response mapping.
## 1.0.18 - 2017-06-30
### Fixed
* Allow metadata to be returned through the getMetadata method.
## 1.0.17 - 2017-06-30
### Fixed
* Allow passing options to methods that don't accept options.
## 1.0.16 - 2017-06-08
### Improved
* Allow the `Tagging` meta option.
## 1.0.15 - 2017-04-28
### Improved
* Indicate this adapter can overwrite files.
## 1.0.14 - 2017-01-02
### Improved
* Now also detect mimetypes of streams.
## 1.0.13 - 2016-06-21
### Fixed
* Uploading a remote stream no longer results in an unexpected exception.
## 1.0.12 - 2016-06-06
### Improved
* Responses are now streamed instead of downloaded fully.
## 1.0.11 - 2016-05-03
### Fixed
* [::has] A regression introduced in 1.0.10 is addressed.
## 1.0.10 - 2016-04-19
### Fixed
* [::has] The `has` method now also respects implicit directories.
## 1.0.9 - 2015-11-19
### Fixed
* [#49] Large listings only returned the last page of the listing.
## 1.0.8 - 2015-11-06
### Improved
* Non-recursive listings now retrieve a shallow listing for better performance.
## 1.0.7 - 2015-11-06
### Fixed
* The `copy` operation now `urlencode`'s the `CopySource` to allow characters like `+`.
## 1.0.6 - 2015-09-25
### Fixed
* The `has` operation now respects path prefix, bug introduced in 1.0.5.
## 1.0.5 - 2015-09-22
### Fixed
* `has` calls now use `doesObjectExist` rather than retrieving metadata.
## 1.0.4 - 2015-07-06
### Fixed
* Fixed delete return value.
## 1.0.3 - 2015-06-16
### Fixed
* Use an iterator for contents listing to break through the 1000 objects limit.
## 1.0.2 - 2015-06-06
### Fixed
* Exception due to misconfiguration no longer causes a fatal error but are properly rethrown.
## 1.0.1 - 2015-05-31
### Fixed
* Stable release depending in the first v3 release of the AWS SDK.

View File

@@ -0,0 +1,28 @@
{
"name": "league/flysystem-aws-s3-v3",
"description": "AWS S3 filesystem adapter for Flysystem.",
"keywords": ["aws", "s3", "flysystem", "filesystem", "storage", "file", "files"],
"type": "library",
"autoload": {
"psr-4": {
"League\\Flysystem\\AwsS3V3\\": ""
}
},
"require": {
"php": "^8.0.2",
"league/flysystem": "^3.10.0",
"league/mime-type-detection": "^1.0.0",
"aws/aws-sdk-php": "^3.220.0"
},
"conflict": {
"guzzlehttp/ringphp": "<1.1.1",
"guzzlehttp/guzzle": "<7.0"
},
"license": "MIT",
"authors": [
{
"name": "Frank de Jonge",
"email": "info@frankdejonge.nl"
}
]
}

View File

@@ -0,0 +1,723 @@
<?php
namespace League\Flysystem\AwsS3v3;
use Aws\Result;
use Aws\S3\Exception\DeleteMultipleObjectsException;
use Aws\S3\Exception\S3Exception;
use Aws\S3\Exception\S3MultipartUploadException;
use Aws\S3\S3Client;
use Aws\S3\S3ClientInterface;
use League\Flysystem\AdapterInterface;
use League\Flysystem\Adapter\AbstractAdapter;
use League\Flysystem\Adapter\CanOverwriteFiles;
use League\Flysystem\Config;
use League\Flysystem\Util;
class AwsS3Adapter extends AbstractAdapter implements CanOverwriteFiles
{
const PUBLIC_GRANT_URI = 'http://acs.amazonaws.com/groups/global/AllUsers';
/**
* @var array
*/
protected static $resultMap = [
'Body' => 'contents',
'ContentLength' => 'size',
'ContentType' => 'mimetype',
'Size' => 'size',
'Metadata' => 'metadata',
'StorageClass' => 'storageclass',
'ETag' => 'etag',
'VersionId' => 'versionid'
];
/**
* @var array
*/
protected static $metaOptions = [
'ACL',
'CacheControl',
'ContentDisposition',
'ContentEncoding',
'ContentLength',
'ContentMD5',
'ContentType',
'Expires',
'GrantFullControl',
'GrantRead',
'GrantReadACP',
'GrantWriteACP',
'Metadata',
'RequestPayer',
'SSECustomerAlgorithm',
'SSECustomerKey',
'SSECustomerKeyMD5',
'SSEKMSKeyId',
'ServerSideEncryption',
'StorageClass',
'Tagging',
'WebsiteRedirectLocation',
];
/**
* @var S3ClientInterface
*/
protected $s3Client;
/**
* @var string
*/
protected $bucket;
/**
* @var array
*/
protected $options = [];
/**
* @var bool
*/
private $streamReads;
public function __construct(S3ClientInterface $client, $bucket, $prefix = '', array $options = [], $streamReads = true)
{
$this->s3Client = $client;
$this->bucket = $bucket;
$this->setPathPrefix($prefix);
$this->options = $options;
$this->streamReads = $streamReads;
}
/**
* Get the S3Client bucket.
*
* @return string
*/
public function getBucket()
{
return $this->bucket;
}
/**
* Set the S3Client bucket.
*
* @return string
*/
public function setBucket($bucket)
{
$this->bucket = $bucket;
}
/**
* Get the S3Client instance.
*
* @return S3ClientInterface
*/
public function getClient()
{
return $this->s3Client;
}
/**
* Write a new file.
*
* @param string $path
* @param string $contents
* @param Config $config Config object
*
* @return false|array false on failure file meta data on success
*/
public function write($path, $contents, Config $config)
{
return $this->upload($path, $contents, $config);
}
/**
* Update a file.
*
* @param string $path
* @param string $contents
* @param Config $config Config object
*
* @return false|array false on failure file meta data on success
*/
public function update($path, $contents, Config $config)
{
return $this->upload($path, $contents, $config);
}
/**
* Rename a file.
*
* @param string $path
* @param string $newpath
*
* @return bool
*/
public function rename($path, $newpath)
{
if ( ! $this->copy($path, $newpath)) {
return false;
}
return $this->delete($path);
}
/**
* Delete a file.
*
* @param string $path
*
* @return bool
*/
public function delete($path)
{
$location = $this->applyPathPrefix($path);
$command = $this->s3Client->getCommand(
'deleteObject',
[
'Bucket' => $this->bucket,
'Key' => $location,
]
);
$this->s3Client->execute($command);
return ! $this->has($path);
}
/**
* Delete a directory.
*
* @param string $dirname
*
* @return bool
*/
public function deleteDir($dirname)
{
try {
$prefix = $this->applyPathPrefix($dirname) . '/';
$this->s3Client->deleteMatchingObjects($this->bucket, $prefix);
} catch (DeleteMultipleObjectsException $exception) {
return false;
}
return true;
}
/**
* Create a directory.
*
* @param string $dirname directory name
* @param Config $config
*
* @return bool|array
*/
public function createDir($dirname, Config $config)
{
return $this->upload($dirname . '/', '', $config);
}
/**
* Check whether a file exists.
*
* @param string $path
*
* @return bool
*/
public function has($path)
{
$location = $this->applyPathPrefix($path);
if ($this->s3Client->doesObjectExist($this->bucket, $location, $this->options)) {
return true;
}
return $this->doesDirectoryExist($location);
}
/**
* Read a file.
*
* @param string $path
*
* @return false|array
*/
public function read($path)
{
$response = $this->readObject($path);
if ($response !== false) {
$response['contents'] = $response['contents']->getContents();
}
return $response;
}
/**
* List contents of a directory.
*
* @param string $directory
* @param bool $recursive
*
* @return array
*/
public function listContents($directory = '', $recursive = false)
{
$prefix = $this->applyPathPrefix(rtrim($directory, '/') . '/');
$options = ['Bucket' => $this->bucket, 'Prefix' => ltrim($prefix, '/')];
if ($recursive === false) {
$options['Delimiter'] = '/';
}
$listing = $this->retrievePaginatedListing($options);
$normalizer = [$this, 'normalizeResponse'];
$normalized = array_map($normalizer, $listing);
return Util::emulateDirectories($normalized);
}
/**
* @param array $options
*
* @return array
*/
protected function retrievePaginatedListing(array $options)
{
$resultPaginator = $this->s3Client->getPaginator('ListObjectsV2', $options);
$listing = [];
foreach ($resultPaginator as $result) {
$listing = array_merge($listing, $result->get('Contents') ?: [], $result->get('CommonPrefixes') ?: []);
}
return $listing;
}
/**
* Get all the meta data of a file or directory.
*
* @param string $path
*
* @return false|array
*/
public function getMetadata($path)
{
$command = $this->s3Client->getCommand(
'headObject',
[
'Bucket' => $this->bucket,
'Key' => $this->applyPathPrefix($path),
] + $this->options
);
/* @var Result $result */
try {
$result = $this->s3Client->execute($command);
} catch (S3Exception $exception) {
if ($this->is404Exception($exception)) {
return false;
}
throw $exception;
}
return $this->normalizeResponse($result->toArray(), $path);
}
/**
* @return bool
*/
private function is404Exception(S3Exception $exception)
{
$response = $exception->getResponse();
if ($response !== null && $response->getStatusCode() === 404) {
return true;
}
return false;
}
/**
* Get all the meta data of a file or directory.
*
* @param string $path
*
* @return false|array
*/
public function getSize($path)
{
return $this->getMetadata($path);
}
/**
* Get the mimetype of a file.
*
* @param string $path
*
* @return false|array
*/
public function getMimetype($path)
{
return $this->getMetadata($path);
}
/**
* Get the timestamp of a file.
*
* @param string $path
*
* @return false|array
*/
public function getTimestamp($path)
{
return $this->getMetadata($path);
}
/**
* Write a new file using a stream.
*
* @param string $path
* @param resource $resource
* @param Config $config Config object
*
* @return array|false false on failure file meta data on success
*/
public function writeStream($path, $resource, Config $config)
{
return $this->upload($path, $resource, $config);
}
/**
* Update a file using a stream.
*
* @param string $path
* @param resource $resource
* @param Config $config Config object
*
* @return array|false false on failure file meta data on success
*/
public function updateStream($path, $resource, Config $config)
{
return $this->upload($path, $resource, $config);
}
/**
* Copy a file.
*
* @param string $path
* @param string $newpath
*
* @return bool
*/
public function copy($path, $newpath)
{
try {
$this->s3Client->copy(
$this->bucket,
$this->applyPathPrefix($path),
$this->bucket,
$this->applyPathPrefix($newpath),
$this->getRawVisibility($path) === AdapterInterface::VISIBILITY_PUBLIC
? 'public-read' : 'private',
$this->options
);
} catch (S3Exception $e) {
return false;
}
return true;
}
/**
* Read a file as a stream.
*
* @param string $path
*
* @return array|false
*/
public function readStream($path)
{
$response = $this->readObject($path);
if ($response !== false) {
$response['stream'] = $response['contents']->detach();
unset($response['contents']);
}
return $response;
}
/**
* Read an object and normalize the response.
*
* @param string $path
*
* @return array|bool
*/
protected function readObject($path)
{
$options = [
'Bucket' => $this->bucket,
'Key' => $this->applyPathPrefix($path),
] + $this->options;
if ($this->streamReads && ! isset($options['@http']['stream'])) {
$options['@http']['stream'] = true;
}
$command = $this->s3Client->getCommand('getObject', $options + $this->options);
try {
/** @var Result $response */
$response = $this->s3Client->execute($command);
} catch (S3Exception $e) {
return false;
}
return $this->normalizeResponse($response->toArray(), $path);
}
/**
* Set the visibility for a file.
*
* @param string $path
* @param string $visibility
*
* @return array|false file meta data
*/
public function setVisibility($path, $visibility)
{
$command = $this->s3Client->getCommand(
'putObjectAcl',
[
'Bucket' => $this->bucket,
'Key' => $this->applyPathPrefix($path),
'ACL' => $visibility === AdapterInterface::VISIBILITY_PUBLIC ? 'public-read' : 'private',
]
);
try {
$this->s3Client->execute($command);
} catch (S3Exception $exception) {
return false;
}
return compact('path', 'visibility');
}
/**
* Get the visibility of a file.
*
* @param string $path
*
* @return array|false
*/
public function getVisibility($path)
{
return ['visibility' => $this->getRawVisibility($path)];
}
/**
* {@inheritdoc}
*/
public function applyPathPrefix($path)
{
return ltrim(parent::applyPathPrefix($path), '/');
}
/**
* {@inheritdoc}
*/
public function setPathPrefix($prefix)
{
$prefix = ltrim((string) $prefix, '/');
return parent::setPathPrefix($prefix);
}
/**
* Get the object acl presented as a visibility.
*
* @param string $path
*
* @return string
*/
protected function getRawVisibility($path)
{
$command = $this->s3Client->getCommand(
'getObjectAcl',
[
'Bucket' => $this->bucket,
'Key' => $this->applyPathPrefix($path),
]
);
$result = $this->s3Client->execute($command);
$visibility = AdapterInterface::VISIBILITY_PRIVATE;
foreach ($result->get('Grants') as $grant) {
if (
isset($grant['Grantee']['URI'])
&& $grant['Grantee']['URI'] === self::PUBLIC_GRANT_URI
&& $grant['Permission'] === 'READ'
) {
$visibility = AdapterInterface::VISIBILITY_PUBLIC;
break;
}
}
return $visibility;
}
/**
* Upload an object.
*
* @param string $path
* @param string|resource $body
* @param Config $config
*
* @return array|bool
*/
protected function upload($path, $body, Config $config)
{
$key = $this->applyPathPrefix($path);
$options = $this->getOptionsFromConfig($config);
$acl = array_key_exists('ACL', $options) ? $options['ACL'] : 'private';
if (!$this->isOnlyDir($path)) {
if ( ! isset($options['ContentType'])) {
$options['ContentType'] = Util::guessMimeType($path, $body);
}
if ( ! isset($options['ContentLength'])) {
$options['ContentLength'] = is_resource($body) ? Util::getStreamSize($body) : Util::contentSize($body);
}
if ($options['ContentLength'] === null) {
unset($options['ContentLength']);
}
}
try {
$this->s3Client->upload($this->bucket, $key, $body, $acl, ['params' => $options]);
} catch (S3MultipartUploadException $multipartUploadException) {
return false;
}
return $this->normalizeResponse($options, $path);
}
/**
* Check if the path contains only directories
*
* @param string $path
*
* @return bool
*/
private function isOnlyDir($path)
{
return substr($path, -1) === '/';
}
/**
* Get options from the config.
*
* @param Config $config
*
* @return array
*/
protected function getOptionsFromConfig(Config $config)
{
$options = $this->options;
if ($visibility = $config->get('visibility')) {
// For local reference
$options['visibility'] = $visibility;
// For external reference
$options['ACL'] = $visibility === AdapterInterface::VISIBILITY_PUBLIC ? 'public-read' : 'private';
}
if ($mimetype = $config->get('mimetype')) {
// For local reference
$options['mimetype'] = $mimetype;
// For external reference
$options['ContentType'] = $mimetype;
}
foreach (static::$metaOptions as $option) {
if ( ! $config->has($option)) {
continue;
}
$options[$option] = $config->get($option);
}
return $options;
}
/**
* Normalize the object result array.
*
* @param array $response
* @param string $path
*
* @return array
*/
protected function normalizeResponse(array $response, $path = null)
{
$result = [
'path' => $path ?: $this->removePathPrefix(
isset($response['Key']) ? $response['Key'] : $response['Prefix']
),
];
$result = array_merge($result, Util::pathinfo($result['path']));
if (isset($response['LastModified'])) {
$result['timestamp'] = strtotime($response['LastModified']);
}
if ($this->isOnlyDir($result['path'])) {
$result['type'] = 'dir';
$result['path'] = rtrim($result['path'], '/');
return $result;
}
return array_merge($result, Util::map($response, static::$resultMap), ['type' => 'file']);
}
/**
* @param string $location
*
* @return bool
*/
protected function doesDirectoryExist($location)
{
// Maybe this isn't an actual key, but a prefix.
// Do a prefix listing of objects to determine.
$command = $this->s3Client->getCommand(
'ListObjectsV2',
[
'Bucket' => $this->bucket,
'Prefix' => rtrim($location, '/') . '/',
'MaxKeys' => 1,
]
);
try {
$result = $this->s3Client->execute($command);
return $result['Contents'] || $result['CommonPrefixes'];
} catch (S3Exception $e) {
if (in_array($e->getStatusCode(), [403, 404], true)) {
return false;
}
throw $e;
}
}
}