[s3_storage] Bump version of akeeba/s3 to version 2.3.1 #1450

Merged
heluecht merged 2 commits from MrPetovan/friendica-addons:bug/deprecated into 2023.09-rc 2023-12-20 14:17:02 +01:00
61 changed files with 1472 additions and 708 deletions
Showing only changes of commit 3e74af9775 - Show all commits

View file

@ -8,32 +8,35 @@
"packages": [ "packages": [
{ {
"name": "akeeba/s3", "name": "akeeba/s3",
"version": "2.0.0", "version": "2.3.1",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/akeeba/s3.git", "url": "https://github.com/akeeba/s3.git",
"reference": "01520dae1f736555e08efda0ddc1044701bd340a" "reference": "7f5b3e929c93eb02ba24472560c0cbbef735aed9"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/akeeba/s3/zipball/01520dae1f736555e08efda0ddc1044701bd340a", "url": "https://api.github.com/repos/akeeba/s3/zipball/7f5b3e929c93eb02ba24472560c0cbbef735aed9",
"reference": "01520dae1f736555e08efda0ddc1044701bd340a", "reference": "7f5b3e929c93eb02ba24472560c0cbbef735aed9",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-curl": "*", "ext-curl": "*",
"ext-simplexml": "*", "ext-simplexml": "*",
"php": ">=7.1.0 <8.1" "php": ">=7.1.0 <8.4"
}, },
"type": "library", "type": "library",
"autoload": { "autoload": {
"files": [
"src/aliasing.php"
],
"psr-4": { "psr-4": {
"Akeeba\\Engine\\Postproc\\Connector\\S3v4\\": "src" "Akeeba\\S3\\": "src"
} }
}, },
"notification-url": "https://packagist.org/downloads/", "notification-url": "https://packagist.org/downloads/",
"license": [ "license": [
"GPL-3.0+" "GPL-3.0-or-later"
], ],
"authors": [ "authors": [
{ {
@ -48,7 +51,7 @@
"keywords": [ "keywords": [
"s3" "s3"
], ],
"time": "2020-11-30T14:03:55+00:00" "time": "2023-09-26T11:40:10+00:00"
} }
], ],
"packages-dev": [], "packages-dev": [],

View file

@ -1,5 +1,6 @@
/.idea/ /.idea/
/000/ /000/
/minitest/config.php /minitest/config.*
!/minitest/config.dist.php
/minitest/tmp /minitest/tmp
/vendor/ /vendor/

View file

@ -4,29 +4,53 @@ A compact, dependency-less Amazon S3 API client implementing the most commonly u
## Why reinvent the wheel ## Why reinvent the wheel
After having a lot of impossible to debug problems with Amazon's Guzzle-based AWS SDK we decided to roll our own connector for Amazon S3. This is by no means a complete implementation, just a small subset of S3's features which are required by our software. The design goals are simplicity, no external dependencies and low memory footprint. After having a lot of impossible to debug problems with Amazon's Guzzle-based AWS SDK we decided to roll our own connector for Amazon S3. This is by no means a complete implementation, just a small subset of S3's features which are required by our software. The design goals are simplicity, no external dependencies and a low memory footprint.
This code was originally based on [S3.php written by Donovan Schonknecht](http://undesigned.org.za/2007/10/22/amazon-s3-php-class) which is available under a BSD-like license. This repository no longer reflects the original author's work and should not be confused with it. This code was originally based on [S3.php written by Donovan Schonknecht](http://undesigned.org.za/2007/10/22/amazon-s3-php-class) which is available under a BSD-like license. This repository no longer reflects the original author's work and should not be confused with it.
This software is distributed under the GNU General Public License version 3 or, at your option, any later version published by the Free Software Foundation (FSF). In short, it's "GPLv3+". This software is distributed under the GNU General Public License version 3 or, at your option, any later version published by the Free Software Foundation (FSF). In short, it's GPL-3.0-or-later, as noted in composer.json.
## Important note about version 2 ## Important notes about version 2
Akeeba Amazon S3 Connector version 2 has dropped support for PPH 5.3 to 7.0 inclusive. It is only compatible with PHP 7.1 or later, up to and including PHP 8.0. ### PHP version support since 2.0
The most significant change in this version is that all methods use scalar type hints for parameters and return values. This _may_ break existing consumers which relied on implicit type conversion e.g. passing strings containing integer values instead of _actual_ integer values. Akeeba Amazon S3 Connector version 2 has dropped support for PHP 5.3 to 7.0 inclusive.
The most significant change in this version is that all methods use scalar type hints for parameters and return values. This _may_ break existing consumers which relied on implicit type conversion.
### Namespace change since 2.3
Up to and including version 2.2 of the library, the namespace was `\Akeeba\Engine\Postproc\Connector\S3v4`. From version 2.3 of the library the namespace has changed to `\Akeeba\S3`.
The library automatically registers aliases of the old classes to the new ones, thus ensuring updating the library will not introduce backwards incompatible changes. This is why it's not a major version update. Aliases will remain in place until at least version 3.0 of the library.
## Using the connector ## Using the connector
You need to define a constant before using or referencing any class in the library:
```php
defined('AKEEBAENGINE') or define('AKEEBAENGINE', 1);
```
All library files have a line similar to
```php
defined('AKEEBAENGINE') or die();
```
to prevent direct access to the libraries files. This is intentional. The primary use case for this library is mass-distributed software which gets installed in a publicly accessible subdirectory of the web root. This line prevents any accidental path disclosure from PHP error messages if someone were to access these files directly on misconfigured servers.
If you are writing a Joomla extension, especially a plugin or module, please _always_ check if the constant has already been defined before defining it yourself. Thank you!
### Get a connector object ### Get a connector object
```php ```php
$configuration = new \Akeeba\Engine\Postproc\Connector\S3v4\Configuration( $configuration = new \Akeeba\S3\Configuration(
'YourAmazonAccessKey', 'YourAmazonAccessKey',
'YourAmazonSecretKey' 'YourAmazonSecretKey'
); );
$connector = new \Akeeba\Engine\Postproc\Connector\S3v4\Connector($configuration); $connector = new \Akeeba\S3\Connector($configuration);
``` ```
If you are running inside an Amazon EC2 instance you can fetch temporary credentials from the instance's metadata If you are running inside an Amazon EC2 instance you can fetch temporary credentials from the instance's metadata
@ -37,7 +61,7 @@ IP hosting the instance's metadata cache service):
$role = file_get_contents('http://169.254.169.254/latest/meta-data/iam/security-credentials/'); $role = file_get_contents('http://169.254.169.254/latest/meta-data/iam/security-credentials/');
$jsonCredentials = file_get_contents('http://169.254.169.254/latest/meta-data/iam/security-credentials/' . $role); $jsonCredentials = file_get_contents('http://169.254.169.254/latest/meta-data/iam/security-credentials/' . $role);
$credentials = json_decode($jsonCredentials, true); $credentials = json_decode($jsonCredentials, true);
$configuration = new \Akeeba\Engine\Postproc\Connector\S3v4\Configuration( $configuration = new \Akeeba\S3\Configuration(
$credentials['AccessKeyId'], $credentials['AccessKeyId'],
$credentials['SecretAccessKey'], $credentials['SecretAccessKey'],
'v4', 'v4',
@ -45,14 +69,14 @@ $configuration = new \Akeeba\Engine\Postproc\Connector\S3v4\Configuration(
); );
$configuration->setToken($credentials['Token']); $configuration->setToken($credentials['Token']);
$connector = new \Akeeba\Engine\Postproc\Connector\S3v4\Connector($configuration); $connector = new \Akeeba\S3\Connector($configuration);
``` ```
where `$yourRegion` is the AWS region of your bucket, e.g. `us-east-1`. Please note that we are passing the security where `$yourRegion` is the AWS region of your bucket, e.g. `us-east-1`. Please note that we are passing the security
token (`$credentials['Token']`) to the Configuration object. This is REQUIRED. The temporary credentials returned by token (`$credentials['Token']`) to the Configuration object. This is REQUIRED. The temporary credentials returned by
the metadata service won't work without it. the metadata service won't work without it.
Also worth noting is that the temporary credentials don't last forever. Check the `$credentials['Expiration']` to see Another point worth noting is that the temporary credentials don't last forever. Check the `$credentials['Expiration']` to see
when they are about to expire. Amazon recommends that you retry fetching new credentials from the metadata service when they are about to expire. Amazon recommends that you retry fetching new credentials from the metadata service
10 minutes before your cached credentials are set to expire. The metadata service is guaranteed to provision fresh 10 minutes before your cached credentials are set to expire. The metadata service is guaranteed to provision fresh
temporary credentials by that time. temporary credentials by that time.
@ -120,21 +144,21 @@ The last parameter (common prefixes) controls the listing of "subdirectories"
From a file: From a file:
```php ```php
$input = \Akeeba\Engine\Postproc\Connector\S3v4\Input::createFromFile($sourceFile); $input = \Akeeba\S3\Input::createFromFile($sourceFile);
$connector->putObject($input, 'mybucket', 'path/to/myfile.txt'); $connector->putObject($input, 'mybucket', 'path/to/myfile.txt');
``` ```
From a string: From a string:
```php ```php
$input = \Akeeba\Engine\Postproc\Connector\S3v4\Input::createFromData($sourceString); $input = \Akeeba\S3\Input::createFromData($sourceString);
$connector->putObject($input, 'mybucket', 'path/to/myfile.txt'); $connector->putObject($input, 'mybucket', 'path/to/myfile.txt');
``` ```
From a stream resource: From a stream resource:
```php ```php
$input = \Akeeba\Engine\Postproc\Connector\S3v4\Input::createFromResource($streamHandle, false); $input = \Akeeba\S3\Input::createFromResource($streamHandle, false);
$connector->putObject($input, 'mybucket', 'path/to/myfile.txt'); $connector->putObject($input, 'mybucket', 'path/to/myfile.txt');
``` ```
@ -145,7 +169,7 @@ In all cases the entirety of the file has to be loaded in memory.
Files are uploaded in 5Mb chunks. Files are uploaded in 5Mb chunks.
```php ```php
$input = \Akeeba\Engine\Postproc\Connector\S3v4\Input::createFromFile($sourceFile); $input = \Akeeba\S3\Input::createFromFile($sourceFile);
$uploadId = $connector->startMultipart($input, 'mybucket', 'mypath/movie.mov'); $uploadId = $connector->startMultipart($input, 'mybucket', 'mypath/movie.mov');
$eTags = array(); $eTags = array();
@ -155,7 +179,7 @@ $partNumber = 0;
do do
{ {
// IMPORTANT: You MUST create the input afresh before each uploadMultipart call // IMPORTANT: You MUST create the input afresh before each uploadMultipart call
$input = \Akeeba\Engine\Postproc\Connector\S3v4\Input::createFromFile($sourceFile); $input = \Akeeba\S3\Input::createFromFile($sourceFile);
$input->setUploadID($uploadId); $input->setUploadID($uploadId);
$input->setPartNumber(++$partNumber); $input->setPartNumber(++$partNumber);
@ -169,7 +193,7 @@ do
while (!is_null($eTag)); while (!is_null($eTag));
// IMPORTANT: You MUST create the input afresh before finalising the multipart upload // IMPORTANT: You MUST create the input afresh before finalising the multipart upload
$input = \Akeeba\Engine\Postproc\Connector\S3v4\Input::createFromFile($sourceFile); $input = \Akeeba\S3\Input::createFromFile($sourceFile);
$input->setUploadID($uploadId); $input->setUploadID($uploadId);
$input->setEtags($eTags); $input->setEtags($eTags);
@ -209,6 +233,23 @@ $content = $connector->getObject('mybucket', 'path/to/file.jpg', false);
$connector->deleteObject('mybucket', 'path/to/file.jpg'); $connector->deleteObject('mybucket', 'path/to/file.jpg');
``` ```
### Test if an object exists
```php
try
{
$headers = $connector->headObject('mybucket', 'path/to/file.jpg');
$exists = true;
}
catch (\Akeeba\S3\Exception\CannotGetFile $e)
{
$headers = [];
$exists = false;
}
```
The `$headers` variable contains an array with the S3 headers returned by the [HeadObject(https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html) API call. The header keys are always in lowercase. Please note that _not all_ of the headers Amazon describes in their documentation are returned in every request.
## Configuration options ## Configuration options
The Configuration option has optional methods which can be used to enable some useful features in the connector. The Configuration option has optional methods which can be used to enable some useful features in the connector.
@ -216,7 +257,7 @@ The Configuration option has optional methods which can be used to enable some u
You need to execute these methods against the Configuration object before passing it to the Connector's constructor. For example: You need to execute these methods against the Configuration object before passing it to the Connector's constructor. For example:
```php ```php
$configuration = new \Akeeba\Engine\Postproc\Connector\S3v4\Configuration( $configuration = new \Akeeba\S3\Configuration(
'YourAmazonAccessKey', 'YourAmazonAccessKey',
'YourAmazonSecretKey' 'YourAmazonSecretKey'
); );
@ -225,7 +266,7 @@ $configuration = new \Akeeba\Engine\Postproc\Connector\S3v4\Configuration(
$configuration->setSignatureMethod('v4'); $configuration->setSignatureMethod('v4');
$configuration->setUseDualstackUrl(true); $configuration->setUseDualstackUrl(true);
$connector = new \Akeeba\Engine\Postproc\Connector\S3v4\Connector($configuration); $connector = new \Akeeba\S3\Connector($configuration);
``` ```
### HTTPS vs plain HTTP ### HTTPS vs plain HTTP
@ -245,7 +286,7 @@ Please note that if the S3-compatible APi uses v4 signatures you need to enter t
```php ```php
// DigitalOcean Spaces using v4 signatures // DigitalOcean Spaces using v4 signatures
// The access credentials are those used in the example at https://developers.digitalocean.com/documentation/spaces/ // The access credentials are those used in the example at https://developers.digitalocean.com/documentation/spaces/
$configuration = new \Akeeba\Engine\Postproc\Connector\S3v4\Configuration( $configuration = new \Akeeba\S3\Configuration(
'532SZONTQ6ALKBCU94OU', '532SZONTQ6ALKBCU94OU',
'zCkY83KVDXD8u83RouEYPKEm/dhPSPB45XsfnWj8fxQ', 'zCkY83KVDXD8u83RouEYPKEm/dhPSPB45XsfnWj8fxQ',
'v4', 'v4',
@ -253,7 +294,7 @@ $configuration = new \Akeeba\Engine\Postproc\Connector\S3v4\Configuration(
); );
$configuration->setEndpoint('nyc3.digitaloceanspaces.com'); $configuration->setEndpoint('nyc3.digitaloceanspaces.com');
$connector = new \Akeeba\Engine\Postproc\Connector\S3v4\Connector($configuration); $connector = new \Akeeba\S3\Connector($configuration);
``` ```
If your S3-compatible API uses v2 signatures you do not need to specify a region. If your S3-compatible API uses v2 signatures you do not need to specify a region.
@ -261,14 +302,14 @@ If your S3-compatible API uses v2 signatures you do not need to specify a region
```php ```php
// DigitalOcean Spaces using v2 signatures // DigitalOcean Spaces using v2 signatures
// The access credentials are those used in the example at https://developers.digitalocean.com/documentation/spaces/ // The access credentials are those used in the example at https://developers.digitalocean.com/documentation/spaces/
$configuration = new \Akeeba\Engine\Postproc\Connector\S3v4\Configuration( $configuration = new \Akeeba\S3\Configuration(
'532SZONTQ6ALKBCU94OU', '532SZONTQ6ALKBCU94OU',
'zCkY83KVDXD8u83RouEYPKEm/dhPSPB45XsfnWj8fxQ', 'zCkY83KVDXD8u83RouEYPKEm/dhPSPB45XsfnWj8fxQ',
'v2' 'v2'
); );
$configuration->setEndpoint('nyc3.digitaloceanspaces.com'); $configuration->setEndpoint('nyc3.digitaloceanspaces.com');
$connector = new \Akeeba\Engine\Postproc\Connector\S3v4\Connector($configuration); $connector = new \Akeeba\S3\Connector($configuration);
``` ```
### Legacy path-style access ### Legacy path-style access
@ -282,7 +323,7 @@ You need to do:
$configuration->setUseLegacyPathStyle(true); $configuration->setUseLegacyPathStyle(true);
``` ```
Caveat: this will not work with v2 signatures if you are using Amazon AWS S3 proper. It will work with the v2 signatures if you are using a custom endpoint, though. In fact, most S3-compatible APIs implementing V2 signatures _expect_ you to use path-style access. Caveat: this will not work with v2 signatures if you are using Amazon AWS S3 proper. It will very likely work with the v2 signatures if you are using a custom endpoint, though.
### Dualstack (IPv4 and IPv6) support ### Dualstack (IPv4 and IPv6) support

View file

@ -1,13 +0,0 @@
Need to check:
endpoint in [amazon, custom]
signature in [v2, v4]
path style in [true, false]
upload
download
presigned URL generation
presigned URL access
USING VIRTUAL HOSTING, v4 SIGNATURES
presigned URL must use s3.amazonaws.com i.e. path-style hosting (because who needs logic?)

View file

@ -3,7 +3,7 @@
"type": "library", "type": "library",
"description": "A compact, dependency-less Amazon S3 API client implementing the most commonly used features", "description": "A compact, dependency-less Amazon S3 API client implementing the most commonly used features",
"require": { "require": {
"php": ">=7.1.0 <8.1", "php": ">=7.1.0 <8.4",
"ext-curl": "*", "ext-curl": "*",
"ext-simplexml": "*" "ext-simplexml": "*"
}, },
@ -11,7 +11,7 @@
"s3" "s3"
], ],
"homepage": "https://github.com/akeeba/s3", "homepage": "https://github.com/akeeba/s3",
"license": "GPL-3.0+", "license": "GPL-3.0-or-later",
"authors": [ "authors": [
{ {
"name": "Nicholas K. Dionysopoulos", "name": "Nicholas K. Dionysopoulos",
@ -22,7 +22,16 @@
], ],
"autoload": { "autoload": {
"psr-4": { "psr-4": {
"Akeeba\\Engine\\Postproc\\Connector\\S3v4\\": "src" "Akeeba\\S3\\": "src"
} },
"files": [
"src/aliasing.php"
]
},
"archive": {
"exclude": [
"minitest",
"TODO.md"
]
} }
} }

View file

@ -1,10 +1,10 @@
{ {
"_readme": [ "_readme": [
"This file locks the dependencies of your project to a known state", "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" "This file is @generated automatically"
], ],
"content-hash": "1070071b351d45a80934e854f0725d64", "content-hash": "27f387a657b2784510b177f73c436346",
"packages": [], "packages": [],
"packages-dev": [], "packages-dev": [],
"aliases": [], "aliases": [],
@ -13,7 +13,10 @@
"prefer-stable": false, "prefer-stable": false,
"prefer-lowest": false, "prefer-lowest": false,
"platform": { "platform": {
"php": ">=5.3.4" "php": ">=7.1.0 <8.4",
"ext-curl": "*",
"ext-simplexml": "*"
}, },
"platform-dev": [] "platform-dev": [],
"plugin-api-version": "2.3.0"
} }

View file

@ -0,0 +1,62 @@
# Testing notes
## Against Amazon S3 proper
This is the _canonical_ method for testing this library since Amazon S3 proper is the canonical provider of the S3 API (and not all of its quirks are fully documented, we might add).
Copy `config.dist.php` to `config.php` and enter the connection information to your Amazon S3 or compatible service.
## Against [LocalStack](https://localstack.cloud)
This method is very useful for development.
Install LocalStack [as per their documentation](https://docs.localstack.cloud/getting-started/installation/).
You will also need to install [`awslocal`](https://github.com/localstack/awscli-local) like so:
```php
pip install awscli
pip install awscli-local
```
Start LocalStack e.g. `localstack start -d`
Create a new bucket called `test` i.e. `awslocal s3 mk s3://test`
Copy `config.dist.php` to `config.php` and make the following changes:
```php
define('DEFAULT_ENDPOINT', 'localhost.localstack.cloud:4566');
define('DEFAULT_ACCESS_KEY', 'ANYRANDOMSTRINGWILLDO');
define('DEFAULT_SECRET_KEY', 'ThisIsAlwaysIgnoredByLocalStack');
define('DEFAULT_REGION', 'us-east-1');
define('DEFAULT_BUCKET', 'test');
define('DEFAULT_SIGNATURE', 'v4');
define('DEFAULT_PATH_ACCESS', true);
```
Note that single- and dualstack tests result in the same URLs for all S3-compatible services, including LocalStack. These tests are essentially duplicates in this use case.
## Against Wasabi
Wasabi nominally supports v4 signatures, but their implementation is actually _non-canonical_, as they only read the date from the optional `x-amz-date` header, without falling back to the standard HTTP `Date` header. We have added a workaround for this behaviour which necessitates testing with it.
Just like with Amazon S3 proper, copy `config.dist.php` to `config.php` and enter the connection information to your Wasabi storage. You will also need to set up the custom endpoint like so:
```php
define('DEFAULT_ENDPOINT', 's3.eu-central-2.wasabisys.com');
```
**IMPORTANT!** The above endpoint will be different, depending on which region you've created your bucket in. The example above assumes the `eu-central-2` region. If you use the wrong region the tests _will_ fail!
## Against Synology C2
Synology C2 is an S3-“compatible” storage service. It is not very “compatible” though, since they implemented Amazon's documentation of the v4 signatures instead of how the v4 signatures work in the real world (yeah, there's a very big difference). While Amazon S3 _in reality_ expects all dates to be formatted as per RFC1123, they document that they expect them to be formatted as per “ISO 8601” and they give their _completely wrong_ interpretation of what the “ISO 8601” format is. Synology did not catch that discrepancy, and they only expect the wrongly formatted dates which is totally NOT what S3 itself expects. Luckily, most third party implementations expect either format because they've caught the discrepancy between documentation and reality, therefore making it possible for us to come up with a viable workaround.
And that's why we need to test with C2 as well, folks.
Copy `config.dist.php` to `config.php` and enter the connection information to your Synology S3 service.
It is very important to note two things:
```php
define('DEFAULT_ENDPOINT', 'eu-002.s3.synologyc2.net');
define('DEFAULT_REGION', 'eu-002');
```
The endpoint URL is given in the Synology C2 Object Manager, next to each bucket. Note the part before `.s3.`. This is the **region** you need to use with v4 signatures. They do not document this anywhere.

View file

@ -3,13 +3,13 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
use RuntimeException; use RuntimeException;
abstract class AbstractTest abstract class AbstractTest
@ -58,24 +58,24 @@ abstract class AbstractTest
*/ */
protected static function createFile(int $size = AbstractTest::SIX_HUNDRED_KB, int $blockSize = self::BLOCK_SIZE, bool $reuseBlock = true) protected static function createFile(int $size = AbstractTest::SIX_HUNDRED_KB, int $blockSize = self::BLOCK_SIZE, bool $reuseBlock = true)
{ {
$tempFilePath = tempnam(self::getTempFolder(), 'as3'); $tempFilePath = tempnam(static::getTempFolder(), 'as3');
if ($tempFilePath === false) if ($tempFilePath === false)
{ {
throw new RuntimeException("Cannot create a temporary file."); throw new RuntimeException("Cannot create a temporary file.");
} }
$fp = @fopen($tempFilePath, 'wb', false); $fp = @fopen($tempFilePath, 'w', false);
if ($fp === false) if ($fp === false)
{ {
throw new RuntimeException("Cannot write to the temporary file."); throw new RuntimeException("Cannot write to the temporary file.");
} }
$blockSize = self::BLOCK_SIZE; $blockSize = static::BLOCK_SIZE;
$lastBlockSize = $size % $blockSize; $lastBlockSize = $size % $blockSize;
$wholeBlocks = (int) (($size - $lastBlockSize) / $blockSize); $wholeBlocks = (int) (($size - $lastBlockSize) / $blockSize);
$blockData = self::getRandomData(); $blockData = static::getRandomData();
for ($i = 0; $i < $wholeBlocks; $i++) for ($i = 0; $i < $wholeBlocks; $i++)
{ {
@ -83,7 +83,7 @@ abstract class AbstractTest
if (!$reuseBlock) if (!$reuseBlock)
{ {
$blockData = self::getRandomData($blockSize); $blockData = static::getRandomData($blockSize);
} }
} }
@ -155,7 +155,7 @@ abstract class AbstractTest
return false; return false;
} }
return hash_file(self::FILE_HASHING_ALGORITHM, $referenceFilePath) === hash_file(self::FILE_HASHING_ALGORITHM, $unknownFilePath); return hash_file(static::FILE_HASHING_ALGORITHM, $referenceFilePath) === hash_file(static::FILE_HASHING_ALGORITHM, $unknownFilePath);
} }
/** /**

View file

@ -3,15 +3,15 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
use Akeeba\Engine\Postproc\Connector\S3v4\Input; use Akeeba\S3\Input;
/** /**
* Upload, download and delete big files (over 1MB), without multipart uploads. Uses string or file sources. * Upload, download and delete big files (over 1MB), without multipart uploads. Uses string or file sources.
@ -51,7 +51,7 @@ class BigFiles extends AbstractTest
/** /**
* Number of uploaded chunks. * Number of uploaded chunks.
* *
* This is set by self::upload(). Zero for single part uploads, non-zero for multipart uploads. * This is set by static::upload(). Zero for single part uploads, non-zero for multipart uploads.
* *
* @var int * @var int
*/ */
@ -59,42 +59,42 @@ class BigFiles extends AbstractTest
public static function upload5MBString(Connector $s3, array $options): bool public static function upload5MBString(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, self::FIVE_MB, 'bigtest_5mb.dat'); return static::upload($s3, $options, static::FIVE_MB, 'bigtest_5mb.dat');
} }
public static function upload6MBString(Connector $s3, array $options): bool public static function upload6MBString(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, self::SIX_MB, 'bigtest_6mb.dat'); return static::upload($s3, $options, static::SIX_MB, 'bigtest_6mb.dat');
} }
public static function upload10MBString(Connector $s3, array $options): bool public static function upload10MBString(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, self::TEN_MB, 'bigtest_10mb.dat'); return static::upload($s3, $options, static::TEN_MB, 'bigtest_10mb.dat');
} }
public static function upload11MBString(Connector $s3, array $options): bool public static function upload11MBString(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, self::ELEVEN_MB, 'bigtest_11mb.dat'); return static::upload($s3, $options, static::ELEVEN_MB, 'bigtest_11mb.dat');
} }
public static function upload5MBFile(Connector $s3, array $options): bool public static function upload5MBFile(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, self::FIVE_MB, 'bigtest_5mb.dat', false); return static::upload($s3, $options, static::FIVE_MB, 'bigtest_5mb.dat', false);
} }
public static function upload6MBFile(Connector $s3, array $options): bool public static function upload6MBFile(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, self::SIX_MB, 'bigtest_6mb.dat', false); return static::upload($s3, $options, static::SIX_MB, 'bigtest_6mb.dat', false);
} }
public static function upload10MBFile(Connector $s3, array $options): bool public static function upload10MBFile(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, self::TEN_MB, 'bigtest_10mb.dat', false); return static::upload($s3, $options, static::TEN_MB, 'bigtest_10mb.dat', false);
} }
public static function upload11MBFile(Connector $s3, array $options): bool public static function upload11MBFile(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, self::ELEVEN_MB, 'bigtest_11mb.dat', false); return static::upload($s3, $options, static::ELEVEN_MB, 'bigtest_11mb.dat', false);
} }
protected static function upload(Connector $s3, array $options, int $size, string $uri, bool $useString = true): bool protected static function upload(Connector $s3, array $options, int $size, string $uri, bool $useString = true): bool
@ -103,24 +103,24 @@ class BigFiles extends AbstractTest
$dotPos = strrpos($uri, '.'); $dotPos = strrpos($uri, '.');
$uri = substr($uri, 0, $dotPos) . '.' . md5(microtime(false)) . substr($uri, $dotPos); $uri = substr($uri, 0, $dotPos) . '.' . md5(microtime(false)) . substr($uri, $dotPos);
self::$numberOfChunks = 0; static::$numberOfChunks = 0;
if ($useString) if ($useString)
{ {
$sourceData = self::getRandomData($size); $sourceData = static::getRandomData($size);
$input = Input::createFromData($sourceData); $input = Input::createFromData($sourceData);
} }
else else
{ {
// Create a file with random data // Create a file with random data
$sourceFile = self::createFile($size); $sourceFile = static::createFile($size);
$input = Input::createFromFile($sourceFile); $input = Input::createFromFile($sourceFile);
} }
// Upload the file. Throws exception if it fails. // Upload the file. Throws exception if it fails.
$bucket = $options['bucket']; $bucket = $options['bucket'];
if (!self::$multipart) if (!static::$multipart)
{ {
$s3->putObject($input, $bucket, $uri); $s3->putObject($input, $bucket, $uri);
} }
@ -149,7 +149,7 @@ class BigFiles extends AbstractTest
$input->setEtags($eTags); $input->setEtags($eTags);
$input->setPartNumber($partNumber); $input->setPartNumber($partNumber);
$etag = $s3->uploadMultipart($input, $bucket, $uri, [], self::$uploadChunkSize); $etag = $s3->uploadMultipart($input, $bucket, $uri, [], static::$uploadChunkSize);
// If the result was null we have no more file parts to process. // If the result was null we have no more file parts to process.
if (is_null($etag)) if (is_null($etag))
@ -166,7 +166,7 @@ class BigFiles extends AbstractTest
$partNumber++; $partNumber++;
} }
self::$numberOfChunks = count($eTags); static::$numberOfChunks = count($eTags);
// Finalize the multipart upload. Tells Amazon to construct the file from the uploaded parts. // Finalize the multipart upload. Tells Amazon to construct the file from the uploaded parts.
$s3->finalizeMultipart($input, $bucket, $uri); $s3->finalizeMultipart($input, $bucket, $uri);
@ -176,7 +176,7 @@ class BigFiles extends AbstractTest
$result = true; $result = true;
// Should I download the file and compare its contents? // Should I download the file and compare its contents?
if (self::$downloadAfter) if (static::$downloadAfter)
{ {
if ($useString) if ($useString)
{ {
@ -184,16 +184,16 @@ class BigFiles extends AbstractTest
$downloadedData = $s3->getObject($bucket, $uri); $downloadedData = $s3->getObject($bucket, $uri);
// Compare the file contents. // Compare the file contents.
$result = self::areStringsEqual($sourceData, $downloadedData); $result = static::areStringsEqual($sourceData, $downloadedData);
} }
else else
{ {
// Download the data. Throws exception if it fails. // Download the data. Throws exception if it fails.
$downloadedFile = tempnam(self::getTempFolder(), 'as3'); $downloadedFile = tempnam(static::getTempFolder(), 'as3');
$s3->getObject($bucket, $uri, $downloadedFile); $s3->getObject($bucket, $uri, $downloadedFile);
// Compare the file contents. // Compare the file contents.
$result = self::areFilesEqual($sourceFile, $downloadedFile); $result = static::areFilesEqual($sourceFile, $downloadedFile);
@unlink($downloadedFile); @unlink($downloadedFile);
} }
@ -206,7 +206,7 @@ class BigFiles extends AbstractTest
} }
// Should I delete the remotely stored file? // Should I delete the remotely stored file?
if (self::$deleteRemote) if (static::$deleteRemote)
{ {
// Delete the remote file. Throws exception if it fails. // Delete the remote file. Throws exception if it fails.
$s3->deleteObject($bucket, $uri); $s3->deleteObject($bucket, $uri);

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
class BucketLocation extends AbstractTest class BucketLocation extends AbstractTest
{ {
@ -18,7 +18,7 @@ class BucketLocation extends AbstractTest
{ {
$location = $s3->getBucketLocation($options['bucket']); $location = $s3->getBucketLocation($options['bucket']);
self::assert($location === $options['region'], "Bucket {$options['bucket']} reports being in region {$location} instead of expected {$options['region']}"); static::assert($location === $options['region'], "Bucket {$options['bucket']} reports being in region {$location} instead of expected {$options['region']}");
return true; return true;
} }

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
use RuntimeException; use RuntimeException;
class BucketsList extends AbstractTest class BucketsList extends AbstractTest
@ -19,16 +19,16 @@ class BucketsList extends AbstractTest
{ {
$buckets = $s3->listBuckets(true); $buckets = $s3->listBuckets(true);
self::assert(is_array($buckets), "Detailed buckets list is not an array"); static::assert(is_array($buckets), "Detailed buckets list is not an array");
self::assert(isset($buckets['owner']), "Detailed buckets list does not list an owner"); static::assert(isset($buckets['owner']), "Detailed buckets list does not list an owner");
self::assert(isset($buckets['owner']['id']), "Detailed buckets list does not list an owner's id"); static::assert(isset($buckets['owner']['id']), "Detailed buckets list does not list an owner's id");
self::assert(isset($buckets['owner']['name']), "Detailed buckets list does not list an owner's name"); static::assert(isset($buckets['owner']['name']), "Detailed buckets list does not list an owner's name");
self::assert(isset($buckets['buckets']), "Detailed buckets list does not list any buckets"); static::assert(isset($buckets['buckets']), "Detailed buckets list does not list any buckets");
foreach ($buckets['buckets'] as $bucketInfo) foreach ($buckets['buckets'] as $bucketInfo)
{ {
self::assert(isset($bucketInfo['name']), "Bucket information does not list a name"); static::assert(isset($bucketInfo['name']), "Bucket information does not list a name");
self::assert(isset($bucketInfo['time']), "Bucket information does not list a created times"); static::assert(isset($bucketInfo['time']), "Bucket information does not list a created times");
if ($bucketInfo['name'] === $options['bucket']) if ($bucketInfo['name'] === $options['bucket'])
{ {
@ -43,8 +43,8 @@ class BucketsList extends AbstractTest
{ {
$buckets = $s3->listBuckets(false); $buckets = $s3->listBuckets(false);
self::assert(is_array($buckets), "Simple buckets list is not an array"); static::assert(is_array($buckets), "Simple buckets list is not an array");
self::assert(in_array($options['bucket'], $buckets), "Simple buckets list does not include configured bucket {$options['bucket']}"); static::assert(in_array($options['bucket'], $buckets), "Simple buckets list does not include configured bucket {$options['bucket']}");
return true; return true;
} }

View file

@ -0,0 +1,67 @@
<?php
/**
* Akeeba Engine
*
* @package akeebaengine
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\MiniTest\Test;
use Akeeba\S3\Connector;
use Akeeba\S3\Exception\CannotDeleteFile;
use Akeeba\S3\Exception\CannotGetFile;
use Akeeba\S3\Input;
class HeadObject extends AbstractTest
{
public static function testExistingFile(Connector $s3, array $options): bool
{
$uri = 'head_test.dat';
// Randomize the name. Required for archive buckets where you cannot overwrite data.
$dotPos = strrpos($uri, '.');
$uri = substr($uri, 0, $dotPos) . '.' . md5(microtime(false)) . substr($uri, $dotPos);
// Create a file with random data
$sourceFile = static::createFile(AbstractTest::TEN_KB);
// Upload the file. Throws exception if it fails.
$bucket = $options['bucket'];
$input = Input::createFromFile($sourceFile);
$s3->putObject($input, $bucket, $uri);
$headers = $s3->headObject($bucket, $uri);
static::assert(isset($headers['size']), 'The returned headers do not contain the object size');
static::assert($headers['size'] == AbstractTest::TEN_KB, 'The returned size does not match');
// Remove the local files
@unlink($sourceFile);
// Delete the remote file. Throws exception if it fails.
$s3->deleteObject($bucket, $uri);
return true;
}
public static function testMissingFile(Connector $s3, array $options): bool
{
$bucket = $options['bucket'];
try
{
$headers = $s3->headObject($bucket, md5(microtime(false)) . '_does_not_exist');
}
catch (CannotGetFile $e)
{
return true;
}
return false;
}
}

View file

@ -3,16 +3,16 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotPutFile; use Akeeba\S3\Exception\CannotPutFile;
use Akeeba\Engine\Postproc\Connector\S3v4\Input; use Akeeba\S3\Input;
class ListFiles extends AbstractTest class ListFiles extends AbstractTest
{ {
@ -34,9 +34,9 @@ class ListFiles extends AbstractTest
public static function setup(Connector $s3, array $options): void public static function setup(Connector $s3, array $options): void
{ {
$data = self::getRandomData(self::TEN_KB); $data = static::getRandomData(static::TEN_KB);
foreach (self::$paths as $uri) foreach (static::$paths as $uri)
{ {
$input = Input::createFromData($data); $input = Input::createFromData($data);
try try
@ -52,7 +52,7 @@ class ListFiles extends AbstractTest
public static function teardown(Connector $s3, array $options): void public static function teardown(Connector $s3, array $options): void
{ {
foreach (self::$paths as $uri) foreach (static::$paths as $uri)
{ {
try try
{ {
@ -69,29 +69,29 @@ class ListFiles extends AbstractTest
{ {
$listing = $s3->getBucket($options['bucket'], 'listtest_'); $listing = $s3->getBucket($options['bucket'], 'listtest_');
self::assert(is_array($listing), "The files listing must be an array"); static::assert(is_array($listing), "The files listing must be an array");
self::assert(count($listing) == 3, "I am expecting to see 3 files"); static::assert(count($listing) == 3, "I am expecting to see 3 files");
// Make sure I have the expected files // Make sure I have the expected files
self::assert(array_key_exists('listtest_one.dat', $listing), "File listtest_one.dat not in listing"); static::assert(array_key_exists('listtest_one.dat', $listing), "File listtest_one.dat not in listing");
self::assert(array_key_exists('listtest_two.dat', $listing), "File listtest_two.dat not in listing"); static::assert(array_key_exists('listtest_two.dat', $listing), "File listtest_two.dat not in listing");
self::assert(array_key_exists('listtest_three.dat', $listing), "File listtest_three.dat not in listing"); static::assert(array_key_exists('listtest_three.dat', $listing), "File listtest_three.dat not in listing");
// I must not see the files in subdirectories // I must not see the files in subdirectories
self::assert(!array_key_exists('listtest_four.dat', $listing), "File listtest_four.dat in listing"); static::assert(!array_key_exists('listtest_four.dat', $listing), "File listtest_four.dat in listing");
self::assert(!array_key_exists('listtest_five.dat', $listing), "File listtest_five.dat in listing"); static::assert(!array_key_exists('listtest_five.dat', $listing), "File listtest_five.dat in listing");
self::assert(!array_key_exists('listtest_six.dat', $listing), "File listtest_six.dat in listing"); static::assert(!array_key_exists('listtest_six.dat', $listing), "File listtest_six.dat in listing");
// I must not see the files not matching the prefix I gave // I must not see the files not matching the prefix I gave
self::assert(!array_key_exists('spam.dat', $listing), "File spam.dat in listing"); static::assert(!array_key_exists('spam.dat', $listing), "File spam.dat in listing");
self::assert(!array_key_exists('ham.dat', $listing), "File ham.dat in listing"); static::assert(!array_key_exists('ham.dat', $listing), "File ham.dat in listing");
foreach ($listing as $fileName => $info) foreach ($listing as $fileName => $info)
{ {
self::assert(isset($info['name']), "File entries must have a name"); static::assert(isset($info['name']), "File entries must have a name");
self::assert(isset($info['time']), "File entries must have a time"); static::assert(isset($info['time']), "File entries must have a time");
self::assert(isset($info['size']), "File entries must have a size"); static::assert(isset($info['size']), "File entries must have a size");
self::assert(isset($info['hash']), "File entries must have a hash"); static::assert(isset($info['hash']), "File entries must have a hash");
} }
return true; return true;
@ -101,37 +101,37 @@ class ListFiles extends AbstractTest
{ {
$listing = $s3->getBucket($options['bucket'], 'listtest_', null, 1); $listing = $s3->getBucket($options['bucket'], 'listtest_', null, 1);
self::assert(is_array($listing), "The files listing must be an array"); static::assert(is_array($listing), "The files listing must be an array");
self::assert(count($listing) == 1, sprintf("I am expecting to see 1 file, %s seen", count($listing))); static::assert(count($listing) == 1, sprintf("I am expecting to see 1 file, %s seen", count($listing)));
$files = array_keys($listing); $files = array_keys($listing);
$continued = $s3->getBucket($options['bucket'], 'listtest_', array_shift($files)); $continued = $s3->getBucket($options['bucket'], 'listtest_', array_shift($files));
self::assert(is_array($continued), "The continued files listing must be an array"); static::assert(is_array($continued), "The continued files listing must be an array");
self::assert(count($continued) == 2, sprintf("I am expecting to see 2 files, %s seen", count($continued))); static::assert(count($continued) == 2, sprintf("I am expecting to see 2 files, %s seen", count($continued)));
$listing = array_merge($listing, $continued); $listing = array_merge($listing, $continued);
// Make sure I have the expected files // Make sure I have the expected files
self::assert(array_key_exists('listtest_one.dat', $listing), "File listtest_one.dat not in listing"); static::assert(array_key_exists('listtest_one.dat', $listing), "File listtest_one.dat not in listing");
self::assert(array_key_exists('listtest_two.dat', $listing), "File listtest_two.dat not in listing"); static::assert(array_key_exists('listtest_two.dat', $listing), "File listtest_two.dat not in listing");
self::assert(array_key_exists('listtest_three.dat', $listing), "File listtest_three.dat not in listing"); static::assert(array_key_exists('listtest_three.dat', $listing), "File listtest_three.dat not in listing");
// I must not see the files in subdirectories // I must not see the files in subdirectories
self::assert(!array_key_exists('listtest_four.dat', $listing), "File listtest_four.dat in listing"); static::assert(!array_key_exists('listtest_four.dat', $listing), "File listtest_four.dat in listing");
self::assert(!array_key_exists('listtest_five.dat', $listing), "File listtest_five.dat in listing"); static::assert(!array_key_exists('listtest_five.dat', $listing), "File listtest_five.dat in listing");
self::assert(!array_key_exists('listtest_six.dat', $listing), "File listtest_six.dat in listing"); static::assert(!array_key_exists('listtest_six.dat', $listing), "File listtest_six.dat in listing");
// I must not see the files not matching the prefix I gave // I must not see the files not matching the prefix I gave
self::assert(!array_key_exists('spam.dat', $listing), "File spam.dat in listing"); static::assert(!array_key_exists('spam.dat', $listing), "File spam.dat in listing");
self::assert(!array_key_exists('ham.dat', $listing), "File ham.dat in listing"); static::assert(!array_key_exists('ham.dat', $listing), "File ham.dat in listing");
foreach ($listing as $fileName => $info) foreach ($listing as $fileName => $info)
{ {
self::assert(isset($info['name']), "File entries must have a name"); static::assert(isset($info['name']), "File entries must have a name");
self::assert(isset($info['time']), "File entries must have a time"); static::assert(isset($info['time']), "File entries must have a time");
self::assert(isset($info['size']), "File entries must have a size"); static::assert(isset($info['size']), "File entries must have a size");
self::assert(isset($info['hash']), "File entries must have a hash"); static::assert(isset($info['hash']), "File entries must have a hash");
} }
return true; return true;
@ -141,30 +141,30 @@ class ListFiles extends AbstractTest
{ {
$listing = $s3->getBucket($options['bucket'], 'list_deeper/test_'); $listing = $s3->getBucket($options['bucket'], 'list_deeper/test_');
self::assert(is_array($listing), "The files listing must be an array"); static::assert(is_array($listing), "The files listing must be an array");
self::assert(count($listing) == 3, "I am expecting to see 3 files"); static::assert(count($listing) == 3, "I am expecting to see 3 files");
// Make sure I have the expected files // Make sure I have the expected files
self::assert(array_key_exists('list_deeper/test_one.dat', $listing), "File test_one.dat not in listing"); static::assert(array_key_exists('list_deeper/test_one.dat', $listing), "File test_one.dat not in listing");
self::assert(array_key_exists('list_deeper/test_two.dat', $listing), "File test_two.dat not in listing"); static::assert(array_key_exists('list_deeper/test_two.dat', $listing), "File test_two.dat not in listing");
self::assert(array_key_exists('list_deeper/test_three.dat', $listing), "File test_three.dat not in listing"); static::assert(array_key_exists('list_deeper/test_three.dat', $listing), "File test_three.dat not in listing");
// I must not see the files with different prefix // I must not see the files with different prefix
self::assert(!array_key_exists('list_deeper/listtest_four.dat', $listing), "File listtest_four.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_four.dat', $listing), "File listtest_four.dat in listing");
self::assert(!array_key_exists('list_deeper/listtest_five.dat', $listing), "File listtest_five.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_five.dat', $listing), "File listtest_five.dat in listing");
self::assert(!array_key_exists('list_deeper/listtest_six.dat', $listing), "File listtest_six.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_six.dat', $listing), "File listtest_six.dat in listing");
self::assert(!array_key_exists('list_deeper/spam.dat', $listing), "File spam.dat in listing"); static::assert(!array_key_exists('list_deeper/spam.dat', $listing), "File spam.dat in listing");
// I must not see the files in subdirectories // I must not see the files in subdirectories
self::assert(!array_key_exists('list_deeper/listtest_deeper/seven.dat', $listing), "File spam.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_deeper/seven.dat', $listing), "File spam.dat in listing");
self::assert(!array_key_exists('list_deeper/listtest_deeper/eight.dat', $listing), "File spam.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_deeper/eight.dat', $listing), "File spam.dat in listing");
foreach ($listing as $fileName => $info) foreach ($listing as $fileName => $info)
{ {
self::assert(isset($info['name']), "File entries must have a name"); static::assert(isset($info['name']), "File entries must have a name");
self::assert(isset($info['time']), "File entries must have a time"); static::assert(isset($info['time']), "File entries must have a time");
self::assert(isset($info['size']), "File entries must have a size"); static::assert(isset($info['size']), "File entries must have a size");
self::assert(isset($info['hash']), "File entries must have a hash"); static::assert(isset($info['hash']), "File entries must have a hash");
} }
return true; return true;
@ -174,41 +174,41 @@ class ListFiles extends AbstractTest
{ {
$listing = $s3->getBucket($options['bucket'], 'list_deeper/test_', null, 1); $listing = $s3->getBucket($options['bucket'], 'list_deeper/test_', null, 1);
self::assert(is_array($listing), "The files listing must be an array"); static::assert(is_array($listing), "The files listing must be an array");
self::assert(count($listing) == 1, sprintf("I am expecting to see 1 file, %s seen", count($listing))); static::assert(count($listing) == 1, sprintf("I am expecting to see 1 file, %s seen", count($listing)));
$files = array_keys($listing); $files = array_keys($listing);
$continued = $s3->getBucket($options['bucket'], 'list_deeper/test_', array_shift($files)); $continued = $s3->getBucket($options['bucket'], 'list_deeper/test_', array_shift($files));
self::assert(is_array($continued), "The continued files listing must be an array"); static::assert(is_array($continued), "The continued files listing must be an array");
self::assert(count($continued) == 2, sprintf("I am expecting to see 2 files, %s seen", count($continued))); static::assert(count($continued) == 2, sprintf("I am expecting to see 2 files, %s seen", count($continued)));
$listing = array_merge($listing, $continued); $listing = array_merge($listing, $continued);
self::assert(is_array($listing), "The files listing must be an array"); static::assert(is_array($listing), "The files listing must be an array");
self::assert(count($listing) == 3, "I am expecting to see 3 files"); static::assert(count($listing) == 3, "I am expecting to see 3 files");
// Make sure I have the expected files // Make sure I have the expected files
self::assert(array_key_exists('list_deeper/test_one.dat', $listing), "File test_one.dat not in listing"); static::assert(array_key_exists('list_deeper/test_one.dat', $listing), "File test_one.dat not in listing");
self::assert(array_key_exists('list_deeper/test_two.dat', $listing), "File test_two.dat not in listing"); static::assert(array_key_exists('list_deeper/test_two.dat', $listing), "File test_two.dat not in listing");
self::assert(array_key_exists('list_deeper/test_three.dat', $listing), "File test_three.dat not in listing"); static::assert(array_key_exists('list_deeper/test_three.dat', $listing), "File test_three.dat not in listing");
// I must not see the files with different prefix // I must not see the files with different prefix
self::assert(!array_key_exists('list_deeper/listtest_four.dat', $listing), "File listtest_four.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_four.dat', $listing), "File listtest_four.dat in listing");
self::assert(!array_key_exists('list_deeper/listtest_five.dat', $listing), "File listtest_five.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_five.dat', $listing), "File listtest_five.dat in listing");
self::assert(!array_key_exists('list_deeper/listtest_six.dat', $listing), "File listtest_six.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_six.dat', $listing), "File listtest_six.dat in listing");
self::assert(!array_key_exists('list_deeper/spam.dat', $listing), "File spam.dat in listing"); static::assert(!array_key_exists('list_deeper/spam.dat', $listing), "File spam.dat in listing");
// I must not see the files in subdirectories // I must not see the files in subdirectories
self::assert(!array_key_exists('list_deeper/listtest_deeper/seven.dat', $listing), "File spam.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_deeper/seven.dat', $listing), "File spam.dat in listing");
self::assert(!array_key_exists('list_deeper/listtest_deeper/eight.dat', $listing), "File spam.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_deeper/eight.dat', $listing), "File spam.dat in listing");
foreach ($listing as $fileName => $info) foreach ($listing as $fileName => $info)
{ {
self::assert(isset($info['name']), "File entries must have a name"); static::assert(isset($info['name']), "File entries must have a name");
self::assert(isset($info['time']), "File entries must have a time"); static::assert(isset($info['time']), "File entries must have a time");
self::assert(isset($info['size']), "File entries must have a size"); static::assert(isset($info['size']), "File entries must have a size");
self::assert(isset($info['hash']), "File entries must have a hash"); static::assert(isset($info['hash']), "File entries must have a hash");
} }
return true; return true;
@ -224,42 +224,42 @@ class ListFiles extends AbstractTest
*/ */
$listing = $s3->getBucket($options['bucket'], 'list_deeper/listtest_', null, 1); $listing = $s3->getBucket($options['bucket'], 'list_deeper/listtest_', null, 1);
self::assert(is_array($listing), "The files listing must be an array"); static::assert(is_array($listing), "The files listing must be an array");
self::assert(count($listing) == 1, sprintf("I am expecting to see 1 files, %s seen", count($listing))); static::assert(count($listing) == 1, sprintf("I am expecting to see 1 files, %s seen", count($listing)));
$files = array_keys($listing); $files = array_keys($listing);
$continued = $s3->getBucket($options['bucket'], 'list_deeper/listtest_', array_shift($files)); $continued = $s3->getBucket($options['bucket'], 'list_deeper/listtest_', array_shift($files));
self::assert(is_array($continued), "The continued files listing must be an array"); static::assert(is_array($continued), "The continued files listing must be an array");
self::assert(count($continued) == 2, sprintf("I am expecting to see 2 files, %s seen", count($continued))); static::assert(count($continued) == 2, sprintf("I am expecting to see 2 files, %s seen", count($continued)));
$listing = array_merge($listing, $continued); $listing = array_merge($listing, $continued);
self::assert(is_array($listing), "The files listing must be an array"); static::assert(is_array($listing), "The files listing must be an array");
self::assert(count($listing) == 3, "I am expecting to see 3 files"); static::assert(count($listing) == 3, "I am expecting to see 3 files");
// Make sure I have the expected files // Make sure I have the expected files
self::assert(array_key_exists('list_deeper/listtest_four.dat', $listing), "File listtest_four.dat not in listing"); static::assert(array_key_exists('list_deeper/listtest_four.dat', $listing), "File listtest_four.dat not in listing");
self::assert(array_key_exists('list_deeper/listtest_five.dat', $listing), "File listtest_five.dat not in listing"); static::assert(array_key_exists('list_deeper/listtest_five.dat', $listing), "File listtest_five.dat not in listing");
self::assert(array_key_exists('list_deeper/listtest_six.dat', $listing), "File listtest_six.dat not in listing"); static::assert(array_key_exists('list_deeper/listtest_six.dat', $listing), "File listtest_six.dat not in listing");
// I must not see the files with different prefix // I must not see the files with different prefix
self::assert(!array_key_exists('list_deeper/test_one.dat', $listing), "File test_one.dat in listing"); static::assert(!array_key_exists('list_deeper/test_one.dat', $listing), "File test_one.dat in listing");
self::assert(!array_key_exists('list_deeper/test_two.dat', $listing), "File test_two.dat in listing"); static::assert(!array_key_exists('list_deeper/test_two.dat', $listing), "File test_two.dat in listing");
self::assert(!array_key_exists('list_deeper/test_three.dat', $listing), "File test_three.dat in listing"); static::assert(!array_key_exists('list_deeper/test_three.dat', $listing), "File test_three.dat in listing");
self::assert(!array_key_exists('list_deeper/spam.dat', $listing), "File spam.dat in listing"); static::assert(!array_key_exists('list_deeper/spam.dat', $listing), "File spam.dat in listing");
// I must not see the files in subdirectories // I must not see the files in subdirectories
self::assert(!array_key_exists('list_deeper/listtest_deeper/seven.dat', $listing), "File spam.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_deeper/seven.dat', $listing), "File spam.dat in listing");
self::assert(!array_key_exists('list_deeper/listtest_deeper/eight.dat', $listing), "File spam.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_deeper/eight.dat', $listing), "File spam.dat in listing");
foreach ($listing as $fileName => $info) foreach ($listing as $fileName => $info)
{ {
self::assert(isset($info['name']), "File entries must have a name"); static::assert(isset($info['name']), "File entries must have a name");
self::assert(isset($info['time']), "File entries must have a time"); static::assert(isset($info['time']), "File entries must have a time");
self::assert(isset($info['size']), "File entries must have a size"); static::assert(isset($info['size']), "File entries must have a size");
self::assert(isset($info['hash']), "File entries must have a hash"); static::assert(isset($info['hash']), "File entries must have a hash");
} }
return true; return true;
@ -269,37 +269,37 @@ class ListFiles extends AbstractTest
{ {
$listing = $s3->getBucket($options['bucket'], 'list_deeper/listtest_', null, null, '/', true); $listing = $s3->getBucket($options['bucket'], 'list_deeper/listtest_', null, null, '/', true);
self::assert(is_array($listing), "The files listing must be an array"); static::assert(is_array($listing), "The files listing must be an array");
self::assert(count($listing) == 4, sprintf("I am expecting to see 4 entries, %s entries seen.", count($listing))); static::assert(count($listing) == 4, sprintf("I am expecting to see 4 entries, %s entries seen.", count($listing)));
// Make sure I have the expected files // Make sure I have the expected files
self::assert(array_key_exists('list_deeper/listtest_four.dat', $listing), "File listtest_four.dat not in listing"); static::assert(array_key_exists('list_deeper/listtest_four.dat', $listing), "File listtest_four.dat not in listing");
self::assert(array_key_exists('list_deeper/listtest_five.dat', $listing), "File listtest_five.dat not in listing"); static::assert(array_key_exists('list_deeper/listtest_five.dat', $listing), "File listtest_five.dat not in listing");
self::assert(array_key_exists('list_deeper/listtest_six.dat', $listing), "File listtest_six.dat not in listing"); static::assert(array_key_exists('list_deeper/listtest_six.dat', $listing), "File listtest_six.dat not in listing");
self::assert(array_key_exists('list_deeper/listtest_deeper/', $listing), "Folder listtest_deeper not in listing"); static::assert(array_key_exists('list_deeper/listtest_deeper/', $listing), "Folder listtest_deeper not in listing");
// I must not see the files in subdirectories // I must not see the files in subdirectories
self::assert(!array_key_exists('list_deeper/listtest_deeper/seven.dat', $listing), "File seven.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_deeper/seven.dat', $listing), "File seven.dat in listing");
self::assert(!array_key_exists('list_deeper/listtest_deeper/eight.dat', $listing), "File eight.dat in listing"); static::assert(!array_key_exists('list_deeper/listtest_deeper/eight.dat', $listing), "File eight.dat in listing");
// I must not see the files with different prefix // I must not see the files with different prefix
self::assert(!array_key_exists('list_deeper/spam.dat', $listing), "File spam.dat in listing"); static::assert(!array_key_exists('list_deeper/spam.dat', $listing), "File spam.dat in listing");
self::assert(!array_key_exists('list_deeper/test_one.dat', $listing), "File test_one.dat not in listing"); static::assert(!array_key_exists('list_deeper/test_one.dat', $listing), "File test_one.dat not in listing");
self::assert(!array_key_exists('list_deeper/test_two.dat', $listing), "File test_two.dat not in listing"); static::assert(!array_key_exists('list_deeper/test_two.dat', $listing), "File test_two.dat not in listing");
self::assert(!array_key_exists('list_deeper/test_three.dat', $listing), "File test_three.dat not in listing"); static::assert(!array_key_exists('list_deeper/test_three.dat', $listing), "File test_three.dat not in listing");
foreach ($listing as $fileName => $info) foreach ($listing as $fileName => $info)
{ {
if (substr($fileName, -1) !== '/') if (substr($fileName, -1) !== '/')
{ {
self::assert(isset($info['name']), "File entries must have a name"); static::assert(isset($info['name']), "File entries must have a name");
self::assert(isset($info['time']), "File entries must have a time"); static::assert(isset($info['time']), "File entries must have a time");
self::assert(isset($info['size']), "File entries must have a size"); static::assert(isset($info['size']), "File entries must have a size");
self::assert(isset($info['hash']), "File entries must have a hash"); static::assert(isset($info['hash']), "File entries must have a hash");
} }
else else
{ {
self::assert(isset($info['prefix']), "Folder entries must return a prefix"); static::assert(isset($info['prefix']), "Folder entries must return a prefix");
} }
} }

View file

@ -0,0 +1,111 @@
<?php
/**
* Akeeba Engine
*
* @package akeebaengine
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\MiniTest\Test;
use Akeeba\S3\Connector;
use Akeeba\S3\Input;
class ListThousandsOfFiles extends AbstractTest
{
private const PATH_PREFIX = 'massive/';
public static function setup(Connector $s3, array $options): void
{
if (defined('CREATE_2100_FILES') && CREATE_2100_FILES === false)
{
return;
}
$data = static::getRandomData(128);
echo "\nPopulating with 2100 files\n";
for ($i = 1; $i <= 2100; $i++)
{
if ($i % 10 === 0)
{
echo "Uploading from $i...\n";
}
$uri = sprintf('%stest_%04u.dat', static::PATH_PREFIX, $i);
$input = Input::createFromData($data);
$s3->putObject($input, $options['bucket'], $uri);
}
}
public static function testGetAll(Connector $s3, array $options): bool
{
$listing = $s3->getBucket($options['bucket'], static::PATH_PREFIX);
static::assert(is_array($listing), "The files listing must be an array");
static::assert(count($listing) === 2100, "I am expecting to see 2100 files");
for ($i = 1; $i <= 2100; $i++)
{
$key = sprintf('%stest_%04u.dat', static::PATH_PREFIX, $i);
static::assert(array_key_exists($key, $listing), sprintf('Results should list object %s', $key));
}
return true;
}
public static function testGetHundred(Connector $s3, array $options): bool
{
$listing = $s3->getBucket($options['bucket'], static::PATH_PREFIX, null, 100);
static::assert(is_array($listing), "The files listing must be an array");
static::assert(count($listing) === 100, "I am expecting to see 100 files");
for ($i = 1; $i <= 100; $i++)
{
$key = sprintf('%stest_%04u.dat', static::PATH_PREFIX, $i);
static::assert(array_key_exists($key, $listing), sprintf('Results should list object %s', $key));
}
return true;
}
public static function testGetElevenHundred(Connector $s3, array $options): bool
{
$listing = $s3->getBucket($options['bucket'], static::PATH_PREFIX, null, 1100);
static::assert(is_array($listing), "The files listing must be an array");
static::assert(count($listing) === 1100, "I am expecting to see 1100 files");
for ($i = 1; $i <= 1100; $i++)
{
$key = sprintf('%stest_%04u.dat', static::PATH_PREFIX, $i);
static::assert(array_key_exists($key, $listing), sprintf('Results should list object %s', $key));
}
return true;
}
public static function testGetLastHundred(Connector $s3, array $options): bool
{
$listing = $s3->getBucket($options['bucket'], static::PATH_PREFIX . 'test_20', null);
static::assert(is_array($listing), "The files listing must be an array");
static::assert(count($listing) === 100, "I am expecting to see 100 files");
for ($i = 2000; $i <= 2099; $i++)
{
$key = sprintf('%stest_%04u.dat', static::PATH_PREFIX, $i);
static::assert(array_key_exists($key, $listing), sprintf('Results should list object %s', $key));
}
return true;
}
}

View file

@ -1,16 +1,22 @@
<?php <?php
/**
* Akeeba Engine
*
* @package akeebaengine
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
class Multipart extends BigFiles class Multipart extends BigFiles
{ {
public static function setup(Connector $s3, array $options): void public static function setup(Connector $s3, array $options): void
{ {
self::$multipart = true; static::$multipart = true;
parent::setup($s3, $options); parent::setup($s3, $options);
} }
@ -20,7 +26,7 @@ class Multipart extends BigFiles
$result = parent::upload5MBString($s3, $options); $result = parent::upload5MBString($s3, $options);
$expectedChunks = 1; $expectedChunks = 1;
self::assert(self::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, self::$numberOfChunks)); static::assert(static::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, static::$numberOfChunks));
return $result; return $result;
} }
@ -30,7 +36,7 @@ class Multipart extends BigFiles
$result = parent::upload6MBString($s3, $options); $result = parent::upload6MBString($s3, $options);
$expectedChunks = 2; $expectedChunks = 2;
self::assert(self::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, self::$numberOfChunks)); static::assert(static::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, static::$numberOfChunks));
return $result; return $result;
} }
@ -40,7 +46,7 @@ class Multipart extends BigFiles
$result = parent::upload10MBString($s3, $options); $result = parent::upload10MBString($s3, $options);
$expectedChunks = 2; $expectedChunks = 2;
self::assert(self::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, self::$numberOfChunks)); static::assert(static::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, static::$numberOfChunks));
return $result; return $result;
} }
@ -50,7 +56,7 @@ class Multipart extends BigFiles
$result = parent::upload11MBString($s3, $options); $result = parent::upload11MBString($s3, $options);
$expectedChunks = 3; $expectedChunks = 3;
self::assert(self::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, self::$numberOfChunks)); static::assert(static::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, static::$numberOfChunks));
return $result; return $result;
} }
@ -60,7 +66,7 @@ class Multipart extends BigFiles
$result = parent::upload5MBFile($s3, $options); $result = parent::upload5MBFile($s3, $options);
$expectedChunks = 1; $expectedChunks = 1;
self::assert(self::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, self::$numberOfChunks)); static::assert(static::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, static::$numberOfChunks));
return $result; return $result;
} }
@ -70,7 +76,7 @@ class Multipart extends BigFiles
$result = parent::upload6MBFile($s3, $options); $result = parent::upload6MBFile($s3, $options);
$expectedChunks = 2; $expectedChunks = 2;
self::assert(self::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, self::$numberOfChunks)); static::assert(static::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, static::$numberOfChunks));
return $result; return $result;
} }
@ -80,7 +86,7 @@ class Multipart extends BigFiles
$result = parent::upload10MBFile($s3, $options); $result = parent::upload10MBFile($s3, $options);
$expectedChunks = 2; $expectedChunks = 2;
self::assert(self::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, self::$numberOfChunks)); static::assert(static::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, static::$numberOfChunks));
return $result; return $result;
} }
@ -90,7 +96,7 @@ class Multipart extends BigFiles
$result = parent::upload11MBFile($s3, $options); $result = parent::upload11MBFile($s3, $options);
$expectedChunks = 3; $expectedChunks = 3;
self::assert(self::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, self::$numberOfChunks)); static::assert(static::$numberOfChunks === $expectedChunks, sprintf("Expected %s chunks, upload complete in %s chunks", $expectedChunks, static::$numberOfChunks));
return $result; return $result;
} }

View file

@ -3,33 +3,33 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Acl; use Akeeba\S3\Acl;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
use Akeeba\Engine\Postproc\Connector\S3v4\Input; use Akeeba\S3\Input;
use RuntimeException; use RuntimeException;
class SignedURLs extends AbstractTest class SignedURLs extends AbstractTest
{ {
public static function signedURLPublicObject(Connector $s3, array $options): bool public static function signedURLPublicObject(Connector $s3, array $options): bool
{ {
return self::signedURL($s3, $options, Acl::ACL_PUBLIC_READ); return static::signedURL($s3, $options, Acl::ACL_PUBLIC_READ);
} }
public static function signedURLPrivateObject(Connector $s3, array $options): bool public static function signedURLPrivateObject(Connector $s3, array $options): bool
{ {
return self::signedURL($s3, $options, Acl::ACL_PRIVATE); return static::signedURL($s3, $options, Acl::ACL_PRIVATE);
} }
private static function signedURL(Connector $s3, array $options, string $aclPrivilege): bool private static function signedURL(Connector $s3, array $options, string $aclPrivilege): bool
{ {
$tempData = self::getRandomData(AbstractTest::TEN_KB); $tempData = static::getRandomData(AbstractTest::TEN_KB);
$input = Input::createFromData($tempData); $input = Input::createFromData($tempData);
$uri = 'test.' . md5(microtime(false)) . '.dat'; $uri = 'test.' . md5(microtime(false)) . '.dat';
@ -52,7 +52,7 @@ class SignedURLs extends AbstractTest
throw new RuntimeException("Failed to download from signed URL {$downloadURL}"); throw new RuntimeException("Failed to download from signed URL {$downloadURL}");
} }
self::assert(self::areStringsEqual($tempData, $downloadedData), "Wrong data received from signed URL {$downloadURL}"); static::assert(static::areStringsEqual($tempData, $downloadedData), "Wrong data received from signed URL {$downloadURL}");
return true; return true;
} }

View file

@ -0,0 +1,43 @@
<?php
/**
* Akeeba Engine
*
* @package akeebaengine
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\MiniTest\Test;
use Akeeba\S3\Connector;
use Akeeba\S3\Input;
/**
* Upload, download and delete small files (under 1MB) using a string source
*
* @package Akeeba\MiniTest\Test
*/
class SingleSmallFile extends AbstractTest
{
public static function upload(Connector $s3, array $options): bool
{
$uri = 'test.txt';
$sourceData = <<< TEXT
This is a small text file.
TEXT;
// Upload the data. Throws exception if it fails.
$bucket = $options['bucket'];
$input = Input::createFromData($sourceData);
$s3->putObject($input, $bucket, $uri);
$downloadedData = $s3->getObject($bucket, $uri);
$result = static::areStringsEqual($sourceData, $downloadedData);
$s3->deleteObject($bucket, $uri);
return $result ?? true;
}
}

View file

@ -3,15 +3,15 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
use Akeeba\Engine\Postproc\Connector\S3v4\Input; use Akeeba\S3\Input;
/** /**
* Upload, download and delete small files (under 1MB) using a file source * Upload, download and delete small files (under 1MB) using a file source
@ -36,32 +36,32 @@ class SmallFiles extends AbstractTest
public static function upload10KbRoot(Connector $s3, array $options): bool public static function upload10KbRoot(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, AbstractTest::TEN_KB, 'root_10kb.dat'); return static::upload($s3, $options, AbstractTest::TEN_KB, 'root_10kb.dat');
} }
public static function upload10KbRootGreek(Connector $s3, array $options): bool public static function upload10KbRootGreek(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, AbstractTest::TEN_KB, οκιμή_10kb.dat'); return static::upload($s3, $options, AbstractTest::TEN_KB, οκιμή_10kb.dat');
} }
public static function upload10KbFolderGreek(Connector $s3, array $options): bool public static function upload10KbFolderGreek(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, AbstractTest::TEN_KB, 'ο_φάκελός_μουοκιμή_10kb.dat'); return static::upload($s3, $options, AbstractTest::TEN_KB, 'ο_φάκελός_μουοκιμή_10kb.dat');
} }
public static function upload600KbRoot(Connector $s3, array $options): bool public static function upload600KbRoot(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, AbstractTest::SIX_HUNDRED_KB, 'root_600kb.dat'); return static::upload($s3, $options, AbstractTest::SIX_HUNDRED_KB, 'root_600kb.dat');
} }
public static function upload10KbFolder(Connector $s3, array $options): bool public static function upload10KbFolder(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, AbstractTest::TEN_KB, 'my_folder/10kb.dat'); return static::upload($s3, $options, AbstractTest::TEN_KB, 'my_folder/10kb.dat');
} }
public static function upload600KbFolder(Connector $s3, array $options): bool public static function upload600KbFolder(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, AbstractTest::SIX_HUNDRED_KB, 'my_folder/600kb.dat'); return static::upload($s3, $options, AbstractTest::SIX_HUNDRED_KB, 'my_folder/600kb.dat');
} }
protected static function upload(Connector $s3, array $options, int $size, string $uri): bool protected static function upload(Connector $s3, array $options, int $size, string $uri): bool
@ -71,7 +71,7 @@ class SmallFiles extends AbstractTest
$uri = substr($uri, 0, $dotPos) . '.' . md5(microtime(false)) . substr($uri, $dotPos); $uri = substr($uri, 0, $dotPos) . '.' . md5(microtime(false)) . substr($uri, $dotPos);
// Create a file with random data // Create a file with random data
$sourceFile = self::createFile($size); $sourceFile = static::createFile($size);
// Upload the file. Throws exception if it fails. // Upload the file. Throws exception if it fails.
$bucket = $options['bucket']; $bucket = $options['bucket'];
@ -83,14 +83,14 @@ class SmallFiles extends AbstractTest
$result = true; $result = true;
// Should I download the file and compare its contents? // Should I download the file and compare its contents?
if (self::$downloadAfter) if (static::$downloadAfter)
{ {
// Donwload the data. Throws exception if it fails. // Donwload the data. Throws exception if it fails.
$downloadedFile = tempnam(self::getTempFolder(), 'as3'); $downloadedFile = tempnam(static::getTempFolder(), 'as3');
$s3->getObject($bucket, $uri, $downloadedFile); $s3->getObject($bucket, $uri, $downloadedFile);
// Compare the file contents. // Compare the file contents.
$result = self::areFilesEqual($sourceFile, $downloadedFile); $result = static::areFilesEqual($sourceFile, $downloadedFile);
} }
// Remove the local files // Remove the local files
@ -98,7 +98,7 @@ class SmallFiles extends AbstractTest
@unlink($downloadedFile); @unlink($downloadedFile);
// Should I delete the remotely stored file? // Should I delete the remotely stored file?
if (self::$deleteRemote) if (static::$deleteRemote)
{ {
// Delete the remote file. Throws exception if it fails. // Delete the remote file. Throws exception if it fails.
$s3->deleteObject($bucket, $uri); $s3->deleteObject($bucket, $uri);

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
/** /**
* Upload and download small files (under 1MB) using a file source * Upload and download small files (under 1MB) using a file source
@ -21,7 +21,7 @@ class SmallFilesNoDelete extends SmallFiles
{ {
public static function setup(Connector $s3, array $options): void public static function setup(Connector $s3, array $options): void
{ {
self::$deleteRemote = false; static::$deleteRemote = false;
parent::setup($s3, $options); parent::setup($s3, $options);
} }

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
/** /**
* Upload small files (under 1MB) using a file source * Upload small files (under 1MB) using a file source
@ -21,8 +21,8 @@ class SmallFilesOnlyUpload extends SmallFiles
{ {
public static function setup(Connector $s3, array $options): void public static function setup(Connector $s3, array $options): void
{ {
self::$deleteRemote = false; static::$deleteRemote = false;
self::$downloadAfter = false; static::$downloadAfter = false;
parent::setup($s3, $options); parent::setup($s3, $options);
} }

View file

@ -3,15 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\S3\Connector;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Input;
use Akeeba\Engine\Postproc\Connector\S3v4\Input;
/** /**
* Upload, download and delete small files (under 1MB) using a string source * Upload, download and delete small files (under 1MB) using a string source
@ -27,7 +26,7 @@ class SmallInlineFiles extends SmallFiles
$uri = substr($uri, 0, $dotPos) . '.' . md5(microtime(false)) . substr($uri, $dotPos); $uri = substr($uri, 0, $dotPos) . '.' . md5(microtime(false)) . substr($uri, $dotPos);
// Create some random data to upload // Create some random data to upload
$sourceData = self::getRandomData($size); $sourceData = static::getRandomData($size);
// Upload the data. Throws exception if it fails. // Upload the data. Throws exception if it fails.
$bucket = $options['bucket']; $bucket = $options['bucket'];
@ -39,15 +38,15 @@ class SmallInlineFiles extends SmallFiles
$result = true; $result = true;
// Should I download the file and compare its contents with my random data? // Should I download the file and compare its contents with my random data?
if (self::$downloadAfter) if (static::$downloadAfter)
{ {
$downloadedData = $s3->getObject($bucket, $uri); $downloadedData = $s3->getObject($bucket, $uri);
$result = self::areStringsEqual($sourceData, $downloadedData); $result = static::areStringsEqual($sourceData, $downloadedData);
} }
// Should I delete the remotely stored file? // Should I delete the remotely stored file?
if (self::$deleteRemote) if (static::$deleteRemote)
{ {
// Delete the remote file. Throws exception if it fails. // Delete the remote file. Throws exception if it fails.
$s3->deleteObject($bucket, $uri); $s3->deleteObject($bucket, $uri);

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
/** /**
* Upload and download small files (under 1MB) using a string source * Upload and download small files (under 1MB) using a string source
@ -21,7 +21,7 @@ class SmallInlineFilesNoDelete extends SmallInlineFiles
{ {
public static function setup(Connector $s3, array $options): void public static function setup(Connector $s3, array $options): void
{ {
self:: $deleteRemote = false; static:: $deleteRemote = false;
parent::setup($s3, $options); parent::setup($s3, $options);
} }

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
/** /**
* Upload small files (under 1MB) using a string source * Upload small files (under 1MB) using a string source
@ -21,8 +21,8 @@ class SmallInlineFilesOnlyUpload extends SmallInlineFiles
{ {
public static function setup(Connector $s3, array $options): void public static function setup(Connector $s3, array $options): void
{ {
self::$deleteRemote = false; static::$deleteRemote = false;
self::$downloadAfter = false; static::$downloadAfter = false;
parent::setup($s3, $options); parent::setup($s3, $options);
} }

View file

@ -0,0 +1,131 @@
<?php
/**
* Akeeba Engine
*
* @package akeebaengine
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
namespace Akeeba\MiniTest\Test;
use Akeeba\S3\Connector;
use Akeeba\S3\Input;
/**
* Upload, download and delete small XML files (under 1MB) using a string source
*
* @package Akeeba\MiniTest\Test
*/
class SmallInlineXMLFiles extends SmallFiles
{
public static function upload10KbRoot(Connector $s3, array $options): bool
{
return static::upload($s3, $options, AbstractTest::TEN_KB, 'root_10kb.xml');
}
public static function upload10KbRootGreek(Connector $s3, array $options): bool
{
return static::upload($s3, $options, AbstractTest::TEN_KB, οκιμή_10kb.xml');
}
public static function upload10KbFolderGreek(Connector $s3, array $options): bool
{
return static::upload($s3, $options, AbstractTest::TEN_KB, 'ο_φάκελός_μουοκιμή_10kb.xml');
}
public static function upload600KbRoot(Connector $s3, array $options): bool
{
return static::upload($s3, $options, AbstractTest::SIX_HUNDRED_KB, 'root_600kb.xml');
}
public static function upload10KbFolder(Connector $s3, array $options): bool
{
return static::upload($s3, $options, AbstractTest::TEN_KB, 'my_folder/10kb.xml');
}
public static function upload600KbFolder(Connector $s3, array $options): bool
{
return static::upload($s3, $options, AbstractTest::SIX_HUNDRED_KB, 'my_folder/600kb.xml');
}
protected static function upload(Connector $s3, array $options, int $size, string $uri): bool
{
// Randomize the name. Required for archive buckets where you cannot overwrite data.
$dotPos = strrpos($uri, '.');
$uri = substr($uri, 0, $dotPos) . '.' . md5(microtime(false)) . substr($uri, $dotPos);
// Create some random data to upload
$sourceData = static::createXMLFile($size);
// Upload the data. Throws exception if it fails.
$bucket = $options['bucket'];
$input = Input::createFromData($sourceData);
$s3->putObject($input, $bucket, $uri);
// Tentatively accept that this method succeeded.
$result = true;
// Should I download the file and compare its contents with my random data?
if (static::$downloadAfter)
{
$downloadedData = $s3->getObject($bucket, $uri);
$result = static::areStringsEqual($sourceData, $downloadedData);
}
// Should I delete the remotely stored file?
if (static::$deleteRemote)
{
// Delete the remote file. Throws exception if it fails.
$s3->deleteObject($bucket, $uri);
}
return $result;
}
private static function createXMLFile(int $size): string
{
$out = <<< XML
<?xml version="1.0" encoding="utf-8" ?>
<root>
XML;
$chunks = floor(($size - 55) / 1024);
for ($i = 1; $i <= $chunks; $i++)
{
$randomBlock = static::genRandomData(1024 - 63);
$out .= <<< XML
<element>
<id>$i</id>
<data><![CDATA[$randomBlock]]></data>
</element>
XML;
}
$out .= <<< XML
</root>
XML;
return $out;
}
private static function genRandomData(int $length): string
{
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ01234567890';
$maxLength = strlen($chars) - 1;
$salt = '';
for ($i = 0; $i < $length; $i++)
{
$salt .= substr($chars, random_int(0, $maxLength), 1);
}
return $salt;
}
}

View file

@ -3,17 +3,17 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\MiniTest\Test; namespace Akeeba\MiniTest\Test;
use Akeeba\Engine\Postproc\Connector\S3v4\Acl; use Akeeba\S3\Acl;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
use Akeeba\Engine\Postproc\Connector\S3v4\Input; use Akeeba\S3\Input;
use Akeeba\Engine\Postproc\Connector\S3v4\StorageClass; use Akeeba\S3\StorageClass;
class StorageClasses extends AbstractTest class StorageClasses extends AbstractTest
{ {
@ -23,12 +23,12 @@ class StorageClasses extends AbstractTest
public static function uploadRRS(Connector $s3, array $options): bool public static function uploadRRS(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, self::TEN_KB, 'rrs_test_10kb.dat', StorageClass::REDUCED_REDUNDANCY); return static::upload($s3, $options, static::TEN_KB, 'rrs_test_10kb.dat', StorageClass::REDUCED_REDUNDANCY);
} }
public static function uploadIntelligentTiering(Connector $s3, array $options): bool public static function uploadIntelligentTiering(Connector $s3, array $options): bool
{ {
return self::upload($s3, $options, self::TEN_KB, 'rrs_test_10kb.dat', StorageClass::INTELLIGENT_TIERING); return static::upload($s3, $options, static::TEN_KB, 'rrs_test_10kb.dat', StorageClass::INTELLIGENT_TIERING);
} }
protected static function upload(Connector $s3, array $options, int $size, string $uri, string $storageClass = null) protected static function upload(Connector $s3, array $options, int $size, string $uri, string $storageClass = null)
@ -38,7 +38,7 @@ class StorageClasses extends AbstractTest
$uri = substr($uri, 0, $dotPos) . '.' . md5(microtime(false)) . substr($uri, $dotPos); $uri = substr($uri, 0, $dotPos) . '.' . md5(microtime(false)) . substr($uri, $dotPos);
// Create some random data to upload // Create some random data to upload
$sourceData = self::getRandomData($size); $sourceData = static::getRandomData($size);
// Upload the data. Throws exception if it fails. // Upload the data. Throws exception if it fails.
$bucket = $options['bucket']; $bucket = $options['bucket'];
@ -54,15 +54,15 @@ class StorageClasses extends AbstractTest
$result = true; $result = true;
// Should I download the file and compare its contents with my random data? // Should I download the file and compare its contents with my random data?
if (self::$downloadAfter) if (static::$downloadAfter)
{ {
$downloadedData = $s3->getObject($bucket, $uri); $downloadedData = $s3->getObject($bucket, $uri);
$result = self::areStringsEqual($sourceData, $downloadedData); $result = static::areStringsEqual($sourceData, $downloadedData);
} }
// Should I delete the remotely stored file? // Should I delete the remotely stored file?
if (self::$deleteRemote) if (static::$deleteRemote)
{ {
// Delete the remote file. Throws exception if it fails. // Delete the remote file. Throws exception if it fails.
$s3->deleteObject($bucket, $uri); $s3->deleteObject($bucket, $uri);

View file

@ -3,10 +3,12 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
// Custom Endpoint. The example below is for using LocalStack, see https://localstack.cloud/
// define('DEFAULT_ENDPOINT', 'localhost.localstack.cloud:4566');
// Default Amazon S3 Access Key // Default Amazon S3 Access Key
define('DEFAULT_ACCESS_KEY', 'your s3 access key'); define('DEFAULT_ACCESS_KEY', 'your s3 access key');
// Default Amazon S3 Secret Key // Default Amazon S3 Secret Key
@ -23,6 +25,8 @@ define('DEFAULT_DUALSTACK', false);
define('DEFAULT_PATH_ACCESS', false); define('DEFAULT_PATH_ACCESS', false);
// Should I use SSL by default? // Should I use SSL by default?
define('DEFAULT_SSL', true); define('DEFAULT_SSL', true);
// Create the 2100 test files in the bucket?
define('CREATE_2100_FILES', true);
/** /**
* Tests for standard key pairs allowing us to read, write and delete * Tests for standard key pairs allowing us to read, write and delete
@ -33,7 +37,9 @@ $standardTests = [
'BucketsList', 'BucketsList',
'BucketLocation', 'BucketLocation',
'SmallFiles', 'SmallFiles',
'HeadObject',
'SmallInlineFiles', 'SmallInlineFiles',
'SmallInlineXMLFiles',
'SignedURLs', 'SignedURLs',
'StorageClasses', 'StorageClasses',
'ListFiles', 'ListFiles',

View file

@ -3,13 +3,13 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
use Akeeba\Engine\Postproc\Connector\S3v4\Configuration; use Akeeba\S3\Configuration;
use Akeeba\Engine\Postproc\Connector\S3v4\Connector; use Akeeba\S3\Connector;
use Akeeba\Engine\Postproc\Connector\S3v4\Input; use Akeeba\S3\Input;
// Necessary for including the library // Necessary for including the library
define('AKEEBAENGINE', 1); define('AKEEBAENGINE', 1);
@ -167,7 +167,7 @@ foreach ($testConfigurations as $description => $setup)
'dualstack' => DEFAULT_DUALSTACK, 'dualstack' => DEFAULT_DUALSTACK,
'path_access' => DEFAULT_PATH_ACCESS, 'path_access' => DEFAULT_PATH_ACCESS,
'ssl' => DEFAULT_SSL, 'ssl' => DEFAULT_SSL,
'endpoint' => null, 'endpoint' => defined('DEFAULT_ENDPOINT') ? constant('DEFAULT_ENDPOINT') : null,
], $setup['configuration']); ], $setup['configuration']);
// Extract the test classes/methods to run // Extract the test classes/methods to run
@ -185,15 +185,21 @@ foreach ($testConfigurations as $description => $setup)
// Create the S3 configuration object // Create the S3 configuration object
$s3Configuration = new Configuration($configOptions['access'], $configOptions['secret'], $configOptions['signature'], $configOptions['region']); $s3Configuration = new Configuration($configOptions['access'], $configOptions['secret'], $configOptions['signature'], $configOptions['region']);
$s3Configuration->setUseDualstackUrl($configOptions['dualstack']); $s3Configuration->setRegion($configOptions['region']);
$s3Configuration->setUseLegacyPathStyle($configOptions['path_access']); $s3Configuration->setSignatureMethod($configOptions['signature']);
$s3Configuration->setSSL($configOptions['ssl']);
if (!is_null($configOptions['endpoint'])) if (!is_null($configOptions['endpoint']))
{ {
$s3Configuration->setEndpoint($configOptions['endpoint']); $s3Configuration->setEndpoint($configOptions['endpoint']);
// We need to redo this because setting the endpoint may reset these options
$s3Configuration->setRegion($configOptions['region']);
$s3Configuration->setSignatureMethod($configOptions['signature']);
} }
$s3Configuration->setUseDualstackUrl($configOptions['dualstack']);
$s3Configuration->setUseLegacyPathStyle($configOptions['path_access']);
$s3Configuration->setSSL($configOptions['ssl']);
// Create the connector object // Create the connector object
$s3Connector = new Connector($s3Configuration); $s3Connector = new Connector($s3Configuration);

View file

@ -3,29 +3,29 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4; namespace Akeeba\S3;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
/** /**
* Shortcuts to often used access control privileges * Shortcuts to often used access control privileges
*/ */
class Acl class Acl
{ {
const ACL_PRIVATE = 'private'; public const ACL_PRIVATE = 'private';
const ACL_PUBLIC_READ = 'public-read'; public const ACL_PUBLIC_READ = 'public-read';
const ACL_PUBLIC_READ_WRITE = 'public-read-write'; public const ACL_PUBLIC_READ_WRITE = 'public-read-write';
const ACL_AUTHENTICATED_READ = 'authenticated-read'; public const ACL_AUTHENTICATED_READ = 'authenticated-read';
const ACL_BUCKET_OWNER_READ = 'bucket-owner-read'; public const ACL_BUCKET_OWNER_READ = 'bucket-owner-read';
const ACL_BUCKET_OWNER_FULL_CONTROL = 'bucket-owner-full-control'; public const ACL_BUCKET_OWNER_FULL_CONTROL = 'bucket-owner-full-control';
} }

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4; namespace Akeeba\S3;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
/** /**
* Holds the Amazon S3 confiugration credentials * Holds the Amazon S3 confiugration credentials
@ -199,6 +199,8 @@ class Configuration
throw new Exception\InvalidSignatureMethod; throw new Exception\InvalidSignatureMethod;
} }
$this->signatureMethod = $signatureMethod;
// If you switch to v2 signatures we unset the region. // If you switch to v2 signatures we unset the region.
if ($signatureMethod == 'v2') if ($signatureMethod == 'v2')
{ {
@ -214,15 +216,9 @@ class Configuration
$this->setUseLegacyPathStyle(false); $this->setUseLegacyPathStyle(false);
} }
} else {
if (empty($this->getRegion())) {
$this->setRegion('us-east-1');
} }
} }
$this->signatureMethod = $signatureMethod;
}
/** /**
* Get the Amazon S3 region * Get the Amazon S3 region
* *

View file

@ -3,22 +3,22 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4; namespace Akeeba\S3;
// Protection against direct access // Protection against direct access
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotDeleteFile; use Akeeba\S3\Exception\CannotDeleteFile;
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotGetBucket; use Akeeba\S3\Exception\CannotGetBucket;
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotGetFile; use Akeeba\S3\Exception\CannotGetFile;
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotListBuckets; use Akeeba\S3\Exception\CannotListBuckets;
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotOpenFileForWrite; use Akeeba\S3\Exception\CannotOpenFileForWrite;
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\CannotPutFile; use Akeeba\S3\Exception\CannotPutFile;
use Akeeba\Engine\Postproc\Connector\S3v4\Response\Error; use Akeeba\S3\Response\Error;
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
class Connector class Connector
{ {
@ -80,9 +80,12 @@ class Connector
} }
if (($input->getSize() <= 0) || (($input->getInputType() == Input::INPUT_DATA) && (!strlen($input->getDataReference())))) if (($input->getSize() <= 0) || (($input->getInputType() == Input::INPUT_DATA) && (!strlen($input->getDataReference()))))
{
if (substr($uri, -1) !== '/')
{ {
throw new CannotPutFile('Missing input parameters', 0); throw new CannotPutFile('Missing input parameters', 0);
} }
}
// We need to post with Content-Length and Content-Type, MD5 is optional // We need to post with Content-Length and Content-Type, MD5 is optional
$request->setHeader('Content-Type', $input->getType()); $request->setHeader('Content-Type', $input->getType());
@ -169,7 +172,7 @@ class Connector
if (!is_resource($saveTo) && is_string($saveTo)) if (!is_resource($saveTo) && is_string($saveTo))
{ {
$fp = @fopen($saveTo, 'wb'); $fp = @fopen($saveTo, 'w');
if ($fp === false) if ($fp === false)
{ {
@ -193,6 +196,53 @@ class Connector
$request->setHeader('Range', "bytes=$from-$to"); $request->setHeader('Range', "bytes=$from-$to");
} }
$response = $request->getResponse(true);
if (!$response->error->isError() && (($response->code !== 200) && ($response->code !== 206)))
{
$response->error = new Error(
$response->code,
"Unexpected HTTP status {$response->code}"
);
}
if ($response->error->isError())
{
throw new CannotGetFile(
sprintf(
__METHOD__ . "({%s}, {%s}): [%s] %s\n\nDebug info:\n%s",
$bucket,
$uri,
$response->error->getCode(),
$response->error->getMessage(),
print_r($response->body, true)
)
);
}
if (!is_resource($fp))
{
return $response->body;
}
return null;
}
/**
* Get information about an object.
*
* @param string $bucket Bucket name
* @param string $uri Object URI
*
* @return array The headers returned by Amazon S3
*
* @throws CannotGetFile If the file does not exist
* @see https://docs.aws.amazon.com/AmazonS3/latest/API/API_HeadObject.html
*/
public function headObject(string $bucket, string $uri): array
{
$request = new Request('HEAD', $bucket, $uri, $this->configuration);
$response = $request->getResponse(); $response = $request->getResponse();
if (!$response->error->isError() && (($response->code !== 200) && ($response->code !== 206))) if (!$response->error->isError() && (($response->code !== 200) && ($response->code !== 206)))
@ -206,19 +256,20 @@ class Connector
if ($response->error->isError()) if ($response->error->isError())
{ {
throw new CannotGetFile( throw new CannotGetFile(
sprintf(__METHOD__ . "({$bucket}, {$uri}): [%s] %s\n\nDebug info:\n%s", sprintf(
$response->error->getCode(), $response->error->getMessage(), print_r($response->body, true)), __METHOD__ . "({%s}, {%s}): [%s] %s\n\nDebug info:\n%s",
$response->error->getCode() $bucket,
$uri,
$response->error->getCode(),
$response->error->getMessage(),
print_r($response->body, true)
)
); );
} }
if (!is_resource($fp)) return $response->getHeaders();
{
return $response->body;
} }
return null;
}
/** /**
* Delete an object * Delete an object
@ -244,9 +295,13 @@ class Connector
if ($response->error->isError()) if ($response->error->isError())
{ {
throw new CannotDeleteFile( throw new CannotDeleteFile(
sprintf(__METHOD__ . "({$bucket}, {$uri}): [%s] %s", sprintf(
$response->error->getCode(), $response->error->getMessage()), __METHOD__ . "({%s}, {%s}): [%s] %s",
$response->error->getCode() $bucket,
$uri,
$response->error->getCode(),
$response->error->getMessage()
)
); );
} }
} }
@ -358,8 +413,7 @@ class Connector
if ($response->error->isError()) if ($response->error->isError())
{ {
throw new CannotGetBucket( throw new CannotGetBucket(
sprintf(__METHOD__ . "(): [%s] %s", $response->error->getCode(), $response->error->getMessage()), sprintf(__METHOD__ . "(): [%s] %s", $response->error->getCode(), $response->error->getMessage())
$response->error->getCode()
); );
} }
@ -403,168 +457,47 @@ class Connector
*/ */
public function getBucket(string $bucket, ?string $prefix = null, ?string $marker = null, ?int $maxKeys = null, string $delimiter = '/', bool $returnCommonPrefixes = false): array public function getBucket(string $bucket, ?string $prefix = null, ?string $marker = null, ?int $maxKeys = null, string $delimiter = '/', bool $returnCommonPrefixes = false): array
{ {
$request = new Request('GET', $bucket, '', $this->configuration); $internalResult = $this->internalGetBucket($bucket, $prefix, $marker, $maxKeys, $delimiter, $returnCommonPrefixes);
if (!empty($prefix)) /**
{ * @var array $objects
$request->setParameter('prefix', $prefix); * @var ?string $nextMarker
} */
extract($internalResult);
unset($internalResult);
if (!empty($marker)) // Loop through truncated results if maxKeys isn't specified or we don't have enough object records yet.
{ if ($nextMarker !== null && ($maxKeys === null || count($objects) < $maxKeys))
$request->setParameter('marker', $marker);
}
if (!empty($maxKeys))
{
$request->setParameter('max-keys', $maxKeys);
}
if (!empty($delimiter))
{
$request->setParameter('delimiter', $delimiter);
}
$response = $request->getResponse();
if (!$response->error->isError() && $response->code !== 200)
{
$response->error = new Error(
$response->code,
"Unexpected HTTP status {$response->code}"
);
}
if ($response->error->isError())
{
throw new CannotGetBucket(
sprintf(__METHOD__ . "(): [%s] %s", $response->error->getCode(), $response->error->getMessage()),
$response->error->getCode()
);
}
$results = [];
$nextMarker = null;
if ($response->hasBody() && isset($response->body->Contents))
{
foreach ($response->body->Contents as $c)
{
$results[(string) $c->Key] = [
'name' => (string) $c->Key,
'time' => strtotime((string) $c->LastModified),
'size' => (int) $c->Size,
'hash' => substr((string) $c->ETag, 1, -1),
];
$nextMarker = (string) $c->Key;
}
}
if ($returnCommonPrefixes && $response->hasBody() && isset($response->body->CommonPrefixes))
{
foreach ($response->body->CommonPrefixes as $c)
{
$results[(string) $c->Prefix] = ['prefix' => (string) $c->Prefix];
}
}
if ($response->hasBody() && isset($response->body->IsTruncated) &&
((string) $response->body->IsTruncated == 'false')
)
{
return $results;
}
if ($response->hasBody() && isset($response->body->NextMarker))
{
$nextMarker = (string) $response->body->NextMarker;
}
// Is it a truncated result?
$isTruncated = ($nextMarker !== null) && ((string) $response->body->IsTruncated == 'true');
// Is this a truncated result and no maxKeys specified?
$isTruncatedAndNoMaxKeys = ($maxKeys == null) && $isTruncated;
// Is this a truncated result with less keys than the specified maxKeys; and common prefixes found but not returned to the caller?
$isTruncatedAndNeedsContinue = ($maxKeys != null) && $isTruncated && (count($results) < $maxKeys);
// Loop through truncated results if maxKeys isn't specified
if ($isTruncatedAndNoMaxKeys || $isTruncatedAndNeedsContinue)
{ {
do do
{ {
$request = new Request('GET', $bucket, '', $this->configuration); $internalResult = $this->internalGetBucket($bucket, $prefix, $nextMarker, $maxKeys, $delimiter, $returnCommonPrefixes);
if (!empty($prefix)) $nextMarker = $internalResult['nextMarker'];
{ $objects = array_merge($objects, $internalResult['objects']);
$request->setParameter('prefix', $prefix);
}
$request->setParameter('marker', $nextMarker); unset($internalResult);
if (!empty($delimiter)) // If the last call did not return a nextMarker I am done iterating.
{ if ($nextMarker === null)
$request->setParameter('delimiter', $delimiter);
}
try
{
$response = $request->getResponse();
}
catch (\Exception $e)
{ {
break; break;
} }
if ($response->hasBody() && isset($response->body->Contents)) // If we have maxKeys AND the number of objects is at least this many I am done iterating.
if ($maxKeys !== null && count($objects) >= $maxKeys)
{ {
foreach ($response->body->Contents as $c) break;
{
$results[(string) $c->Key] = [
'name' => (string) $c->Key,
'time' => strtotime((string) $c->LastModified),
'size' => (int) $c->Size,
'hash' => substr((string) $c->ETag, 1, -1),
];
$nextMarker = (string) $c->Key;
} }
} while (true);
} }
if ($returnCommonPrefixes && $response->hasBody() && isset($response->body->CommonPrefixes)) if ($maxKeys !== null)
{ {
foreach ($response->body->CommonPrefixes as $c) return array_splice($objects, 0, $maxKeys);
{
$results[(string) $c->Prefix] = ['prefix' => (string) $c->Prefix];
}
} }
if ($response->hasBody() && isset($response->body->NextMarker)) return $objects;
{
$nextMarker = (string) $response->body->NextMarker;
}
$continueCondition = false;
if ($isTruncatedAndNoMaxKeys)
{
$continueCondition = !$response->error->isError() && $isTruncated;
}
if ($isTruncatedAndNeedsContinue)
{
$continueCondition = !$response->error->isError() && $isTruncated && (count($results) < $maxKeys);
}
} while ($continueCondition);
}
if (!is_null($maxKeys))
{
$results = array_splice($results, 0, $maxKeys);
}
return $results;
} }
/** /**
@ -594,8 +527,7 @@ class Connector
if ($response->error->isError()) if ($response->error->isError())
{ {
throw new CannotListBuckets( throw new CannotListBuckets(
sprintf(__METHOD__ . "(): [%s] %s", $response->error->getCode(), $response->error->getMessage()), sprintf(__METHOD__ . "(): [%s] %s", $response->error->getCode(), $response->error->getMessage())
$response->error->getCode()
); );
} }
@ -691,7 +623,12 @@ class Connector
if ($response->error->isError()) if ($response->error->isError())
{ {
throw new CannotPutFile( throw new CannotPutFile(
sprintf(__METHOD__ . "(): [%s] %s\n\nDebug info:\n%s", $response->error->getCode(), $response->error->getMessage(), print_r($response->body, true)) sprintf(
__METHOD__ . "(): [%s] %s\n\nDebug info:\n%s",
$response->error->getCode(),
$response->error->getMessage(),
print_r($response->body, true)
)
); );
} }
@ -958,4 +895,90 @@ class Connector
{ {
return $this->configuration; return $this->configuration;
} }
private function internalGetBucket(string $bucket, ?string $prefix = null, ?string $marker = null, ?int $maxKeys = null, string $delimiter = '/', bool $returnCommonPrefixes = false): array
{
$request = new Request('GET', $bucket, '', $this->configuration);
if (!empty($prefix))
{
$request->setParameter('prefix', $prefix);
}
if (!empty($marker))
{
$request->setParameter('marker', $marker);
}
if (!empty($maxKeys))
{
$request->setParameter('max-keys', $maxKeys);
}
if (!empty($delimiter))
{
$request->setParameter('delimiter', $delimiter);
}
$response = $request->getResponse();
if (!$response->error->isError() && $response->code !== 200)
{
$response->error = new Error(
$response->code,
"Unexpected HTTP status {$response->code}"
);
}
if ($response->error->isError())
{
throw new CannotGetBucket(
sprintf(__METHOD__ . "(): [%s] %s", $response->error->getCode(), $response->error->getMessage())
);
}
$results = [
'objects' => [],
'nextMarker' => null,
];
if ($response->hasBody() && isset($response->body->Contents))
{
foreach ($response->body->Contents as $c)
{
$results['objects'][(string) $c->Key] = [
'name' => (string) $c->Key,
'time' => strtotime((string) $c->LastModified),
'size' => (int) $c->Size,
'hash' => substr((string) $c->ETag, 1, -1),
];
$results['nextMarker'] = (string) $c->Key;
}
}
if ($returnCommonPrefixes && $response->hasBody() && isset($response->body->CommonPrefixes))
{
foreach ($response->body->CommonPrefixes as $c)
{
$results['objects'][(string) $c->Prefix] = ['prefix' => (string) $c->Prefix];
}
}
if ($response->hasBody() && isset($response->body->IsTruncated) &&
((string) $response->body->IsTruncated == 'false')
)
{
$results['nextMarker'] = null;
return $results;
}
if ($response->hasBody() && isset($response->body->NextMarker))
{
$results['nextMarker'] = (string) $response->body->NextMarker;
}
return $results;
}
} }

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use RuntimeException; use RuntimeException;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use RuntimeException; use RuntimeException;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use RuntimeException; use RuntimeException;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use RuntimeException; use RuntimeException;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use Exception; use Exception;
use RuntimeException; use RuntimeException;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use Exception; use Exception;
use RuntimeException; use RuntimeException;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use RuntimeException; use RuntimeException;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use RuntimeException; use RuntimeException;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use Exception; use Exception;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use Exception; use Exception;
use RuntimeException; use RuntimeException;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use Exception; use Exception;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use Exception; use Exception;
use InvalidArgumentException; use InvalidArgumentException;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use Exception; use Exception;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use Exception; use Exception;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use Exception; use Exception;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Exception; namespace Akeeba\S3\Exception;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use LogicException; use LogicException;

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4; namespace Akeeba\S3;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
/** /**
* Defines an input source for PUT/POST requests to Amazon S3 * Defines an input source for PUT/POST requests to Amazon S3
@ -20,17 +20,17 @@ class Input
/** /**
* Input type: resource * Input type: resource
*/ */
const INPUT_RESOURCE = 1; public const INPUT_RESOURCE = 1;
/** /**
* Input type: file * Input type: file
*/ */
const INPUT_FILE = 2; public const INPUT_FILE = 2;
/** /**
* Input type: raw data * Input type: raw data
*/ */
const INPUT_DATA = 3; public const INPUT_DATA = 3;
/** /**
* File pointer, in case we have a resource * File pointer, in case we have a resource
@ -176,9 +176,15 @@ class Input
function __destruct() function __destruct()
{ {
if (is_resource($this->fp)) if (is_resource($this->fp))
{
try
{ {
@fclose($this->fp); @fclose($this->fp);
} }
catch (\Throwable $e)
{
}
}
} }
/** /**
@ -257,11 +263,17 @@ class Input
$this->data = null; $this->data = null;
if (is_resource($this->fp)) if (is_resource($this->fp))
{
try
{ {
@fclose($this->fp); @fclose($this->fp);
} }
catch (\Throwable $e)
{
}
}
$this->fp = @fopen($file, 'rb'); $this->fp = @fopen($file, 'r');
if ($this->fp === false) if ($this->fp === false)
{ {
@ -294,9 +306,15 @@ class Input
$this->data = $data; $this->data = $data;
if (is_resource($this->fp)) if (is_resource($this->fp))
{
try
{ {
@fclose($this->fp); @fclose($this->fp);
} }
catch (\Throwable $e)
{
}
}
$this->file = null; $this->file = null;
$this->fp = null; $this->fp = null;
@ -328,9 +346,15 @@ class Input
$this->data = $data; $this->data = $data;
if (is_resource($this->fp)) if (is_resource($this->fp))
{
try
{ {
@fclose($this->fp); @fclose($this->fp);
} }
catch (\Throwable $e)
{
}
}
$this->file = null; $this->file = null;
$this->fp = null; $this->fp = null;
@ -450,7 +474,7 @@ class Input
*/ */
public function setSha256(?string $sha256): void public function setSha256(?string $sha256): void
{ {
$this->sha256 = strtolower($sha256); $this->sha256 = is_null($sha256) ? null : strtolower($sha256);
} }
/** /**
@ -532,7 +556,7 @@ class Input
switch ($this->getInputType()) switch ($this->getInputType())
{ {
case self::INPUT_DATA: case self::INPUT_DATA:
return function_exists('mb_strlen') ? mb_strlen($this->data, '8bit') : strlen($this->data); return function_exists('mb_strlen') ? mb_strlen($this->data ?? '', '8bit') : strlen($this->data ?? '');
break; break;
case self::INPUT_FILE: case self::INPUT_FILE:
@ -635,7 +659,7 @@ class Input
$ext = strtolower(pathInfo($file, PATHINFO_EXTENSION)); $ext = strtolower(pathInfo($file, PATHINFO_EXTENSION));
return isset($exts[$ext]) ? $exts[$ext] : 'application/octet-stream'; return $exts[$ext] ?? 'application/octet-stream';
} }
/** /**

View file

@ -3,16 +3,16 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4; namespace Akeeba\S3;
use Akeeba\Engine\Postproc\Connector\S3v4\Response\Error; use Akeeba\S3\Response\Error;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
class Request class Request
@ -142,6 +142,12 @@ class Request
// The date must always be added as a header // The date must always be added as a header
$this->headers['Date'] = gmdate('D, d M Y H:i:s O'); $this->headers['Date'] = gmdate('D, d M Y H:i:s O');
// S3-"compatible" services use a different date format. Because why not?
if (strpos($this->headers['Host'], '.amazonaws.com') === false)
{
$this->headers['Date'] = gmdate('D, d M Y H:i:s T');
}
// If there is a security token we need to set up the X-Amz-Security-Token header // If there is a security token we need to set up the X-Amz-Security-Token header
$token = $this->configuration->getToken(); $token = $this->configuration->getToken();
@ -367,7 +373,7 @@ class Request
* *
* @return Response * @return Response
*/ */
public function getResponse(): Response public function getResponse(bool $rawResponse = false): Response
{ {
$this->processParametersIntoResource(); $this->processParametersIntoResource();
@ -417,8 +423,10 @@ class Request
* Caveat: if your bucket contains dots in the name we have to turn off host verification due to the way the * Caveat: if your bucket contains dots in the name we have to turn off host verification due to the way the
* S3 SSL certificates are set up. * S3 SSL certificates are set up.
*/ */
$isAmazonS3 = (substr($this->headers['Host'], -14) == '.amazonaws.com') || $isAmazonS3 = (substr($this->headers['Host'], -14) == '.amazonaws.com')
substr($this->headers['Host'], -16) == 'amazonaws.com.cn'; || substr(
$this->headers['Host'], -16
) == 'amazonaws.com.cn';
$tooManyDots = substr_count($this->headers['Host'], '.') > 4; $tooManyDots = substr_count($this->headers['Host'], '.') > 4;
$verifyHost = ($isAmazonS3 && $tooManyDots) ? 0 : 2; $verifyHost = ($isAmazonS3 && $tooManyDots) ? 0 : 2;
@ -429,6 +437,27 @@ class Request
curl_setopt($curl, CURLOPT_URL, $url); curl_setopt($curl, CURLOPT_URL, $url);
/**
* Set the optional x-amz-date header for third party services.
*
* Amazon S3 proper expects to get the date from the Date header. Third party services typically implement the
* (wrongly) documented behaviour of using the x-amz-date header but, if it's missing, fall back to the Date
* header. Wasabi does not fall back; it only uses the x-amz-date header which is why we have to set it here if
* the request iss not made to Amazon S3 proper.
*/
$this->headers['x-amz-date'] = strpos($this->headers['Host'], '.amazonaws.com') !== false
? ''
: (new \DateTime($this->headers['Date']))->format('Ymd\THis\Z');
/**
* Remove empty headers.
*
* While Amazon S3 proper and most third party implementations have no problem with that, there a few of them
* (such as Synology C2) which choke on empty headers.
*/
$this->headers = array_filter($this->headers);
// Get the request signature
$signer = Signature::getSignatureObject($this, $this->configuration->getSignatureMethod()); $signer = Signature::getSignatureObject($this, $this->configuration->getSignatureMethod());
$signer->preProcessHeaders($this->headers, $this->amzHeaders); $signer->preProcessHeaders($this->headers, $this->amzHeaders);
@ -482,7 +511,7 @@ class Request
$data = $this->input->getDataReference(); $data = $this->input->getDataReference();
if (strlen($data)) if (strlen($data ?? ''))
{ {
curl_setopt($curl, CURLOPT_POSTFIELDS, $data); curl_setopt($curl, CURLOPT_POSTFIELDS, $data);
} }
@ -538,12 +567,18 @@ class Request
@curl_close($curl); @curl_close($curl);
// Set the body data // Set the body data
$this->response->finaliseBody(); $this->response->finaliseBody($rawResponse);
// Clean up file resources // Clean up file resources
if (!is_null($this->fp) && is_resource($this->fp)) if (!is_null($this->fp) && is_resource($this->fp))
{ {
fclose($this->fp); try
{
@fclose($this->fp);
}
catch (\Throwable $e)
{
}
} }
return $this->response; return $this->response;
@ -560,7 +595,7 @@ class Request
*/ */
protected function __responseWriteCallback($curl, string $data): int protected function __responseWriteCallback($curl, string $data): int
{ {
if (in_array($this->response->code, [200, 206]) && !is_null($this->fp) && is_resource($this->fp)) if (in_array($this->response->code, [0, 200, 206]) && !is_null($this->fp) && is_resource($this->fp))
{ {
return fwrite($this->fp, $data); return fwrite($this->fp, $data);
} }
@ -592,7 +627,15 @@ class Request
return $strlen; return $strlen;
} }
[$header, $value] = explode(': ', trim($data), 2); // Ignore malformed headers without a value.
if (strpos($data, ':') === false)
{
return $strlen;
}
[$header, $value] = explode(':', trim($data), 2);
$header = trim($header ?? '');
$value = trim($value ?? '');
switch (strtolower($header)) switch (strtolower($header))
{ {
@ -609,10 +652,12 @@ class Request
break; break;
case 'etag': case 'etag':
$this->response->setHeader('hash', $value[0] == '"' ? substr($value, 1, -1) : $value); $this->response->setHeader('hash', trim($value, '"'));
break; break;
default: default:
$this->response->setHeader(strtolower($header), is_numeric($value) ? (int) $value : $value);
if (preg_match('/^x-amz-meta-.*$/', $header)) if (preg_match('/^x-amz-meta-.*$/', $header))
{ {
$this->setHeader($header, is_numeric($value) ? (int) $value : $value); $this->setHeader($header, is_numeric($value) ? (int) $value : $value);
@ -652,13 +697,12 @@ class Request
$query = substr($query, 0, -1); $query = substr($query, 0, -1);
$this->uri .= $query; $this->uri .= $query;
if (array_key_exists('acl', $this->parameters) || if (array_key_exists('acl', $this->parameters) || array_key_exists('location', $this->parameters)
array_key_exists('location', $this->parameters) || || array_key_exists('torrent', $this->parameters)
array_key_exists('torrent', $this->parameters) || || array_key_exists('logging', $this->parameters)
array_key_exists('logging', $this->parameters) || || array_key_exists('uploads', $this->parameters)
array_key_exists('uploads', $this->parameters) || || array_key_exists('uploadId', $this->parameters)
array_key_exists('uploadId', $this->parameters) || || array_key_exists('partNumber', $this->parameters)
array_key_exists('partNumber', $this->parameters)
) )
{ {
$this->resource .= $query; $this->resource .= $query;
@ -720,6 +764,8 @@ class Request
} }
/** /**
* Only applies to Amazon S3 proper.
*
* When using the Amazon S3 with the v4 signature API we have to use a different hostname per region. The * When using the Amazon S3 with the v4 signature API we have to use a different hostname per region. The
* mapping can be found in https://docs.aws.amazon.com/general/latest/gr/s3.html#s3_region * mapping can be found in https://docs.aws.amazon.com/general/latest/gr/s3.html#s3_region
* *
@ -728,7 +774,8 @@ class Request
* *
* v4 signing does NOT support non-Amazon endpoints. * v4 signing does NOT support non-Amazon endpoints.
*/ */
if (in_array($endpoint, ['s3.amazonaws.com', 'amazonaws.com.cn']))
{
// Most endpoints: s3-REGION.amazonaws.com // Most endpoints: s3-REGION.amazonaws.com
$regionalEndpoint = $region . '.amazonaws.com'; $regionalEndpoint = $region . '.amazonaws.com';
@ -748,6 +795,7 @@ class Request
{ {
$endpoint = 's3.' . $regionalEndpoint; $endpoint = 's3.' . $regionalEndpoint;
} }
}
// Legacy path style access: return just the endpoint // Legacy path style access: return just the endpoint
if ($configuration->getUseLegacyPathStyle()) if ($configuration->getUseLegacyPathStyle())

View file

@ -3,18 +3,18 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4; namespace Akeeba\S3;
use Akeeba\Engine\Postproc\Connector\S3v4\Exception\PropertyNotFound; use Akeeba\S3\Exception\PropertyNotFound;
use Akeeba\Engine\Postproc\Connector\S3v4\Response\Error; use Akeeba\S3\Response\Error;
use SimpleXMLElement; use SimpleXMLElement;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
/** /**
* Amazon S3 API response object * Amazon S3 API response object
@ -124,7 +124,7 @@ class Response
* *
* @param string|SimpleXMLElement|null $body * @param string|SimpleXMLElement|null $body
*/ */
public function setBody($body): void public function setBody($body, bool $rawResponse = false): void
{ {
$this->body = null; $this->body = null;
@ -135,7 +135,7 @@ class Response
$this->body = $body; $this->body = $body;
$this->finaliseBody(); $this->finaliseBody($rawResponse);
} }
public function resetBody(): void public function resetBody(): void
@ -153,7 +153,7 @@ class Response
$this->body .= $data; $this->body .= $data;
} }
public function finaliseBody(): void public function finaliseBody(bool $rawResponse = false): void
{ {
if (!$this->hasBody()) if (!$this->hasBody())
{ {
@ -165,8 +165,14 @@ class Response
$this->headers['type'] = 'text/plain'; $this->headers['type'] = 'text/plain';
} }
if (is_string($this->body) && if (
(($this->headers['type'] == 'application/xml') || (substr($this->body, 0, 5) == '<?xml')) !$rawResponse
&& is_string($this->body)
&&
(
($this->headers['type'] == 'application/xml')
|| (substr($this->body, 0, 5) == '<?xml')
)
) )
{ {
$this->body = simplexml_load_string($this->body); $this->body = simplexml_load_string($this->body);
@ -332,8 +338,8 @@ class Response
) )
{ {
$this->error = new Error( $this->error = new Error(
$this->code, 500,
(string) $this->body->Message (string) $this->body->Code . ':' . (string) $this->body->Message
); );
if (isset($this->body->Resource)) if (isset($this->body->Resource))

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Response; namespace Akeeba\S3\Response;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
/** /**
* S3 response error object * S3 response error object

View file

@ -3,14 +3,14 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4; namespace Akeeba\S3;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
/** /**
* Base class for request signing objects. * Base class for request signing objects.
@ -44,7 +44,7 @@ abstract class Signature
*/ */
public static function getSignatureObject(Request $request, string $method = 'v2'): self public static function getSignatureObject(Request $request, string $method = 'v2'): self
{ {
$className = '\\Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Signature\\' . ucfirst($method); $className = __NAMESPACE__ . '\\Signature\\' . ucfirst($method);
return new $className($request); return new $className($request);
} }

View file

@ -3,16 +3,16 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Signature; namespace Akeeba\S3\Signature;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Postproc\Connector\S3v4\Signature; use Akeeba\S3\Signature;
/** /**
* Implements the Amazon AWS v2 signatures * Implements the Amazon AWS v2 signatures
@ -123,7 +123,7 @@ class V2 extends Signature
} }
// AMZ headers must be sorted and sent as separate lines // AMZ headers must be sorted and sent as separate lines
if (sizeof($amz) > 0) if (count($amz) > 0)
{ {
sort($amz); sort($amz);
$amzString = "\n" . implode("\n", $amz); $amzString = "\n" . implode("\n", $amz);
@ -150,8 +150,8 @@ class V2 extends Signature
} }
$stringToSign = $verb . "\n" . $stringToSign = $verb . "\n" .
(isset($headers['Content-MD5']) ? $headers['Content-MD5'] : '') . "\n" . ($headers['Content-MD5'] ?? '') . "\n" .
(isset($headers['Content-Type']) ? $headers['Content-Type'] : '') . "\n" . ($headers['Content-Type'] ?? '') . "\n" .
$headers['Date'] . $headers['Date'] .
$amzString . "\n" . $amzString . "\n" .
$resourcePath; $resourcePath;

View file

@ -3,16 +3,16 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4\Signature; namespace Akeeba\S3\Signature;
// Protection against direct access // Protection against direct access
defined('AKEEBAENGINE') or die(); defined('AKEEBAENGINE') || die();
use Akeeba\Engine\Postproc\Connector\S3v4\Signature; use Akeeba\S3\Signature;
use DateTime; use DateTime;
/** /**
@ -77,14 +77,20 @@ class V4 extends Signature
* http://s3-eu-west-1.amazonaws.com/example instead of http://example.amazonaws.com/ for all authenticated URLs * http://s3-eu-west-1.amazonaws.com/example instead of http://example.amazonaws.com/ for all authenticated URLs
*/ */
$region = $this->request->getConfiguration()->getRegion(); $region = $this->request->getConfiguration()->getRegion();
$bucket = $this->request->getBucket();
$hostname = $this->getPresignedHostnameForRegion($region); $hostname = $this->getPresignedHostnameForRegion($region);
if ($this->isValidBucketName($bucket))
{
$hostname = $bucket . '.' . $hostname;
}
$this->request->setHeader('Host', $hostname); $this->request->setHeader('Host', $hostname);
// Set the expiration time in seconds // Set the expiration time in seconds
$this->request->setHeader('Expires', (int) $lifetime); $this->request->setHeader('Expires', (int) $lifetime);
// Get the query parameters, including the calculated signature // Get the query parameters, including the calculated signature
$bucket = $this->request->getBucket();
$uri = $this->request->getResource(); $uri = $this->request->getResource();
$headers = $this->request->getHeaders(); $headers = $this->request->getHeaders();
$protocol = $https ? 'https' : 'http'; $protocol = $https ? 'https' : 'http';
@ -93,6 +99,11 @@ class V4 extends Signature
// The query parameters are returned serialized; unserialize them, then build and return the URL. // The query parameters are returned serialized; unserialize them, then build and return the URL.
$queryParameters = unserialize($serialisedParams); $queryParameters = unserialize($serialisedParams);
if ($this->isValidBucketName($bucket) && strpos($uri, '/' . $bucket) === 0)
{
$uri = substr($uri, strlen($bucket) + 1);
}
$query = http_build_query($queryParameters); $query = http_build_query($queryParameters);
$url = $protocol . '://' . $headers['Host'] . $uri; $url = $protocol . '://' . $headers['Host'] . $uri;
@ -208,12 +219,14 @@ class V4 extends Signature
// The canonical URI is the resource path // The canonical URI is the resource path
$canonicalURI = $resourcePath; $canonicalURI = $resourcePath;
$bucketResource = '/' . $bucket; $bucketResource = '/' . $bucket;
$regionalHostname = ($headers['Host'] != 's3.amazonaws.com') && ($headers['Host'] != $bucket . '.s3.amazonaws.com'); $regionalHostname = ($headers['Host'] != 's3.amazonaws.com')
&& ($headers['Host'] != $bucket . '.s3.amazonaws.com');
// Special case: if the canonical URI ends in /?location the bucket name DOES count as part of the canonical URL // Special case: if the canonical URI ends in /?location the bucket name DOES count as part of the canonical URL
// even though the Host is s3.amazonaws.com (in which case it normally shouldn't count). Yeah, I know, it makes // even though the Host is s3.amazonaws.com (in which case it normally shouldn't count). Yeah, I know, it makes
// no sense!!! // no sense!!!
if (!$regionalHostname && ($headers['Host'] == 's3.amazonaws.com') && (substr($canonicalURI, -10) == '/?location')) if (!$regionalHostname && ($headers['Host'] == 's3.amazonaws.com')
&& (substr($canonicalURI, -10) == '/?location'))
{ {
$regionalHostname = true; $regionalHostname = true;
} }
@ -290,8 +303,31 @@ class V4 extends Signature
$headers['Date'] = ''; $headers['Date'] = '';
} }
/**
* The Date in the String-to-Sign is a messy situation.
*
* Amazon's documentation says it must be in ISO 8601 format: `Ymd\THis\Z`. Unfortunately, Amazon's
* documentation is actually wrong :troll_face: The actual Amazon S3 service expects the date to be formatted as
* per RFC1123.
*
* Most third party implementations have caught up to the fact that Amazon has documented the v4 signatures
* wrongly (naughty AWS!) and accept either format.
*
* Some other third party implementations, which never bothered to validate their implementations against Amazon
* S3 proper, only expect what Amazon has documented as "ISO 8601". Therefore, we detect third party services
* and switch to the as-documented date format.
*
* Some other third party services, like Wasabi, are broken in yet a different way. They will only use the date
* from the x-amz-date header, WITHOUT falling back to the Date header if the former is not present. This is
* the opposite of Amazon S3 proper which does expect the Date header. That's why the Request class sets both
* headers if the request is made to a service _other_ than Amazon S3 proper.
*/
$dateToSignFor = strpos($headers['Host'], '.amazonaws.com') !== false
? $headers['Date']
: $signatureDate->format('Ymd\THis\Z');
$stringToSign = "AWS4-HMAC-SHA256\n" . $stringToSign = "AWS4-HMAC-SHA256\n" .
$headers['Date'] . "\n" . $dateToSignFor . "\n" .
$credentialScope . "\n" . $credentialScope . "\n" .
$hashedCanonicalRequest; $hashedCanonicalRequest;
@ -365,8 +401,15 @@ class V4 extends Signature
* @return string * @return string
*/ */
private function getPresignedHostnameForRegion(string $region): string private function getPresignedHostnameForRegion(string $region): string
{
$config = $this->request->getConfiguration();
$endpoint = $config->getEndpoint();
if (empty($endpoint))
{ {
$endpoint = 's3.' . $region . '.amazonaws.com'; $endpoint = 's3.' . $region . '.amazonaws.com';
}
$dualstackEnabled = $this->request->getConfiguration()->getDualstackUrl(); $dualstackEnabled = $this->request->getConfiguration()->getDualstackUrl();
// If dual-stack URLs are enabled then prepend the endpoint // If dual-stack URLs are enabled then prepend the endpoint
@ -382,4 +425,83 @@ class V4 extends Signature
return $endpoint; return $endpoint;
} }
/**
* Is this a valid bucket name?
*
* @param string $bucketName The bucket name to check
* @param bool $asSubdomain Should I put additional restrictions for use as a subdomain?
*
* @return bool
* @since 2.3.1
*
* @see https://docs.aws.amazon.com/AmazonS3/latest/userguide/bucketnamingrules.html
*/
private function isValidBucketName(string $bucketName, bool $asSubdomain = true): bool
{
/**
* If there are dots in the bucket name I can't use it as a subdomain.
*
* "If you include dots in a bucket's name, you can't use virtual-host-style addressing over HTTPS, unless you
* perform your own certificate validation. This is because the security certificates used for virtual hosting
* of buckets don't work for buckets with dots in their names."
*/
if ($asSubdomain && strpos($bucketName, '.') !== false)
{
return false;
}
/**
* - Bucket names must be between 3 (min) and 63 (max) characters long.
* - Bucket names can consist only of lowercase letters, numbers, dots (.), and hyphens (-).
*/
if (!preg_match('/^[a-z0-9\-.]{3,63}$/', $bucketName))
{
return false;
}
// Bucket names must begin and end with a letter or number.
if (!preg_match('/^[a-z0-9].*[a-z0-9]$/', $bucketName))
{
return false;
}
// Bucket names must not contain two adjacent periods.
if (preg_match('/\.\./', $bucketName))
{
return false;
}
// Bucket names must not be formatted as an IP address (for example, 192.168.5.4).
if (preg_match('/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/', $bucketName))
{
return false;
}
// Bucket names must not start with the prefix xn--.
if (strpos($bucketName, 'xn--') === 0)
{
return false;
}
// Bucket names must not start with the prefix sthree- and the prefix sthree-configurator.
if (strpos($bucketName, 'sthree-') === 0)
{
return false;
}
// Bucket names must not end with the suffix -s3alias.
if (substr($bucketName, -8) === '-s3alias')
{
return false;
}
// Bucket names must not end with the suffix --ol-s3.
if (substr($bucketName, -7) === '--ol-s3')
{
return false;
}
return true;
}
} }

View file

@ -3,11 +3,11 @@
* Akeeba Engine * Akeeba Engine
* *
* @package akeebaengine * @package akeebaengine
* @copyright Copyright (c)2006-2020 Nicholas K. Dionysopoulos / Akeeba Ltd * @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later * @license GNU General Public License version 3, or later
*/ */
namespace Akeeba\Engine\Postproc\Connector\S3v4; namespace Akeeba\S3;
/** /**
* Amazon S3 Storage Classes * Amazon S3 Storage Classes
@ -28,39 +28,39 @@ class StorageClass
/** /**
* Amazon S3 Standard (S3 Standard) * Amazon S3 Standard (S3 Standard)
*/ */
const STANDARD = 'STANDARD'; public const STANDARD = 'STANDARD';
/** /**
* Reduced redundancy storage * Reduced redundancy storage
* *
* Not recommended anymore. Use INTELLIGENT_TIERING instead. * Not recommended anymore. Use INTELLIGENT_TIERING instead.
*/ */
const REDUCED_REDUNDANCY = 'REDUCED_REDUNDANCY'; public const REDUCED_REDUNDANCY = 'REDUCED_REDUNDANCY';
/** /**
* Amazon S3 Intelligent-Tiering (S3 Intelligent-Tiering) * Amazon S3 Intelligent-Tiering (S3 Intelligent-Tiering)
*/ */
const INTELLIGENT_TIERING = 'INTELLIGENT_TIERING'; public const INTELLIGENT_TIERING = 'INTELLIGENT_TIERING';
/** /**
* Amazon S3 Standard-Infrequent Access (S3 Standard-IA) * Amazon S3 Standard-Infrequent Access (S3 Standard-IA)
*/ */
const STANDARD_IA = 'STANDARD_IA'; public const STANDARD_IA = 'STANDARD_IA';
/** /**
* Amazon S3 One Zone-Infrequent Access (S3 One Zone-IA) * Amazon S3 One Zone-Infrequent Access (S3 One Zone-IA)
*/ */
const ONEZONE_IA = 'ONEZONE_IA'; public const ONEZONE_IA = 'ONEZONE_IA';
/** /**
* Amazon S3 Glacier (S3 Glacier) * Amazon S3 Glacier (S3 Glacier)
*/ */
const GLACIER = 'GLACIER'; public const GLACIER = 'GLACIER';
/** /**
* Amazon S3 Glacier Deep Archive (S3 Glacier Deep Archive) * Amazon S3 Glacier Deep Archive (S3 Glacier Deep Archive)
*/ */
const DEEP_ARCHIVE = 'DEEP_ARCHIVE'; public const DEEP_ARCHIVE = 'DEEP_ARCHIVE';
/** /**
* Manipulate the $headers array, setting the X-Amz-Storage-Class header for the requested storage class. * Manipulate the $headers array, setting the X-Amz-Storage-Class header for the requested storage class.

View file

@ -0,0 +1,35 @@
<?php
/**
* Akeeba Engine
*
* @package akeebaengine
* @copyright Copyright (c)2006-2023 Nicholas K. Dionysopoulos / Akeeba Ltd
* @license GNU General Public License version 3, or later
*/
/**
* Automatic aliasing of the old namespace to the new, as you use each old class.
*/
spl_autoload_register(
static function (string $className)
{
$oldNS = 'Akeeba\Engine\Postproc\Connector\S3v4';
$newNS = 'Akeeba\S3';
$className = trim($className, '\\');
if (strpos($className, $oldNS) !== 0)
{
return false;
}
$newClassName = $newNS . '\\' . trim(substr($className, strlen($oldNS)), '\\');
if (class_exists($newClassName, true))
{
class_alias($newClassName, $className, false);
}
return true;
}
);

View file

@ -6,31 +6,31 @@ $vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir); $baseDir = dirname($vendorDir);
return array( return array(
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Acl' => $vendorDir . '/akeeba/s3/src/Acl.php', 'Akeeba\\S3\\Acl' => $vendorDir . '/akeeba/s3/src/Acl.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Configuration' => $vendorDir . '/akeeba/s3/src/Configuration.php', 'Akeeba\\S3\\Configuration' => $vendorDir . '/akeeba/s3/src/Configuration.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Connector' => $vendorDir . '/akeeba/s3/src/Connector.php', 'Akeeba\\S3\\Connector' => $vendorDir . '/akeeba/s3/src/Connector.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\CannotDeleteFile' => $vendorDir . '/akeeba/s3/src/Exception/CannotDeleteFile.php', 'Akeeba\\S3\\Exception\\CannotDeleteFile' => $vendorDir . '/akeeba/s3/src/Exception/CannotDeleteFile.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\CannotGetBucket' => $vendorDir . '/akeeba/s3/src/Exception/CannotGetBucket.php', 'Akeeba\\S3\\Exception\\CannotGetBucket' => $vendorDir . '/akeeba/s3/src/Exception/CannotGetBucket.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\CannotGetFile' => $vendorDir . '/akeeba/s3/src/Exception/CannotGetFile.php', 'Akeeba\\S3\\Exception\\CannotGetFile' => $vendorDir . '/akeeba/s3/src/Exception/CannotGetFile.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\CannotListBuckets' => $vendorDir . '/akeeba/s3/src/Exception/CannotListBuckets.php', 'Akeeba\\S3\\Exception\\CannotListBuckets' => $vendorDir . '/akeeba/s3/src/Exception/CannotListBuckets.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\CannotOpenFileForRead' => $vendorDir . '/akeeba/s3/src/Exception/CannotOpenFileForRead.php', 'Akeeba\\S3\\Exception\\CannotOpenFileForRead' => $vendorDir . '/akeeba/s3/src/Exception/CannotOpenFileForRead.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\CannotOpenFileForWrite' => $vendorDir . '/akeeba/s3/src/Exception/CannotOpenFileForWrite.php', 'Akeeba\\S3\\Exception\\CannotOpenFileForWrite' => $vendorDir . '/akeeba/s3/src/Exception/CannotOpenFileForWrite.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\CannotPutFile' => $vendorDir . '/akeeba/s3/src/Exception/CannotPutFile.php', 'Akeeba\\S3\\Exception\\CannotPutFile' => $vendorDir . '/akeeba/s3/src/Exception/CannotPutFile.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\ConfigurationError' => $vendorDir . '/akeeba/s3/src/Exception/ConfigurationError.php', 'Akeeba\\S3\\Exception\\ConfigurationError' => $vendorDir . '/akeeba/s3/src/Exception/ConfigurationError.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\InvalidAccessKey' => $vendorDir . '/akeeba/s3/src/Exception/InvalidAccessKey.php', 'Akeeba\\S3\\Exception\\InvalidAccessKey' => $vendorDir . '/akeeba/s3/src/Exception/InvalidAccessKey.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\InvalidBody' => $vendorDir . '/akeeba/s3/src/Exception/InvalidBody.php', 'Akeeba\\S3\\Exception\\InvalidBody' => $vendorDir . '/akeeba/s3/src/Exception/InvalidBody.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\InvalidEndpoint' => $vendorDir . '/akeeba/s3/src/Exception/InvalidEndpoint.php', 'Akeeba\\S3\\Exception\\InvalidEndpoint' => $vendorDir . '/akeeba/s3/src/Exception/InvalidEndpoint.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\InvalidFilePointer' => $vendorDir . '/akeeba/s3/src/Exception/InvalidFilePointer.php', 'Akeeba\\S3\\Exception\\InvalidFilePointer' => $vendorDir . '/akeeba/s3/src/Exception/InvalidFilePointer.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\InvalidRegion' => $vendorDir . '/akeeba/s3/src/Exception/InvalidRegion.php', 'Akeeba\\S3\\Exception\\InvalidRegion' => $vendorDir . '/akeeba/s3/src/Exception/InvalidRegion.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\InvalidSecretKey' => $vendorDir . '/akeeba/s3/src/Exception/InvalidSecretKey.php', 'Akeeba\\S3\\Exception\\InvalidSecretKey' => $vendorDir . '/akeeba/s3/src/Exception/InvalidSecretKey.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\InvalidSignatureMethod' => $vendorDir . '/akeeba/s3/src/Exception/InvalidSignatureMethod.php', 'Akeeba\\S3\\Exception\\InvalidSignatureMethod' => $vendorDir . '/akeeba/s3/src/Exception/InvalidSignatureMethod.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\PropertyNotFound' => $vendorDir . '/akeeba/s3/src/Exception/PropertyNotFound.php', 'Akeeba\\S3\\Exception\\PropertyNotFound' => $vendorDir . '/akeeba/s3/src/Exception/PropertyNotFound.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Input' => $vendorDir . '/akeeba/s3/src/Input.php', 'Akeeba\\S3\\Input' => $vendorDir . '/akeeba/s3/src/Input.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Request' => $vendorDir . '/akeeba/s3/src/Request.php', 'Akeeba\\S3\\Request' => $vendorDir . '/akeeba/s3/src/Request.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Response' => $vendorDir . '/akeeba/s3/src/Response.php', 'Akeeba\\S3\\Response' => $vendorDir . '/akeeba/s3/src/Response.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Response\\Error' => $vendorDir . '/akeeba/s3/src/Response/Error.php', 'Akeeba\\S3\\Response\\Error' => $vendorDir . '/akeeba/s3/src/Response/Error.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Signature' => $vendorDir . '/akeeba/s3/src/Signature.php', 'Akeeba\\S3\\Signature' => $vendorDir . '/akeeba/s3/src/Signature.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Signature\\V2' => $vendorDir . '/akeeba/s3/src/Signature/V2.php', 'Akeeba\\S3\\Signature\\V2' => $vendorDir . '/akeeba/s3/src/Signature/V2.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Signature\\V4' => $vendorDir . '/akeeba/s3/src/Signature/V4.php', 'Akeeba\\S3\\Signature\\V4' => $vendorDir . '/akeeba/s3/src/Signature/V4.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\StorageClass' => $vendorDir . '/akeeba/s3/src/StorageClass.php', 'Akeeba\\S3\\StorageClass' => $vendorDir . '/akeeba/s3/src/StorageClass.php',
); );

View file

@ -0,0 +1,10 @@
<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir);
return array(
'714ccd4b330431237faf946f71c4c9a4' => $vendorDir . '/akeeba/s3/src/aliasing.php',
);

View file

@ -6,5 +6,5 @@ $vendorDir = dirname(dirname(__FILE__));
$baseDir = dirname($vendorDir); $baseDir = dirname($vendorDir);
return array( return array(
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\' => array($vendorDir . '/akeeba/s3/src'), 'Akeeba\\S3\\' => array($vendorDir . '/akeeba/s3/src'),
); );

View file

@ -50,6 +50,24 @@ class ComposerAutoloaderInitS3StorageAddon
$loader->register(true); $loader->register(true);
if ($useStaticLoader) {
$includeFiles = Composer\Autoload\ComposerStaticInitS3StorageAddon::$files;
} else {
$includeFiles = require __DIR__ . '/autoload_files.php';
}
foreach ($includeFiles as $fileIdentifier => $file) {
composerRequireS3StorageAddon($fileIdentifier, $file);
}
return $loader; return $loader;
} }
} }
function composerRequireS3StorageAddon($fileIdentifier, $file)
{
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
require $file;
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
}
}

View file

@ -6,48 +6,52 @@ namespace Composer\Autoload;
class ComposerStaticInitS3StorageAddon class ComposerStaticInitS3StorageAddon
{ {
public static $files = array (
'714ccd4b330431237faf946f71c4c9a4' => __DIR__ . '/..' . '/akeeba/s3/src/aliasing.php',
);
public static $prefixLengthsPsr4 = array ( public static $prefixLengthsPsr4 = array (
'A' => 'A' =>
array ( array (
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\' => 38, 'Akeeba\\S3\\' => 10,
), ),
); );
public static $prefixDirsPsr4 = array ( public static $prefixDirsPsr4 = array (
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\' => 'Akeeba\\S3\\' =>
array ( array (
0 => __DIR__ . '/..' . '/akeeba/s3/src', 0 => __DIR__ . '/..' . '/akeeba/s3/src',
), ),
); );
public static $classMap = array ( public static $classMap = array (
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Acl' => __DIR__ . '/..' . '/akeeba/s3/src/Acl.php', 'Akeeba\\S3\\Acl' => __DIR__ . '/..' . '/akeeba/s3/src/Acl.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Configuration' => __DIR__ . '/..' . '/akeeba/s3/src/Configuration.php', 'Akeeba\\S3\\Configuration' => __DIR__ . '/..' . '/akeeba/s3/src/Configuration.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Connector' => __DIR__ . '/..' . '/akeeba/s3/src/Connector.php', 'Akeeba\\S3\\Connector' => __DIR__ . '/..' . '/akeeba/s3/src/Connector.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\CannotDeleteFile' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/CannotDeleteFile.php', 'Akeeba\\S3\\Exception\\CannotDeleteFile' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/CannotDeleteFile.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\CannotGetBucket' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/CannotGetBucket.php', 'Akeeba\\S3\\Exception\\CannotGetBucket' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/CannotGetBucket.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\CannotGetFile' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/CannotGetFile.php', 'Akeeba\\S3\\Exception\\CannotGetFile' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/CannotGetFile.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\CannotListBuckets' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/CannotListBuckets.php', 'Akeeba\\S3\\Exception\\CannotListBuckets' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/CannotListBuckets.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\CannotOpenFileForRead' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/CannotOpenFileForRead.php', 'Akeeba\\S3\\Exception\\CannotOpenFileForRead' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/CannotOpenFileForRead.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\CannotOpenFileForWrite' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/CannotOpenFileForWrite.php', 'Akeeba\\S3\\Exception\\CannotOpenFileForWrite' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/CannotOpenFileForWrite.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\CannotPutFile' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/CannotPutFile.php', 'Akeeba\\S3\\Exception\\CannotPutFile' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/CannotPutFile.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\ConfigurationError' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/ConfigurationError.php', 'Akeeba\\S3\\Exception\\ConfigurationError' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/ConfigurationError.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\InvalidAccessKey' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/InvalidAccessKey.php', 'Akeeba\\S3\\Exception\\InvalidAccessKey' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/InvalidAccessKey.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\InvalidBody' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/InvalidBody.php', 'Akeeba\\S3\\Exception\\InvalidBody' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/InvalidBody.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\InvalidEndpoint' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/InvalidEndpoint.php', 'Akeeba\\S3\\Exception\\InvalidEndpoint' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/InvalidEndpoint.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\InvalidFilePointer' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/InvalidFilePointer.php', 'Akeeba\\S3\\Exception\\InvalidFilePointer' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/InvalidFilePointer.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\InvalidRegion' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/InvalidRegion.php', 'Akeeba\\S3\\Exception\\InvalidRegion' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/InvalidRegion.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\InvalidSecretKey' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/InvalidSecretKey.php', 'Akeeba\\S3\\Exception\\InvalidSecretKey' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/InvalidSecretKey.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\InvalidSignatureMethod' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/InvalidSignatureMethod.php', 'Akeeba\\S3\\Exception\\InvalidSignatureMethod' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/InvalidSignatureMethod.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Exception\\PropertyNotFound' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/PropertyNotFound.php', 'Akeeba\\S3\\Exception\\PropertyNotFound' => __DIR__ . '/..' . '/akeeba/s3/src/Exception/PropertyNotFound.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Input' => __DIR__ . '/..' . '/akeeba/s3/src/Input.php', 'Akeeba\\S3\\Input' => __DIR__ . '/..' . '/akeeba/s3/src/Input.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Request' => __DIR__ . '/..' . '/akeeba/s3/src/Request.php', 'Akeeba\\S3\\Request' => __DIR__ . '/..' . '/akeeba/s3/src/Request.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Response' => __DIR__ . '/..' . '/akeeba/s3/src/Response.php', 'Akeeba\\S3\\Response' => __DIR__ . '/..' . '/akeeba/s3/src/Response.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Response\\Error' => __DIR__ . '/..' . '/akeeba/s3/src/Response/Error.php', 'Akeeba\\S3\\Response\\Error' => __DIR__ . '/..' . '/akeeba/s3/src/Response/Error.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Signature' => __DIR__ . '/..' . '/akeeba/s3/src/Signature.php', 'Akeeba\\S3\\Signature' => __DIR__ . '/..' . '/akeeba/s3/src/Signature.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Signature\\V2' => __DIR__ . '/..' . '/akeeba/s3/src/Signature/V2.php', 'Akeeba\\S3\\Signature\\V2' => __DIR__ . '/..' . '/akeeba/s3/src/Signature/V2.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\Signature\\V4' => __DIR__ . '/..' . '/akeeba/s3/src/Signature/V4.php', 'Akeeba\\S3\\Signature\\V4' => __DIR__ . '/..' . '/akeeba/s3/src/Signature/V4.php',
'Akeeba\\Engine\\Postproc\\Connector\\S3v4\\StorageClass' => __DIR__ . '/..' . '/akeeba/s3/src/StorageClass.php', 'Akeeba\\S3\\StorageClass' => __DIR__ . '/..' . '/akeeba/s3/src/StorageClass.php',
); );
public static function getInitializer(ClassLoader $loader) public static function getInitializer(ClassLoader $loader)

View file

@ -1,35 +1,38 @@
[ [
{ {
"name": "akeeba/s3", "name": "akeeba/s3",
"version": "2.0.0", "version": "2.3.1",
"version_normalized": "2.0.0.0", "version_normalized": "2.3.1.0",
"source": { "source": {
"type": "git", "type": "git",
"url": "https://github.com/akeeba/s3.git", "url": "https://github.com/akeeba/s3.git",
"reference": "01520dae1f736555e08efda0ddc1044701bd340a" "reference": "7f5b3e929c93eb02ba24472560c0cbbef735aed9"
}, },
"dist": { "dist": {
"type": "zip", "type": "zip",
"url": "https://api.github.com/repos/akeeba/s3/zipball/01520dae1f736555e08efda0ddc1044701bd340a", "url": "https://api.github.com/repos/akeeba/s3/zipball/7f5b3e929c93eb02ba24472560c0cbbef735aed9",
"reference": "01520dae1f736555e08efda0ddc1044701bd340a", "reference": "7f5b3e929c93eb02ba24472560c0cbbef735aed9",
"shasum": "" "shasum": ""
}, },
"require": { "require": {
"ext-curl": "*", "ext-curl": "*",
"ext-simplexml": "*", "ext-simplexml": "*",
"php": ">=7.1.0 <8.1" "php": ">=7.1.0 <8.4"
}, },
"time": "2020-11-30T14:03:55+00:00", "time": "2023-09-26T11:40:10+00:00",
"type": "library", "type": "library",
"installation-source": "dist", "installation-source": "dist",
"autoload": { "autoload": {
"files": [
"src/aliasing.php"
],
"psr-4": { "psr-4": {
"Akeeba\\Engine\\Postproc\\Connector\\S3v4\\": "src" "Akeeba\\S3\\": "src"
} }
}, },
"notification-url": "https://packagist.org/downloads/", "notification-url": "https://packagist.org/downloads/",
"license": [ "license": [
"GPL-3.0+" "GPL-3.0-or-later"
], ],
"authors": [ "authors": [
{ {