From 37aaf5fe7129aae6533b883ee775bae74fe101f0 Mon Sep 17 00:00:00 2001 From: Luciano Mammino Date: Fri, 12 Apr 2013 03:05:24 +0300 Subject: [PATCH 001/426] Fix OpenCloud adapter exists always return false Should fix #160 --- src/Gaufrette/Adapter/OpenCloud.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Gaufrette/Adapter/OpenCloud.php b/src/Gaufrette/Adapter/OpenCloud.php index e3e604200..aca0cdd85 100644 --- a/src/Gaufrette/Adapter/OpenCloud.php +++ b/src/Gaufrette/Adapter/OpenCloud.php @@ -118,7 +118,7 @@ public function write($key, $content) public function exists($key) { $this->initialize(); - return $this->tryGetObject($key)->bytes > 0; + return intval($this->tryGetObject($key)->content_length) > 0; } /** @@ -222,4 +222,4 @@ protected function tryGetObject($key) return false; } } -} \ No newline at end of file +} From c105c742edbe3ce90ff6db8929631d81f368e2af Mon Sep 17 00:00:00 2001 From: Luciano Mammino Date: Fri, 12 Apr 2013 13:34:53 +0300 Subject: [PATCH 002/426] Update OpenCloud.php Changed exists check condition --- src/Gaufrette/Adapter/OpenCloud.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/OpenCloud.php b/src/Gaufrette/Adapter/OpenCloud.php index aca0cdd85..8990b5e3e 100644 --- a/src/Gaufrette/Adapter/OpenCloud.php +++ b/src/Gaufrette/Adapter/OpenCloud.php @@ -118,7 +118,7 @@ public function write($key, $content) public function exists($key) { $this->initialize(); - return intval($this->tryGetObject($key)->content_length) > 0; + return ($this->tryGetObject($key) !== false); } /** From 1ebd120d6c5f7a53027f114719d87299b3540701 Mon Sep 17 00:00:00 2001 From: l3l0 Date: Sat, 20 Apr 2013 09:46:11 +0200 Subject: [PATCH 003/426] Make pecl install verbose to check what is falling at travis when install ssh2-beta ext. to check what is falling at travis when install ssh2-beta ext. --- bin/configure_test_env.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/configure_test_env.sh b/bin/configure_test_env.sh index ee582373c..394e5f259 100755 --- a/bin/configure_test_env.sh +++ b/bin/configure_test_env.sh @@ -4,6 +4,6 @@ sudo apt-get update -qq sudo apt-get install -qq libssh2-1-dev libssh2-php sudo pecl install mongo &> /dev/null touch .interactive -(sudo pecl install -f ssh2-beta < .interactive) &> /dev/null +(sudo pecl install -f ssh2-beta < .interactive) cp tests/Gaufrette/Functional/adapters/DoctrineDbal.php.dist tests/Gaufrette/Functional/adapters/DoctrineDbal.php -f From f2af9392006ad6656d1edfe9283ba841a1f2b360 Mon Sep 17 00:00:00 2001 From: l3l0 Date: Sat, 20 Apr 2013 09:55:40 +0200 Subject: [PATCH 004/426] Remove sudo from pecl command runned at travis. --- bin/configure_test_env.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/configure_test_env.sh b/bin/configure_test_env.sh index 394e5f259..7e3cafbb7 100755 --- a/bin/configure_test_env.sh +++ b/bin/configure_test_env.sh @@ -2,8 +2,8 @@ sudo apt-get update -qq sudo apt-get install -qq libssh2-1-dev libssh2-php -sudo pecl install mongo &> /dev/null +pecl install mongo &> /dev/null touch .interactive -(sudo pecl install -f ssh2-beta < .interactive) +(pecl install -f ssh2-beta < .interactive) cp tests/Gaufrette/Functional/adapters/DoctrineDbal.php.dist tests/Gaufrette/Functional/adapters/DoctrineDbal.php -f From 1540391e4115ac7c01255e07e02ea3250ca31c51 Mon Sep 17 00:00:00 2001 From: l3l0 Date: Sat, 20 Apr 2013 10:12:21 +0200 Subject: [PATCH 005/426] Marked RackspaceCloudfiles as depreciated and change repo in composer.json so it will not fail. --- composer.json | 4 +- composer.lock | 44 +++++++++++++++++-- src/Gaufrette/Adapter/RackspaceCloudfiles.php | 1 + 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/composer.json b/composer.json index 6e46d0264..fc4c8a17b 100644 --- a/composer.json +++ b/composer.json @@ -23,12 +23,12 @@ "name": "rackspace/php-cloudfiles", "version": "master", "source": { - "url": "git://github.com/rackspace/php-cloudfiles.git", + "url": "git://github.com/rackerlabs/php-cloudfiles.git", "type": "git", "reference": "master" }, "dist": { - "url": "https://github.com/rackspace/php-cloudfiles/zipball/master", + "url": "https://github.com/rackerlabs/php-cloudfiles/zipball/master", "type": "zip" }, "autoload": { diff --git a/composer.lock b/composer.lock index 480de3e9b..9d989bc0b 100644 --- a/composer.lock +++ b/composer.lock @@ -1,5 +1,5 @@ { - "hash": "43a051992b6dbce50d171784fadcc0d1", + "hash": "0557c6bf3b54057cf0c8a7f25ef0d79d", "packages": [ ], @@ -1114,12 +1114,12 @@ "version": "master", "source": { "type": "git", - "url": "git://github.com/rackspace/php-cloudfiles.git", + "url": "git://github.com/rackerlabs/php-cloudfiles.git", "reference": "master" }, "dist": { "type": "zip", - "url": "https://github.com/rackspace/php-cloudfiles/zipball/master", + "url": "https://github.com/rackerlabs/php-cloudfiles/zipball/master", "reference": null, "shasum": null }, @@ -1129,7 +1129,42 @@ "cloudfiles.php" ] }, - "time": "2013-01-10 18:09:51" + "time": "2013-02-20 21:04:46" + }, + { + "name": "rackspace/php-opencloud", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/rackspace/php-opencloud.git", + "reference": "c3850c5e7d749357873f0ebf6bcde9692f33049c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/rackspace/php-opencloud/zipball/c3850c5e7d749357873f0ebf6bcde9692f33049c", + "reference": "c3850c5e7d749357873f0ebf6bcde9692f33049c", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.3.0" + }, + "time": "2013-03-30 12:53:51", + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Rackspace open cloud php library", + "keywords": [ + "cloudfiles", + "opencloud" + ] }, { "name": "symfony/console", @@ -1317,6 +1352,7 @@ ], "minimum-stability": "dev", "stability-flags": { + "rackspace/php-opencloud": 20, "phpspec/phpspec2": 20 } } diff --git a/src/Gaufrette/Adapter/RackspaceCloudfiles.php b/src/Gaufrette/Adapter/RackspaceCloudfiles.php index a1f89c4c5..bd36a65a1 100644 --- a/src/Gaufrette/Adapter/RackspaceCloudfiles.php +++ b/src/Gaufrette/Adapter/RackspaceCloudfiles.php @@ -11,6 +11,7 @@ * * @package Gaufrette * @author Antoine Hérault + * @deprecated php-cloudfiles is deprecated and will be unavailable after August 1, 2013 */ class RackspaceCloudfiles implements Adapter, ChecksumCalculator From fc7d1cf4045fa3b51d6fbdf88315ce8b67aee549 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Haso=C5=88?= Date: Mon, 22 Apr 2013 22:53:17 +0200 Subject: [PATCH 006/426] Fixed Filesystem::listKeys --- spec/Gaufrette/Filesystem.php | 4 ++-- src/Gaufrette/Filesystem.php | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/Gaufrette/Filesystem.php b/spec/Gaufrette/Filesystem.php index f2e90c6a7..f76ddb42f 100644 --- a/spec/Gaufrette/Filesystem.php +++ b/spec/Gaufrette/Filesystem.php @@ -289,7 +289,7 @@ function it_should_match_listed_keys_using_specified_pattern($adapter) ); $this->listKeys('Key')->shouldReturn( array( - 'keys' => array('testKey', 'KeyTest'), + 'keys' => array('KeyTest'), 'dirs' => array() ) ); @@ -326,7 +326,7 @@ function it_should_decide_which_key_is_dir_using_adapter($adapter) $this->listKeys('Key')->shouldReturn( array( 'keys' => array('KeyTest'), - 'dirs' => array('testKey') + 'dirs' => array() ) ); } diff --git a/src/Gaufrette/Filesystem.php b/src/Gaufrette/Filesystem.php index 8c53ca942..42928615e 100644 --- a/src/Gaufrette/Filesystem.php +++ b/src/Gaufrette/Filesystem.php @@ -187,7 +187,7 @@ public function listKeys($prefix = '') $keys = array(); foreach ($this->keys() as $key) { - if (empty($prefix) || false !== strpos($key, $prefix)) { + if (empty($prefix) || 0 === strpos($key, $prefix)) { if ($this->adapter->isDirectory($key)) { $dirs[] = $key; } else { From 9485b3bd0ea7490b87c4e121d37978b2e7a0b810 Mon Sep 17 00:00:00 2001 From: djoos Date: Thu, 25 Apr 2013 14:52:44 +0000 Subject: [PATCH 007/426] OpenCloud\ObjectStore > OpenCloud\ObjectStore\Service --- src/Gaufrette/Adapter/OpenCloud.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Gaufrette/Adapter/OpenCloud.php b/src/Gaufrette/Adapter/OpenCloud.php index 8990b5e3e..7c41a40c7 100644 --- a/src/Gaufrette/Adapter/OpenCloud.php +++ b/src/Gaufrette/Adapter/OpenCloud.php @@ -4,7 +4,7 @@ use Gaufrette\Adapter; use OpenCloud\ObjectStore\Container; -use OpenCloud\ObjectStore; +use OpenCloud\ObjectStore\Service; use OpenCloud\ObjectStore\CreateUpdateError; use OpenCloud\ObjectStore\ObjFetchError; @@ -38,7 +38,7 @@ class OpenCloud implements Adapter, */ protected $container; - public function __construct(ObjectStore $objectStore, $containerName, $createContainer = false, $detectContentType = true) + public function __construct(Service $objectStore, $containerName, $createContainer = false, $detectContentType = true) { $this->objectStore = $objectStore; $this->containerName = $containerName; From 7eeac5285179f3a1771b783cb96f4fee3a95b201 Mon Sep 17 00:00:00 2001 From: djoos Date: Fri, 26 Apr 2013 09:09:53 +0000 Subject: [PATCH 008/426] Fix invalid Error-class import(s) --- src/Gaufrette/Adapter/OpenCloud.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Gaufrette/Adapter/OpenCloud.php b/src/Gaufrette/Adapter/OpenCloud.php index 7c41a40c7..4e1016571 100644 --- a/src/Gaufrette/Adapter/OpenCloud.php +++ b/src/Gaufrette/Adapter/OpenCloud.php @@ -5,8 +5,8 @@ use Gaufrette\Adapter; use OpenCloud\ObjectStore\Container; use OpenCloud\ObjectStore\Service; -use OpenCloud\ObjectStore\CreateUpdateError; -use OpenCloud\ObjectStore\ObjFetchError; +use OpenCloud\Base\Exceptions\CreateUpdateError; +use OpenCloud\Base\Exceptions\ObjFetchError; /** * OpenCloud adapter From d2ad2bb85f821fe8edfc7617069c95c1b503fb7a Mon Sep 17 00:00:00 2001 From: Nicholas Masters Date: Fri, 17 May 2013 16:08:09 +0100 Subject: [PATCH 009/426] Escape forward slashes --- src/Gaufrette/Adapter/Apc.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Gaufrette/Adapter/Apc.php b/src/Gaufrette/Adapter/Apc.php index 4dd9d4e21..41be154b0 100644 --- a/src/Gaufrette/Adapter/Apc.php +++ b/src/Gaufrette/Adapter/Apc.php @@ -77,7 +77,7 @@ public function keys() $keys = array(); foreach ($cachedKeys as $key => $value) { - $pattern = sprintf('/^%s/', preg_quote($this->prefix)); + $pattern = sprintf('/^%s/', preg_quote($this->prefix, '/')); $keys[] = preg_replace($pattern, '', $key); } sort($keys); @@ -142,7 +142,7 @@ public function computePath($key) */ protected function getCachedKeysIterator($key = '', $format = APC_ITER_NONE) { - $pattern = sprintf('/^%s/', preg_quote($this->prefix.$key)); + $pattern = sprintf('/^%s/', preg_quote($this->prefix.$key, '/')); return new \APCIterator('user', $pattern, $format); } From 2fe4f9361a543a381d099ee5584b3144d1a36446 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Haso=C5=88?= Date: Fri, 12 Apr 2013 22:49:53 +0200 Subject: [PATCH 010/426] Added phpseclib sftp adapter --- composer.json | 6 +- composer.lock | 373 ++++++++++++------ spec/Gaufrette/Adapter/PhpseclibSftp.php | 136 +++++++ src/Gaufrette/Adapter/PhpseclibSftp.php | 238 +++++++++++ .../Functional/Adapter/PhpseclibSftpTest.php | 9 + .../adapters/PhpseclibSftp.php.dist | 10 + 6 files changed, 639 insertions(+), 133 deletions(-) create mode 100644 spec/Gaufrette/Adapter/PhpseclibSftp.php create mode 100644 src/Gaufrette/Adapter/PhpseclibSftp.php create mode 100644 tests/Gaufrette/Functional/Adapter/PhpseclibSftpTest.php create mode 100644 tests/Gaufrette/Functional/adapters/PhpseclibSftp.php.dist diff --git a/composer.json b/composer.json index fc4c8a17b..ba2f53574 100644 --- a/composer.json +++ b/composer.json @@ -35,10 +35,6 @@ "files": ["cloudfiles.php"] } } - }, - { - "type": "vcs", - "url": "https://github.com/Herzult/php-ssh" } ], "require": { @@ -48,6 +44,7 @@ "amazonwebservices/aws-sdk-for-php": "1.5.*", "rackspace/php-opencloud" : "dev-master", "phpspec/phpspec2": "dev-master", + "phpseclib/phpseclib": "dev-master", "rackspace/php-cloudfiles": "*", "doctrine/dbal": ">=2.3", "dropbox-php/dropbox-php": "*", @@ -59,6 +56,7 @@ "dropbox-php/dropbox-php": "to use the Dropbox adapter", "rackspace/php-opencloud" : "to use Opencloud adapter", "amazonwebservices/aws-sdk-for-php": "to use the Amazon S3 adapter", + "phpseclib/phpseclib": "to use the SFTP", "doctrine/dbal": "to use the Doctrine DBAL adapter", "ext-zip": "to use the Zip adapter", "ext-apc": "to use the APC adapter", diff --git a/composer.lock b/composer.lock index 9d989bc0b..3eea5c3f6 100644 --- a/composer.lock +++ b/composer.lock @@ -1,5 +1,9 @@ { - "hash": "0557c6bf3b54057cf0c8a7f25ef0d79d", + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" + ], + "hash": "eb71255413a0b25ca71e82c7dd1e2acf", "packages": [ ], @@ -21,7 +25,6 @@ "require": { "php": ">=5.2.0" }, - "time": "2012-11-26 23:59:55", "type": "library", "autoload": { "classmap": [ @@ -52,20 +55,21 @@ "ec2", "s3", "sdk" - ] + ], + "time": "2012-11-26 23:59:55" }, { "name": "doctrine/annotations", - "version": "v1.0", + "version": "v1.1.1", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "v1.0" + "reference": "v1.1.1" }, "dist": { "type": "zip", - "url": "https://github.com/doctrine/annotations/archive/v1.0.zip", - "reference": "v1.0", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/v1.1.1", + "reference": "v1.1.1", "shasum": "" }, "require": { @@ -75,8 +79,12 @@ "require-dev": { "doctrine/cache": "1.*" }, - "time": "2013-01-12 19:23:32", "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, "autoload": { "psr-0": { "Doctrine\\Common\\Annotations\\": "lib/" @@ -118,27 +126,35 @@ "annotations", "docblock", "parser" - ] + ], + "time": "2013-04-20 08:30:17" }, { "name": "doctrine/cache", - "version": "v1.0", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/doctrine/cache.git", - "reference": "v1.0" + "reference": "45123145f70dd79618963a72a5271b4f389712e4" }, "dist": { "type": "zip", - "url": "https://github.com/doctrine/cache/archive/v1.0.zip", - "reference": "v1.0", + "url": "https://api.github.com/repos/doctrine/cache/zipball/45123145f70dd79618963a72a5271b4f389712e4", + "reference": "45123145f70dd79618963a72a5271b4f389712e4", "shasum": "" }, "require": { "php": ">=5.3.2" }, - "time": "2013-01-10 22:43:46", + "conflict": { + "doctrine/common": ">2.2,<2.4" + }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, "autoload": { "psr-0": { "Doctrine\\Common\\Cache\\": "lib/" @@ -170,7 +186,7 @@ { "name": "Johannes Schmitt", "email": "schmittjoh@gmail.com", - "homepage": "https://github.com/schmittjoh", + "homepage": "http://jmsyst.com", "role": "Developer of wrapped JMSSerializerBundle" } ], @@ -179,27 +195,32 @@ "keywords": [ "cache", "caching" - ] + ], + "time": "2013-05-13 02:51:07" }, { "name": "doctrine/collections", - "version": "v1.0", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "v1.0" + "reference": "3db3ab843ff76774bee4679d4cb3a10cffb0a935" }, "dist": { "type": "zip", - "url": "https://github.com/doctrine/collections/archive/v1.0.zip", - "reference": "v1.0", + "url": "https://api.github.com/repos/doctrine/collections/zipball/3db3ab843ff76774bee4679d4cb3a10cffb0a935", + "reference": "3db3ab843ff76774bee4679d4cb3a10cffb0a935", "shasum": "" }, "require": { "php": ">=5.3.2" }, - "time": "2013-01-12 16:36:50", "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, "autoload": { "psr-0": { "Doctrine\\Common\\Collections\\": "lib/" @@ -241,20 +262,21 @@ "array", "collections", "iterator" - ] + ], + "time": "2013-05-26 05:21:22" }, { "name": "doctrine/common", "version": "dev-master", "source": { "type": "git", - "url": "https://github.com/doctrine/common", - "reference": "53859ae1c84ccf1a5aa58c8379c69cd9adedf03a" + "url": "https://github.com/doctrine/common.git", + "reference": "2169b0ce1d253d448c60b7d40bbe4e4b5afe22fe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/common/zipball/53859ae1c84ccf1a5aa58c8379c69cd9adedf03a", - "reference": "53859ae1c84ccf1a5aa58c8379c69cd9adedf03a", + "url": "https://api.github.com/repos/doctrine/common/zipball/2169b0ce1d253d448c60b7d40bbe4e4b5afe22fe", + "reference": "2169b0ce1d253d448c60b7d40bbe4e4b5afe22fe", "shasum": "" }, "require": { @@ -265,7 +287,6 @@ "doctrine/lexer": "1.*", "php": ">=5.3.2" }, - "time": "2013-01-29 12:48:56", "type": "library", "extra": { "branch-alias": { @@ -303,7 +324,7 @@ { "name": "Johannes Schmitt", "email": "schmittjoh@gmail.com", - "homepage": "https://github.com/schmittjoh", + "homepage": "http://jmsyst.com", "role": "Developer of wrapped JMSSerializerBundle" } ], @@ -315,33 +336,34 @@ "eventmanager", "persistence", "spl" - ] + ], + "time": "2013-05-27 19:11:46" }, { "name": "doctrine/dbal", "version": "dev-master", "source": { "type": "git", - "url": "https://github.com/doctrine/dbal", - "reference": "eb6ee9a86421ba534d7c5514b190d1d06b30d4b1" + "url": "https://github.com/doctrine/dbal.git", + "reference": "da9564df54d248de03f9fd3d31e5eade86f8f747" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/eb6ee9a86421ba534d7c5514b190d1d06b30d4b1", - "reference": "eb6ee9a86421ba534d7c5514b190d1d06b30d4b1", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/da9564df54d248de03f9fd3d31e5eade86f8f747", + "reference": "da9564df54d248de03f9fd3d31e5eade86f8f747", "shasum": "" }, "require": { - "doctrine/common": "2.4.x-dev", + "doctrine/common": "2.4.*@beta", "php": ">=5.3.2" }, "require-dev": { + "phpunit/phpunit": "3.7.*", "symfony/console": "2.*" }, "suggest": { - "symfony/console": "For helpful console commands such as SQL execution and import of files." + "symfony/console": "Allows use of the command line interface" }, - "time": "2013-02-09 23:28:29", "type": "library", "extra": { "branch-alias": { @@ -384,27 +406,32 @@ "dbal", "persistence", "queryobject" - ] + ], + "time": "2013-05-27 19:17:11" }, { "name": "doctrine/inflector", - "version": "v1.0", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "v1.0" + "reference": "8b4b3ccec7aafc596e2fc1e593c9f2e78f939c8c" }, "dist": { "type": "zip", - "url": "https://github.com/doctrine/inflector/archive/v1.0.zip", - "reference": "v1.0", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/8b4b3ccec7aafc596e2fc1e593c9f2e78f939c8c", + "reference": "8b4b3ccec7aafc596e2fc1e593c9f2e78f939c8c", "shasum": "" }, "require": { "php": ">=5.3.2" }, - "time": "2013-01-10 21:49:15", "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, "autoload": { "psr-0": { "Doctrine\\Common\\Inflector\\": "lib/" @@ -444,30 +471,35 @@ "homepage": "http://www.doctrine-project.org", "keywords": [ "inflection", - "pluarlize", - "singuarlize", + "pluralize", + "singularize", "string" - ] + ], + "time": "2013-04-10 16:14:30" }, { "name": "doctrine/lexer", - "version": "v1.0", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "v1.0" + "reference": "bc0e1f0cc285127a38c6c8ea88bc5dba2fd53e94" }, "dist": { "type": "zip", - "url": "https://github.com/doctrine/lexer/archive/v1.0.zip", - "reference": "v1.0", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/bc0e1f0cc285127a38c6c8ea88bc5dba2fd53e94", + "reference": "bc0e1f0cc285127a38c6c8ea88bc5dba2fd53e94", "shasum": "" }, "require": { "php": ">=5.3.2" }, - "time": "2013-01-12 18:59:04", "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, "autoload": { "psr-0": { "Doctrine\\Common\\Lexer\\": "lib/" @@ -499,20 +531,21 @@ "keywords": [ "lexer", "parser" - ] + ], + "time": "2013-03-07 12:15:25" }, { "name": "dropbox-php/dropbox-php", "version": "dev-master", "source": { "type": "git", - "url": "http://github.com/Dropbox-PHP/dropbox-php", - "reference": "3dbc9912ce2846d46b3b0eaf72c3bd9f6a3efef0" + "url": "https://github.com/Dropbox-PHP/dropbox-php.git", + "reference": "5d05e32f825fa98182c98a93f46a176868e314c6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Dropbox-PHP/dropbox-php/zipball/3dbc9912ce2846d46b3b0eaf72c3bd9f6a3efef0", - "reference": "3dbc9912ce2846d46b3b0eaf72c3bd9f6a3efef0", + "url": "https://api.github.com/repos/Dropbox-PHP/dropbox-php/zipball/5d05e32f825fa98182c98a93f46a176868e314c6", + "reference": "5d05e32f825fa98182c98a93f46a176868e314c6", "shasum": "" }, "require": { @@ -522,7 +555,6 @@ "http_oauth": ">=1.1.18", "oauth": "*" }, - "time": "2013-01-19 02:59:05", "type": "library", "autoload": { "psr-0": { @@ -559,7 +591,8 @@ "homepage": "http://dropbox-php.com/", "keywords": [ "dropbox" - ] + ], + "time": "2013-03-29 22:25:23" }, { "name": "herzult/php-ssh", @@ -567,12 +600,12 @@ "source": { "type": "git", "url": "https://github.com/Herzult/php-ssh.git", - "reference": "49bce71cb7f3d51aed3009d8f301f1593308cc0a" + "reference": "772b7ae229b22112bf0b80e2e18229673c446f81" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Herzult/php-ssh/zipball/49bce71cb7f3d51aed3009d8f301f1593308cc0a", - "reference": "49bce71cb7f3d51aed3009d8f301f1593308cc0a", + "url": "https://api.github.com/repos/Herzult/php-ssh/zipball/772b7ae229b22112bf0b80e2e18229673c446f81", + "reference": "772b7ae229b22112bf0b80e2e18229673c446f81", "shasum": "" }, "require": { @@ -582,19 +615,20 @@ "require-dev": { "phpunit/phpunit": "3.7.*" }, - "time": "2013-01-31 21:44:04", "type": "library", "autoload": { "psr-0": { "Ssh": "src/" } }, + "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], "authors": [ { "name": "Antoine Hérault", + "email": "antoine.herault@gmail.com", "homepage": "https://github.com/Herzult" }, { @@ -607,10 +641,7 @@ "ssh", "ssh2" ], - "support": { - "source": "https://github.com/Herzult/php-ssh/tree/master", - "issues": "https://github.com/Herzult/php-ssh/issues" - } + "time": "2013-05-24 20:02:36" }, { "name": "mockery/mockery", @@ -632,7 +663,6 @@ "suggest": { "Hamcrest": "1.0.0" }, - "time": "2012-01-24 12:22:39", "type": "library", "autoload": { "psr-0": { @@ -663,7 +693,82 @@ "test", "test double", "testing" - ] + ], + "time": "2012-01-24 12:22:39" + }, + { + "name": "phpseclib/phpseclib", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/phpseclib/phpseclib.git", + "reference": "f596c577cf3a666be3818316562db90aced09330" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/f596c577cf3a666be3818316562db90aced09330", + "reference": "f596c577cf3a666be3818316562db90aced09330", + "shasum": "" + }, + "require": { + "php": ">=5.0.0" + }, + "suggest": { + "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", + "ext-mcrypt": "Install the Mcrypt extension in order to speed up a wide variety of cryptographic operations.", + "pear-pear/PHP_Compat": "Install PHP_Compat to get phpseclib working on PHP >= 4.3.3." + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "0.3-dev" + } + }, + "autoload": { + "psr-0": { + "Crypt": "phpseclib/", + "File": "phpseclib/", + "Math": "phpseclib/", + "Net": "phpseclib/" + }, + "files": [ + "phpseclib/Crypt/Random.php" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "include-path": [ + "phpseclib/" + ], + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jim Wigginton", + "email": "terrafrost@php.net", + "role": "Developer" + } + ], + "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", + "homepage": "http://phpseclib.sourceforge.net", + "keywords": [ + "BigInteger", + "aes", + "asn.1", + "asn1", + "crypto", + "cryptography", + "encryption", + "rsa", + "security", + "sftp", + "signature", + "signing", + "ssh", + "x.509", + "x509" + ], + "time": "2013-05-21 14:01:10" }, { "name": "phpspec/php-diff", @@ -671,15 +776,14 @@ "source": { "type": "git", "url": "https://github.com/phpspec/php-diff.git", - "reference": "8d82ac415225fac373a4073ba14b1fe286aa2312" + "reference": "v1.0.1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/php-diff/zipball/8d82ac415225fac373a4073ba14b1fe286aa2312", - "reference": "8d82ac415225fac373a4073ba14b1fe286aa2312", + "url": "https://api.github.com/repos/phpspec/php-diff/zipball/v1.0.1", + "reference": "v1.0.1", "shasum": "" }, - "time": "2012-11-08 08:55:45", "type": "library", "autoload": { "psr-0": { @@ -696,7 +800,8 @@ "homepage": "http://github.com/chrisboulton" } ], - "description": "A comprehensive library for generating differences between two hashable objects (strings or arrays)." + "description": "A comprehensive library for generating differences between two hashable objects (strings or arrays).", + "time": "2012-11-08 08:55:45" }, { "name": "phpspec/phpspec2", @@ -720,7 +825,6 @@ "symfony/event-dispatcher": ">=2.1.0,<2.2.0", "symfony/finder": ">=2.1.0,<2.2.0" }, - "time": "2013-02-08 11:00:28", "bin": [ "bin/phpspec" ], @@ -753,7 +857,8 @@ "TDD", "spec", "specification" - ] + ], + "time": "2013-02-08 11:00:28" }, { "name": "phpunit/php-code-coverage", @@ -761,12 +866,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "1.2.9" + "reference": "1.2.11" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/1.2.9", - "reference": "1.2.9", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/1.2.11", + "reference": "1.2.11", "shasum": "" }, "require": { @@ -775,11 +880,13 @@ "phpunit/php-text-template": ">=1.1.1@stable", "phpunit/php-token-stream": ">=1.1.3@stable" }, + "require-dev": { + "phpunit/phpunit": "3.7.*" + }, "suggest": { "ext-dom": "*", "ext-xdebug": ">=2.0.5" }, - "time": "2013-02-26 18:55:56", "type": "library", "autoload": { "classmap": [ @@ -806,7 +913,8 @@ "coverage", "testing", "xunit" - ] + ], + "time": "2013-05-23 18:23:24" }, { "name": "phpunit/php-file-iterator", @@ -825,7 +933,6 @@ "require": { "php": ">=5.3.3" }, - "time": "2013-01-07 10:47:05", "type": "library", "autoload": { "classmap": [ @@ -851,7 +958,8 @@ "keywords": [ "filesystem", "iterator" - ] + ], + "time": "2013-01-07 10:47:05" }, { "name": "phpunit/php-text-template", @@ -870,7 +978,6 @@ "require": { "php": ">=5.3.3" }, - "time": "2013-01-07 10:56:17", "type": "library", "autoload": { "classmap": [ @@ -895,7 +1002,8 @@ "homepage": "https://github.com/sebastianbergmann/php-text-template/", "keywords": [ "template" - ] + ], + "time": "2013-01-07 10:56:17" }, { "name": "phpunit/php-timer", @@ -914,7 +1022,6 @@ "require": { "php": ">=5.3.3" }, - "time": "2013-01-30 06:08:51", "type": "library", "autoload": { "classmap": [ @@ -939,7 +1046,8 @@ "homepage": "https://github.com/sebastianbergmann/php-timer/", "keywords": [ "timer" - ] + ], + "time": "2013-01-30 06:08:51" }, { "name": "phpunit/php-token-stream", @@ -959,7 +1067,6 @@ "ext-tokenizer": "*", "php": ">=5.3.3" }, - "time": "2013-01-07 10:56:35", "type": "library", "autoload": { "classmap": [ @@ -984,7 +1091,8 @@ "homepage": "https://github.com/sebastianbergmann/php-token-stream/", "keywords": [ "tokenizer" - ] + ], + "time": "2013-01-07 10:56:35" }, { "name": "phpunit/phpunit", @@ -992,12 +1100,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "3.7.15" + "reference": "3.7.21" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3.7.15", - "reference": "3.7.15", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3.7.21", + "reference": "3.7.21", "shasum": "" }, "require": { @@ -1011,7 +1119,7 @@ "phpunit/php-text-template": ">=1.1.1", "phpunit/php-timer": ">=1.0.2,<1.1.0", "phpunit/phpunit-mock-objects": ">=1.2.0,<1.3.0", - "symfony/yaml": ">=2.2.0" + "symfony/yaml": ">=2.0,<3.0" }, "require-dev": { "pear-pear/pear": "1.9.4" @@ -1022,7 +1130,6 @@ "ext-tokenizer": "*", "phpunit/php-invoker": ">=1.1.0,<1.2.0" }, - "time": "2013-03-01 11:55:06", "bin": [ "composer/bin/phpunit" ], @@ -1058,7 +1165,8 @@ "phpunit", "testing", "xunit" - ] + ], + "time": "2013-05-23 18:54:29" }, { "name": "phpunit/phpunit-mock-objects", @@ -1066,12 +1174,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "d49b5683200b5db9b1c64cb06f52f50d147891c4" + "reference": "8e9d897b39ca6310ccc2d81b8f29cabd5ce12786" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/d49b5683200b5db9b1c64cb06f52f50d147891c4", - "reference": "d49b5683200b5db9b1c64cb06f52f50d147891c4", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/8e9d897b39ca6310ccc2d81b8f29cabd5ce12786", + "reference": "8e9d897b39ca6310ccc2d81b8f29cabd5ce12786", "shasum": "" }, "require": { @@ -1081,7 +1189,6 @@ "suggest": { "ext-soap": "*" }, - "time": "2013-02-05 07:46:41", "type": "library", "autoload": { "classmap": [ @@ -1107,7 +1214,8 @@ "keywords": [ "mock", "xunit" - ] + ], + "time": "2013-05-07 12:11:45" }, { "name": "rackspace/php-cloudfiles", @@ -1137,19 +1245,18 @@ "source": { "type": "git", "url": "https://github.com/rackspace/php-opencloud.git", - "reference": "c3850c5e7d749357873f0ebf6bcde9692f33049c" + "reference": "v1.5.7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rackspace/php-opencloud/zipball/c3850c5e7d749357873f0ebf6bcde9692f33049c", - "reference": "c3850c5e7d749357873f0ebf6bcde9692f33049c", + "url": "https://api.github.com/repos/rackspace/php-opencloud/zipball/v1.5.7", + "reference": "v1.5.7", "shasum": "" }, "require": { "ext-curl": "*", "php": ">=5.3.0" }, - "time": "2013-03-30 12:53:51", "type": "library", "autoload": { "classmap": [ @@ -1164,7 +1271,8 @@ "keywords": [ "cloudfiles", "opencloud" - ] + ], + "time": "2013-05-23 19:22:19" }, { "name": "symfony/console", @@ -1172,19 +1280,18 @@ "target-dir": "Symfony/Component/Console", "source": { "type": "git", - "url": "https://github.com/symfony/Console", - "reference": "v2.1.7" + "url": "https://github.com/symfony/Console.git", + "reference": "4709ede4eb34e02d9349b6bf2809bd76be008404" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Console/zipball/v2.1.7", - "reference": "v2.1.7", + "url": "https://api.github.com/repos/symfony/Console/zipball/4709ede4eb34e02d9349b6bf2809bd76be008404", + "reference": "4709ede4eb34e02d9349b6bf2809bd76be008404", "shasum": "" }, "require": { "php": ">=5.3.3" }, - "time": "2013-01-17 15:20:05", "type": "library", "autoload": { "psr-0": { @@ -1206,7 +1313,8 @@ } ], "description": "Symfony Console Component", - "homepage": "http://symfony.com" + "homepage": "http://symfony.com", + "time": "2013-05-26 18:42:07" }, { "name": "symfony/event-dispatcher", @@ -1214,13 +1322,13 @@ "target-dir": "Symfony/Component/EventDispatcher", "source": { "type": "git", - "url": "https://github.com/symfony/EventDispatcher", - "reference": "e1d18ff0ff6f3e45ac82f000bc221135df635527" + "url": "https://github.com/symfony/EventDispatcher.git", + "reference": "v2.1.10" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/e1d18ff0ff6f3e45ac82f000bc221135df635527", - "reference": "e1d18ff0ff6f3e45ac82f000bc221135df635527", + "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/v2.1.10", + "reference": "v2.1.10", "shasum": "" }, "require": { @@ -1233,7 +1341,6 @@ "symfony/dependency-injection": "2.1.*", "symfony/http-kernel": "2.1.*" }, - "time": "2013-02-11 11:26:14", "type": "library", "autoload": { "psr-0": { @@ -1255,7 +1362,8 @@ } ], "description": "Symfony EventDispatcher Component", - "homepage": "http://symfony.com" + "homepage": "http://symfony.com", + "time": "2013-02-11 11:26:14" }, { "name": "symfony/finder", @@ -1263,19 +1371,18 @@ "target-dir": "Symfony/Component/Finder", "source": { "type": "git", - "url": "https://github.com/symfony/Finder", - "reference": "v2.1.7" + "url": "https://github.com/symfony/Finder.git", + "reference": "0dbc61194b58bc513e003789853ddfe0aea8cf33" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Finder/zipball/v2.1.7", - "reference": "v2.1.7", + "url": "https://api.github.com/repos/symfony/Finder/zipball/0dbc61194b58bc513e003789853ddfe0aea8cf33", + "reference": "0dbc61194b58bc513e003789853ddfe0aea8cf33", "shasum": "" }, "require": { "php": ">=5.3.3" }, - "time": "2013-01-09 08:51:07", "type": "library", "autoload": { "psr-0": { @@ -1297,7 +1404,8 @@ } ], "description": "Symfony Finder Component", - "homepage": "http://symfony.com" + "homepage": "http://symfony.com", + "time": "2013-05-25 15:47:15" }, { "name": "symfony/yaml", @@ -1306,22 +1414,21 @@ "source": { "type": "git", "url": "https://github.com/symfony/Yaml.git", - "reference": "f198ac28048eeceae852419c076123aaee59cd1c" + "reference": "82100c7732c95452702d11433aad50988700d36c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Yaml/zipball/f198ac28048eeceae852419c076123aaee59cd1c", - "reference": "f198ac28048eeceae852419c076123aaee59cd1c", + "url": "https://api.github.com/repos/symfony/Yaml/zipball/82100c7732c95452702d11433aad50988700d36c", + "reference": "82100c7732c95452702d11433aad50988700d36c", "shasum": "" }, "require": { "php": ">=5.3.3" }, - "time": "2013-01-31 21:39:01", "type": "library", "extra": { "branch-alias": { - "dev-master": "2.3-dev" + "dev-master": "2.4-dev" } }, "autoload": { @@ -1344,7 +1451,8 @@ } ], "description": "Symfony Yaml Component", - "homepage": "http://symfony.com" + "homepage": "http://symfony.com", + "time": "2013-05-16 07:54:39" } ], "aliases": [ @@ -1353,6 +1461,13 @@ "minimum-stability": "dev", "stability-flags": { "rackspace/php-opencloud": 20, - "phpspec/phpspec2": 20 - } + "phpspec/phpspec2": 20, + "phpseclib/phpseclib": 20 + }, + "platform": { + "php": ">=5.3.2" + }, + "platform-dev": [ + + ] } diff --git a/spec/Gaufrette/Adapter/PhpseclibSftp.php b/spec/Gaufrette/Adapter/PhpseclibSftp.php new file mode 100644 index 000000000..ed6df7075 --- /dev/null +++ b/spec/Gaufrette/Adapter/PhpseclibSftp.php @@ -0,0 +1,136 @@ +beConstructedWith($sftp, '/home/l3l0'); + } + + function it_should_be_initializable() + { + $this->shouldHaveType('Gaufrette\Adapter\PhpseclibSftp'); + $this->shouldHaveType('Gaufrette\Adapter\FileFactory'); + $this->shouldHaveType('Gaufrette\Adapter\ListKeysAware'); + } + + /** + * @param \spec\Gaufrette\Adapter\Net_SFTP $sftp + */ + function it_should_get_keys($sftp) + { + $sftp + ->rawlist('/home/l3l0/') + ->willReturn(array( + 'filename' => array('type' => NET_SFTP_TYPE_REGULAR), + 'filename1' => array('type' => NET_SFTP_TYPE_REGULAR), + 'aaa' => array('type' => NET_SFTP_TYPE_DIRECTORY) + )); + $sftp + ->rawlist('/home/l3l0/aaa') + ->willReturn(array( + 'filename' => array('type' => NET_SFTP_TYPE_REGULAR), + )); + + $this->keys()->shouldReturn(array('filename', 'filename1', 'aaa', 'aaa/filename')); + } + + /** + * @param \spec\Gaufrette\Adapter\Net_SFTP $sftp + */ + function it_should_read_file($sftp) + { + $sftp->get('/home/l3l0/filename')->willReturn('some content'); + + $this->read('filename')->shouldReturn('some content'); + } + + /** + * @param \spec\Gaufrette\Adapter\Net_SFTP $sftp + */ + function it_should_write_file($sftp) + { + $sftp->put('/home/l3l0/filename', 'some content')->willReturn(true); + $sftp->size('/home/l3l0/filename')->willReturn(12); + + $this->write('filename', 'some content')->shouldReturn(12); + } + + /** + * @param \spec\Gaufrette\Adapter\Net_SFTP $sftp + */ + function it_should_rename_file($sftp) + { + $sftp + ->rename('/home/l3l0/filename', '/home/l3l0/filename1') + ->willReturn(true); + + $this->rename('filename', 'filename1')->shouldReturn(true); + } + + /** + * @param \spec\Gaufrette\Adapter\Net_SFTP $sftp + */ + function it_should_check_if_file_exists($sftp) + { + $sftp->stat('/home/l3l0/filename')->willReturn(array( + 'name' => '/home/l3l0/filename' + )); + $sftp->stat('/home/l3l0/filename1')->willReturn(false); + + $this->exists('filename')->shouldReturn(true); + $this->exists('filename1')->shouldReturn(false); + } + + /** + * @param \spec\Gaufrette\Adapter\Net_SFTP $sftp + */ + function it_should_check_is_directory($sftp) + { + $sftp->pwd()->willReturn('/home/l3l0'); + $sftp->chdir('/home/l3l0')->willReturn(true); + $sftp->chdir('/home/l3l0/aaa')->willReturn(true); + $sftp->chdir('/home/l3l0/filename')->willReturn(false); + + $this->isDirectory('aaa')->shouldReturn(true); + $this->isDirectory('filename')->shouldReturn(false); + } + + /** + * @param \spec\Gaufrette\Adapter\Net_SFTP $sftp + * @param \Gaufrette\Filesystem $filesystem + */ + function it_should_create_file($sftp, $filesystem) + { + $sftp->stat('/home/l3l0/filename')->willReturn(array( + 'name' => '/home/l3l0/filename', + 'size' => '30', + )); + + $this->createFile('filename', $filesystem)->beAnInstanceOf('Gaufrette\File'); + } +} diff --git a/src/Gaufrette/Adapter/PhpseclibSftp.php b/src/Gaufrette/Adapter/PhpseclibSftp.php new file mode 100644 index 000000000..ddeef2548 --- /dev/null +++ b/src/Gaufrette/Adapter/PhpseclibSftp.php @@ -0,0 +1,238 @@ +sftp = $sftp; + $this->directory = $directory; + $this->create = $create; + } + + /** + * {@inheritDoc} + */ + public function read($key) + { + return $this->sftp->get($this->computePath($key)); + } + + /** + * {@inheritDoc} + */ + public function rename($sourceKey, $targetKey) + { + $this->initialize(); + + $sourcePath = $this->computePath($sourceKey); + $targetPath = $this->computePath($targetKey); + + $this->ensureDirectoryExists(dirname($targetPath), true); + + return $this->sftp->rename($sourcePath, $targetPath); + } + + /** + * {@inheritDoc} + */ + public function write($key, $content) + { + $this->initialize(); + + $path = $this->computePath($key); + $this->ensureDirectoryExists(dirname($path), true); + if ($this->sftp->put($path, $content)) { + return $this->sftp->size($path); + } + + return false; + } + + /** + * {@inheritDoc} + */ + public function exists($key) + { + $this->initialize(); + + return false !== $this->sftp->stat($this->computePath($key)); + } + + /** + * {@inheritDoc} + */ + public function isDirectory($key) + { + $this->initialize(); + + $pwd = $this->sftp->pwd(); + if ($this->sftp->chdir($this->computePath($key))) { + $this->sftp->chdir($pwd); + + return true; + } + + return false; + } + + /** + * {@inheritDoc} + */ + public function keys() + { + $keys = $this->fetchKeys(); + + return $keys['keys']; + } + + /** + * {@inheritDoc} + */ + public function listKeys($prefix = '') + { + preg_match('/(.*?)[^\/]*$/', $prefix, $match); + $directory = rtrim($match[1], '/'); + + $keys = $this->fetchKeys($directory, false); + + if ($directory === $prefix) { + return $keys; + } + + $filteredKeys = array(); + foreach (array('keys', 'dirs') as $hash) { + $filteredKeys[$hash] = array(); + foreach ($keys[$hash] as $key) { + if (0 === strpos($key, $prefix)) { + $filteredKeys[$hash][] = $key; + } + } + } + + return $filteredKeys; + } + + /** + * {@inheritDoc} + */ + public function mtime($key) + { + $this->initialize(); + + $stat = $this->sftp->stat($this->computePath($key)); + + return isset($stat['mtime']) ? $stat['mtime'] : false; + } + + /** + * {@inheritDoc} + */ + public function delete($key) + { + return $this->sftp->delete($this->computePath($key), false); + } + + /** + * {@inheritDoc} + */ + public function createFile($key, Filesystem $filesystem) + { + $file = new File($key, $filesystem); + + $stat = $this->sftp->stat($this->computePath($key)); + if (isset($stat['size'])) { + $file->setSize($stat['size']); + } + + return $file; + } + + /** + * Performs the adapter's initialization + * + * It will ensure the root directory exists + */ + protected function initialize() + { + if ($this->initialized) { + return; + } + + $this->ensureDirectoryExists($this->directory, $this->create); + + $this->initialized = true; + } + + protected function ensureDirectoryExists($directory, $create = false) + { + $pwd = $this->sftp->pwd(); + if ($this->sftp->chdir($directory)) { + $this->sftp->chdir($pwd); + } elseif ($this->create) { + if (!$this->sftp->mkdir($directory, 0777, true)) { + throw new \RuntimeException(sprintf('The directory \'%s\' does not exist and could not be created (%s).', $this->directory, $this->sftp->getLastSFTPError())); + } + } else { + throw new \RuntimeException(sprintf('The directory \'%s\' does not exist.', $this->directory)); + } + } + + protected function computePath($key) + { + return $this->directory . '/' . ltrim($key, '/'); + } + + protected function fetchKeys($directory = '', $onlyKeys = true) + { + $keys = array('keys' => array(), 'dirs' => array()); + + $list = $this->sftp->rawlist($this->computePath($directory)); + foreach ((array) $list as $filename => $stat) { + if ('.' === $filename || '..' === $filename) { + continue; + } + + $path = ltrim($directory . '/' . $filename, '/'); + if ($stat['type'] === NET_SFTP_TYPE_DIRECTORY) { + $keys['dirs'][] = $path; + } else { + $keys['keys'][] = $path; + } + } + + $dirs = $keys['dirs']; + + if ($onlyKeys && !empty($dirs)) { + $keys['keys'] = array_merge($keys['keys'], $dirs); + $keys['dirs'] = array(); + } + + foreach ($dirs as $dir) { + $keys = array_merge_recursive($keys, $this->fetchKeys($dir, $onlyKeys)); + } + + return $keys; + } +} diff --git a/tests/Gaufrette/Functional/Adapter/PhpseclibSftpTest.php b/tests/Gaufrette/Functional/Adapter/PhpseclibSftpTest.php new file mode 100644 index 000000000..60cfed698 --- /dev/null +++ b/tests/Gaufrette/Functional/Adapter/PhpseclibSftpTest.php @@ -0,0 +1,9 @@ +login('username', 'password'); + +$directory = '/some/directory'; +$sftp->delete($directory, true); +$adapter = new Gaufrette\Adapter\PhpseclibSftp($sftp, $directory, true); + +return $adapter; From aca046a14c1f6d339e189ad619db9284e75532a2 Mon Sep 17 00:00:00 2001 From: Alessandro Tagliapietra Date: Wed, 12 Jun 2013 17:04:29 +0200 Subject: [PATCH 011/426] Set S3 permissions during file write I've added this since with GaufretteBundle you can't use AclAwareAmazonS3 and this would be a nice way to set file permissions on write. --- src/Gaufrette/Adapter/AmazonS3.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Gaufrette/Adapter/AmazonS3.php b/src/Gaufrette/Adapter/AmazonS3.php index cfd062ad2..a5e052896 100644 --- a/src/Gaufrette/Adapter/AmazonS3.php +++ b/src/Gaufrette/Adapter/AmazonS3.php @@ -116,12 +116,12 @@ public function rename($sourceKey, $targetKey) /** * {@inheritDoc} */ - public function write($key, $content) + public function write($key, $content, $permission = AmazonClient::ACL_PUBLIC) { $this->ensureBucketExists(); $opt = array_replace_recursive( - array('acl' => AmazonClient::ACL_PUBLIC), + array('acl' => $permission), $this->getMetadata($key), array('body' => $content) ); From a0b93972b4fb56933dff0e9f5f8c3da505bc1926 Mon Sep 17 00:00:00 2001 From: Alessandro Tagliapietra Date: Wed, 12 Jun 2013 17:30:32 +0200 Subject: [PATCH 012/426] Added acl into class options and acl getter/setter --- src/Gaufrette/Adapter/AmazonS3.php | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/src/Gaufrette/Adapter/AmazonS3.php b/src/Gaufrette/Adapter/AmazonS3.php index a5e052896..b123ac16c 100644 --- a/src/Gaufrette/Adapter/AmazonS3.php +++ b/src/Gaufrette/Adapter/AmazonS3.php @@ -26,11 +26,31 @@ public function __construct(AmazonClient $service, $bucket, $options = array()) $this->service = $service; $this->bucket = $bucket; $this->options = array_replace_recursive( - array('directory' => '', 'create' => false, 'region' => AmazonClient::REGION_US_E1), + array('directory' => '', 'create' => false, 'region' => AmazonClient::REGION_US_E1, 'acl' => AmazonClient::ACL_PUBLIC), $options ); } + /** + * Set the acl used when writing files + * + * @param string $acl + */ + public function setAcl($acl) + { + $this->options['acl'] = $acl; + } + + /** + * Get the acl used when writing files + * + * @return string + */ + public function getAcl() + { + return $this->options['acl']; + } + /** * Set the base directory the user will have access to * @@ -116,12 +136,12 @@ public function rename($sourceKey, $targetKey) /** * {@inheritDoc} */ - public function write($key, $content, $permission = AmazonClient::ACL_PUBLIC) + public function write($key, $content) { $this->ensureBucketExists(); $opt = array_replace_recursive( - array('acl' => $permission), + array('acl' => $this->options['acl']), $this->getMetadata($key), array('body' => $content) ); From abb1ce3c764d42568c37185bc58e0404eabf912a Mon Sep 17 00:00:00 2001 From: l3l0 Date: Mon, 17 Jun 2013 11:31:18 +0200 Subject: [PATCH 013/426] Add spec for acl handling in amazon adapter. Renaming of spec methods. --- spec/Gaufrette/Adapter/AclAwareAmazonS3.php | 32 ++- spec/Gaufrette/Adapter/AmazonS3.php | 302 ++++++++++++++------ 2 files changed, 228 insertions(+), 106 deletions(-) diff --git a/spec/Gaufrette/Adapter/AclAwareAmazonS3.php b/spec/Gaufrette/Adapter/AclAwareAmazonS3.php index 9e955010d..4a833d26d 100644 --- a/spec/Gaufrette/Adapter/AclAwareAmazonS3.php +++ b/spec/Gaufrette/Adapter/AclAwareAmazonS3.php @@ -15,17 +15,21 @@ function let($adapter, $service) $this->beConstructedWith($adapter, $service, 'bucketName'); } - function it_should_be_initializable() + function it_is_initializable() { $this->shouldHaveType('Gaufrette\Adapter\AclAwareAmazonS3'); $this->shouldHaveType('Gaufrette\Adapter'); + } + + function it_supports_metadata() + { $this->shouldHaveType('Gaufrette\Adapter\MetadataSupporter'); } /** * @param \Gaufrette\Adapter $adapter */ - function it_should_delegate_read($adapter) + function it_delegates_read($adapter) { $adapter->read('filename')->willReturn('some content'); $adapter->read('filename2')->willReturn('other content'); @@ -38,7 +42,7 @@ function it_should_delegate_read($adapter) * @param \Gaufrette\Adapter $adapter * @param \AmazonS3 $service */ - function it_should_delegate_rename_and_update_acl($adapter, $service) + function it_delegates_rename_and_update_acl($adapter, $service) { $service ->set_object_acl('bucketName', 'filename2', \AmazonS3::ACL_PRIVATE) @@ -59,7 +63,7 @@ function it_should_delegate_rename_and_update_acl($adapter, $service) * @param \Gaufrette\Adapter $adapter * @param \AmazonS3 $service */ - function it_should_not_rename_when_cannot_update_acl($adapter, $service) + function it_does_not_rename_when_cannot_update_acl($adapter, $service) { $service ->set_object_acl('bucketName', 'filename2', \AmazonS3::ACL_PRIVATE) @@ -80,7 +84,7 @@ function it_should_not_rename_when_cannot_update_acl($adapter, $service) * @param \Gaufrette\Adapter $adapter * @param \AmazonS3 $service */ - function it_should_update_acl_with_users_array_when_rename($adapter, $service) + function it_updates_acl_with_users_array_when_rename($adapter, $service) { $service ->set_object_acl('bucketName', 'filename2', array(array('id' => 'someId', 'permission' => \AmazonS3::GRANT_READ))) @@ -98,7 +102,7 @@ function it_should_update_acl_with_users_array_when_rename($adapter, $service) * @param \Gaufrette\Adapter $adapter * @param \AmazonS3 $service */ - function it_should_delegate_write_and_update_acl($adapter, $service) + function it_delegates_write_and_update_acl($adapter, $service) { $service ->set_object_acl('bucketName', 'filename', \AmazonS3::ACL_PRIVATE) @@ -119,7 +123,7 @@ function it_should_delegate_write_and_update_acl($adapter, $service) * @param \Gaufrette\Adapter $adapter * @param \AmazonS3 $service */ - function it_should_not_write_when_cannot_update_acl($adapter, $service) + function it_does_not_write_when_cannot_update_acl($adapter, $service) { $service ->set_object_acl('bucketName', 'filename', \AmazonS3::ACL_PRIVATE) @@ -140,7 +144,7 @@ function it_should_not_write_when_cannot_update_acl($adapter, $service) * @param \Gaufrette\Adapter $adapter * @param \AmazonS3 $service */ - function it_should_update_acl_with_users_array_when_write($adapter, $service) + function it_updates_acl_with_users_array_when_write($adapter, $service) { $service ->set_object_acl('bucketName', 'filename', array(array('id' => 'someId', 'permission' => \AmazonS3::GRANT_READ))) @@ -157,7 +161,7 @@ function it_should_update_acl_with_users_array_when_write($adapter, $service) /** * @param \Gaufrette\Adapter $adapter */ - function it_should_delegate_exists($adapter) + function it_delegates_exists($adapter) { $adapter->exists('filename')->willReturn(true); $adapter->exists('filename2')->willReturn(false); @@ -169,7 +173,7 @@ function it_should_delegate_exists($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_should_delegate_mtime($adapter) + function it_delegates_mtime($adapter) { $adapter->mtime('filename')->willReturn(1234); $adapter->mtime('filename2')->willReturn(2345); @@ -181,7 +185,7 @@ function it_should_delegate_mtime($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_should_delegate_is_directory_check($adapter) + function it_delegates_directory_check($adapter) { $adapter->isDirectory('filename')->willReturn(true); $adapter->isDirectory('filename2')->willReturn(false); @@ -193,7 +197,7 @@ function it_should_delegate_is_directory_check($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_should_delegate_keys($adapter) + function it_delegates_keys($adapter) { $adapter->keys->willReturn(array('filename', 'filename2')); @@ -204,7 +208,7 @@ function it_should_delegate_keys($adapter) * @param \spec\Gaufrette\Adapter\TestDelegateAdapter $extendedAdapter * @param \AmazonS3 $service */ - function it_should_delegate_metadata($extendedAdapter, $service) + function it_delegates_metadata_handling($extendedAdapter, $service) { $this->beConstructedWith($extendedAdapter, $service, 'bucketName'); @@ -218,7 +222,7 @@ function it_should_delegate_metadata($extendedAdapter, $service) /** * @param \Gaufrette\Adapter $adapter */ - function it_should_not_delegate_metadata_when_delegate_object_does_not_support_it($adapter) + function it_does_not_delegate_metadata_when_delegate_object_does_not_support_it($adapter) { $adapter->setMetadata('filename', array('some'))->shouldNotBeCalled(); $adapter->getMetadata('filename')->shouldNotBeCalled(); diff --git a/spec/Gaufrette/Adapter/AmazonS3.php b/spec/Gaufrette/Adapter/AmazonS3.php index 22a913386..5504631d6 100644 --- a/spec/Gaufrette/Adapter/AmazonS3.php +++ b/spec/Gaufrette/Adapter/AmazonS3.php @@ -14,17 +14,21 @@ function let($service) $this->beConstructedWith($service, 'bucketName'); } - function it_should_be_initializable() + function it_is_amazon_adapter() { $this->shouldHaveType('Gaufrette\Adapter\AmazonS3'); $this->shouldHaveType('Gaufrette\Adapter'); + } + + function it_supports_metadata() + { $this->shouldHaveType('Gaufrette\Adapter\MetadataSupporter'); } /** * @param \AmazonS3 $service */ - function it_should_read_file($service) + function it_reads_file($service) { $options = array( 'range' => 12, @@ -36,7 +40,8 @@ function it_should_read_file($service) $service ->if_bucket_exists('bucketName') ->shouldBeCalled() - ->willReturn(true); + ->willReturn(true) + ; $service ->get_object( 'bucketName', @@ -44,7 +49,8 @@ function it_should_read_file($service) $options ) ->shouldBeCalled() - ->willReturn(new \CFResponse('header', 'some content', 200)); + ->willReturn(new \CFResponse('header', 'some content', 200)) + ; $this->setMetadata('filename', $options); $this->read('filename')->shouldReturn('some content'); @@ -53,12 +59,13 @@ function it_should_read_file($service) /** * @param \AmazonS3 $service */ - function it_should_return_false_when_cannot_read($service) + function it_returns_false_when_cannot_read($service) { $service ->if_bucket_exists('bucketName') ->shouldBeCalled() - ->willReturn(true); + ->willReturn(true) + ; $service ->get_object( 'bucketName', @@ -66,7 +73,8 @@ function it_should_return_false_when_cannot_read($service) array() ) ->shouldBeCalled() - ->willReturn(new \CFResponse('header', 'some content', 500)); + ->willReturn(new \CFResponse('header', 'some content', 500)) + ; $this->read('filename')->shouldReturn(false); } @@ -74,19 +82,21 @@ function it_should_return_false_when_cannot_read($service) /** * @param \AmazonS3 $service */ - function it_should_not_mask_exception_when_read($service) + function it_is_verbose_and_throws_exceptions_when_read($service) { $service ->if_bucket_exists('bucketName') ->shouldBeCalled() - ->willReturn(true); + ->willReturn(true) + ; $service ->get_object( 'bucketName', 'filename', array() ) - ->willThrow(new \RuntimeException('read')); + ->willThrow(new \RuntimeException('read')) + ; $this->shouldThrow(new \RuntimeException('read'))->duringRead('filename'); } @@ -94,12 +104,13 @@ function it_should_not_mask_exception_when_read($service) /** * @param \AmazonS3 $service */ - function it_should_rename_file($service) + function it_rename_file($service) { $service ->if_bucket_exists('bucketName') ->shouldBeCalled() - ->willReturn(true); + ->willReturn(true) + ; $service ->copy_object( array( @@ -113,7 +124,8 @@ function it_should_rename_file($service) array('acl' => \AmazonS3::ACL_OWNER_READ) ) ->shouldBeCalled() - ->willReturn(new \CFResponse('header', 'some content', 200)); + ->willReturn(new \CFResponse('header', 'some content', 200)) + ; $service ->delete_object( 'bucketName', @@ -121,24 +133,27 @@ function it_should_rename_file($service) ANY_ARGUMENT ) ->shouldBeCalled() - ->willReturn(new \CFResponse(array(), 'some', 200)); + ->willReturn(new \CFResponse(array(), 'some', 200)) + ; - $this->setMetadata('filename1', array('acl' => \AmazonS3::ACL_OWNER_READ)); - $this->rename('filename1', 'filename2')->shouldReturn(true); + $this->setMetadata('filename1', array('acl' => \AmazonS3::ACL_OWNER_READ)); + $this->rename('filename1', 'filename2')->shouldReturn(true); } /** * @param \AmazonS3 $service */ - function it_should_not_mask_exception_when_rename($service) + function it_is_verbose_and_throws_exceptions_when_rename($service) { $service ->if_bucket_exists('bucketName') ->shouldBeCalled() - ->willReturn(true); + ->willReturn(true) + ; $service ->copy_object(ANY_ARGUMENT, ANY_ARGUMENT, ANY_ARGUMENT) - ->willThrow(new \RuntimeException('rename')); + ->willThrow(new \RuntimeException('rename')) + ; $this->shouldThrow(new \RuntimeException('rename'))->duringRename('filename', 'filename1'); } @@ -146,12 +161,13 @@ function it_should_not_mask_exception_when_rename($service) /** * @param \AmazonS3 $service */ - function it_should_return_false_when_cannot_rename($service) + function it_returns_false_when_cannot_rename($service) { $service ->if_bucket_exists('bucketName') ->shouldBeCalled() - ->willReturn(true); + ->willReturn(true) + ; $service ->copy_object( array( @@ -165,9 +181,10 @@ function it_should_return_false_when_cannot_rename($service) array() ) ->shouldBeCalled() - ->willReturn(new \CFResponse('header', 'some content', 500)); + ->willReturn(new \CFResponse('header', 'some content', 500)) + ; - $this->rename('filename1', 'filename2')->shouldReturn(false); + $this->rename('filename1', 'filename2')->shouldReturn(false); } /** @@ -178,7 +195,8 @@ function it_should_write_file($service) $service ->if_bucket_exists('bucketName') ->shouldBeCalled() - ->willReturn(true); + ->willReturn(true) + ; $service ->create_object( 'bucketName', @@ -189,7 +207,8 @@ function it_should_write_file($service) ) ) ->shouldBeCalled() - ->willReturn(new \CFResponse(array('x-aws-requestheaders' => array('Content-Length' => 12)), 'some content', 200)); + ->willReturn(new \CFResponse(array('x-aws-requestheaders' => array('Content-Length' => 12)), 'some content', 200)) + ; $this->setMetadata('filename', array('acl' => \AmazonS3::ACL_PRIVATE, 'body' => 'other content')); $this->write('filename', 'some content')->shouldReturn(12); @@ -198,12 +217,13 @@ function it_should_write_file($service) /** * @param \AmazonS3 $service */ - function it_should_return_false_when_cannot_write($service) + function it_returns_false_when_cannot_write($service) { $service ->if_bucket_exists('bucketName') ->shouldBeCalled() - ->willReturn(true); + ->willReturn(true) + ; $service ->create_object( 'bucketName', @@ -214,7 +234,8 @@ function it_should_return_false_when_cannot_write($service) ) ) ->shouldBeCalled() - ->willReturn(new \CFResponse(array('x-aws-requestheaders' => array('Content-Length' => 12)), 'some content', 500)); + ->willReturn(new \CFResponse(array('x-aws-requestheaders' => array('Content-Length' => 12)), 'some content', 500)) + ; $this->write('filename', 'some content')->shouldReturn(false); } @@ -222,15 +243,17 @@ function it_should_return_false_when_cannot_write($service) /** * @param \AmazonS3 $service */ - function it_should_not_mask_exception_when_write($service) + function it_is_verbose_and_throws_exceptions_when_write($service) { $service ->if_bucket_exists('bucketName') ->shouldBeCalled() - ->willReturn(true); + ->willReturn(true) + ; $service ->create_object(ANY_ARGUMENT, ANY_ARGUMENT, ANY_ARGUMENT) - ->willThrow(new \RuntimeException('write')); + ->willThrow(new \RuntimeException('write')) + ; $this->shouldThrow(new \RuntimeException('write'))->duringWrite('filename', 'some content'); } @@ -243,7 +266,8 @@ function it_should_check_if_file_exists($service) $service ->if_bucket_exists('bucketName') ->shouldBeCalled() - ->willReturn(true); + ->willReturn(true) + ; $service->if_object_exists('bucketName', 'filename')->willReturn(true); $this->exists('filename')->shouldReturn(true); @@ -255,15 +279,17 @@ function it_should_check_if_file_exists($service) /** * @param \AmazonS3 $service */ - function it_should_not_mask_exception_when_check_if_file_exists($service) + function it_is_verbose_and_throws_exceptions_when_file_exists($service) { $service ->if_bucket_exists('bucketName') ->shouldBeCalled() - ->willReturn(true); + ->willReturn(true) + ; $service ->if_object_exists('bucketName', 'filename') - ->willThrow(new \RuntimeException('exists')); + ->willThrow(new \RuntimeException('exists')) + ; $this->shouldThrow(new \RuntimeException('exists'))->duringExists('filename'); } @@ -277,7 +303,8 @@ function it_should_get_file_mtime($service) $service ->if_bucket_exists('bucketName') ->shouldBeCalled() - ->willReturn(true); + ->willReturn(true) + ; $service ->get_object_metadata( @@ -286,21 +313,23 @@ function it_should_get_file_mtime($service) $metadata ) ->shouldBeCalled() - ->willReturn(array('Headers' => array('last-modified' => '2012-01-01 23:10:10'))); + ->willReturn(array('Headers' => array('last-modified' => '2012-01-01 23:10:10'))) + ; - $this->setMetadata('filename', $metadata); - $this->mtime('filename')->shouldReturn(strtotime('2012-01-01 23:10:10')); + $this->setMetadata('filename', $metadata); + $this->mtime('filename')->shouldReturn(strtotime('2012-01-01 23:10:10')); } /** * @param \AmazonS3 $service */ - function it_should_return_false_when_cannot_mtime($service) + function it_returns_false_when_cannot_fetch_mtime($service) { $service ->if_bucket_exists('bucketName') ->shouldBeCalled() - ->willReturn(true); + ->willReturn(true) + ; $service ->get_object_metadata( @@ -309,7 +338,8 @@ function it_should_return_false_when_cannot_mtime($service) array() ) ->shouldBeCalled() - ->willReturn(array('Headers' => array())); + ->willReturn(array('Headers' => array())) + ; $this->mtime('filename')->shouldReturn(false); } @@ -317,15 +347,17 @@ function it_should_return_false_when_cannot_mtime($service) /** * @param \AmazonS3 $service */ - function it_should_not_mask_exception_when_get_mtime($service) + function it_is_verbose_and_throws_exceptions_when_fetch_mtime($service) { $service ->if_bucket_exists('bucketName') ->shouldBeCalled() - ->willReturn(true); + ->willReturn(true) + ; $service ->get_object_metadata('bucketName', 'filename', ANY_ARGUMENT) - ->willThrow(new \RuntimeException('mtime')); + ->willThrow(new \RuntimeException('mtime')) + ; $this->shouldThrow(new \RuntimeException('mtime'))->duringMtime('filename'); } @@ -340,14 +372,16 @@ function it_should_delete_file($service) $service ->if_bucket_exists('bucketName') ->shouldBeCalled() - ->willReturn(true); + ->willReturn(true) + ; $service ->delete_object( 'bucketName', 'filename', $metadata ) - ->willReturn(new \CFResponse(array(), 'some', 200)); + ->willReturn(new \CFResponse(array(), 'some', 200)) + ; $this->setMetadata('filename', $metadata); $this->delete('filename')->shouldReturn(true); @@ -356,18 +390,20 @@ function it_should_delete_file($service) /** * @param \AmazonS3 $service */ - function it_should_not_mask_exception_when_delete($service) + function it_is_verbose_and_throws_exceptions_when_fetch_delete($service) { $service ->if_bucket_exists('bucketName') - ->willReturn(true); + ->willReturn(true) + ; $service ->delete_object( 'bucketName', 'filename', ANY_ARGUMENT ) - ->willThrow(new \RuntimeException('delete')); + ->willThrow(new \RuntimeException('delete')) + ; $this->shouldThrow(new \RuntimeException('delete'))->duringDelete('filename'); } @@ -375,19 +411,21 @@ function it_should_not_mask_exception_when_delete($service) /** * @param \AmazonS3 $service */ - function it_should_return_false_when_cannot_delete_file($service) + function it_returns_false_when_cannot_delete($service) { $service ->if_bucket_exists('bucketName') ->shouldBeCalled() - ->willReturn(true); + ->willReturn(true) + ; $service ->delete_object( 'bucketName', 'filename', array() ) - ->willReturn(new \CFResponse(array(), 'some', 500)); + ->willReturn(new \CFResponse(array(), 'some', 500)) + ; $this->delete('filename')->shouldReturn(false); } @@ -400,11 +438,13 @@ function it_should_get_keys($service) $service ->if_bucket_exists('bucketName') ->shouldBeCalled() - ->willReturn(true); + ->willReturn(true) + ; $service ->get_object_list('bucketName') ->shouldBeCalled() - ->willReturn(array('filename2', 'aaa/filename', 'filename1')); + ->willReturn(array('filename2', 'aaa/filename', 'filename1')) + ; $this->keys()->shouldReturn(array('aaa', 'aaa/filename', 'filename1', 'filename2')); } @@ -412,14 +452,16 @@ function it_should_get_keys($service) /** * @param \AmazonS3 $service */ - function it_should_not_mask_exception_when_get_keys($service) + function it_is_verbose_and_throws_exceptions_when_fetch_keys($service) { $service ->if_bucket_exists('bucketName') - ->willReturn(true); + ->willReturn(true) + ; $service ->get_object_list('bucketName') - ->willThrow(new \RuntimeException('keys')); + ->willThrow(new \RuntimeException('keys')) + ; $this->shouldThrow(new \RuntimeException('keys'))->duringKeys(); } @@ -431,17 +473,21 @@ function it_should_handle_dirs($service) { $service ->if_bucket_exists('bucketName') - ->willReturn(true); + ->willReturn(true) + ; $service ->if_object_exists('bucketName', 'filename') - ->shouldNotBeCalled(); + ->shouldNotBeCalled() + ; $service ->if_object_exists('bucketName', 'filename/') ->shouldBeCalled() - ->willReturn(false); + ->willReturn(false) + ; $service ->if_object_exists('bucketName', 'dirname/') - ->willReturn(true); + ->willReturn(true) + ; $this->isDirectory('filename')->shouldReturn(false); $this->isDirectory('dirname')->shouldReturn(true); @@ -454,46 +500,56 @@ function it_should_fail_when_bucket_does_not_exist($service) { $service ->if_bucket_exists('bucketName') - ->willReturn(false); - + ->willReturn(false) + ; $this ->shouldThrow(new \RuntimeException('The configured bucket "bucketName" does not exist.')) - ->duringRead('filename'); + ->duringRead('filename') + ; $this ->shouldThrow(new \RuntimeException('The configured bucket "bucketName" does not exist.')) - ->duringWrite('filename', 'content'); + ->duringWrite('filename', 'content') + ; $this ->shouldThrow(new \RuntimeException('The configured bucket "bucketName" does not exist.')) - ->duringDelete('filename'); + ->duringDelete('filename') + ; $this ->shouldThrow(new \RuntimeException('The configured bucket "bucketName" does not exist.')) - ->duringExists('filename'); + ->duringExists('filename') + ; $this ->shouldThrow(new \RuntimeException('The configured bucket "bucketName" does not exist.')) - ->duringMtime('filename'); + ->duringMtime('filename') + ; $this ->shouldThrow(new \RuntimeException('The configured bucket "bucketName" does not exist.')) - ->duringRename('filename', 'filename2'); + ->duringRename('filename', 'filename2') + ; $this ->shouldThrow(new \RuntimeException('The configured bucket "bucketName" does not exist.')) - ->duringKeys(); + ->duringKeys() + ; } /** * @param \AmazonS3 $service */ - function it_should_create_bucket_if_create_mode_is_enabled($service) + function it_creates_bucket_if_create_mode_is_enabled($service) { $service ->if_bucket_exists('bucketName') - ->willReturn(false); + ->willReturn(false) + ; $service ->create_bucket('bucketName', \AmazonS3::REGION_US_E1) ->shouldBeCalled() - ->willReturn(new \CFResponse(array(), 'created', 201)); + ->willReturn(new \CFResponse(array(), 'created', 201)) + ; $service ->if_object_exists('bucketName', 'filename') - ->willReturn(false); + ->willReturn(false) + ; $this->beConstructedWith($service, 'bucketName', array('create' => true)); $this->exists('filename'); @@ -502,18 +558,56 @@ function it_should_create_bucket_if_create_mode_is_enabled($service) /** * @param \AmazonS3 $service */ - function it_allows_to_configure_region_when_creating_bucket($service) + function it_fails_when_cannot_create_bucket($service) { $service ->if_bucket_exists('bucketName') - ->willReturn(false); + ->willReturn(false) + ; + $service + ->create_bucket('bucketName', \AmazonS3::REGION_US_E1) + ->shouldBeCalled() + ->willReturn(new \CFResponse(array(), 'created', 500)) + ; + + $this->beConstructedWith($service, 'bucketName', array('create' => true)); + $this + ->shouldThrow(new \RuntimeException('Failed to create the configured bucket "bucketName".')) + ->duringExists('filename') + ; + } + + /** + * @param \AmazonS3 $service + */ + function it_allows_to_configure_reqion($service) + { + $service + ->set_region(\AmazonS3::REGION_EU_W1) + ->shouldBeCalled(); + + $this->beConstructedWith($service, 'bucketName', array('region' => \AmazonS3::REGION_EU_W1)); + $this->exists('filename'); + } + + /** + * @param \AmazonS3 $service + */ + function it_allows_to_configure_region_for_bucket($service) + { + $service + ->if_bucket_exists('bucketName') + ->willReturn(false) + ; $service ->create_bucket('bucketName', \AmazonS3::REGION_EU_W1) ->shouldBeCalled() - ->willReturn(new \CFResponse(array(), 'created', 201)); + ->willReturn(new \CFResponse(array(), 'created', 201)) + ; $service ->if_object_exists('bucketName', 'filename') - ->willReturn(false); + ->willReturn(false) + ; $this->beConstructedWith($service, 'bucketName', array('create' => true, 'region' => \AmazonS3::REGION_EU_W1)); $this->exists('filename'); @@ -522,32 +616,56 @@ function it_allows_to_configure_region_when_creating_bucket($service) /** * @param \AmazonS3 $service */ - function it_should_fail_when_cannot_create_bucket($service) + function it_allows_to_configure_acl($service) { + $this->setAcl('123abc'); $service ->if_bucket_exists('bucketName') - ->willReturn(false); + ->shouldBeCalled() + ->willReturn(true) + ; $service - ->create_bucket('bucketName', \AmazonS3::REGION_US_E1) + ->create_object( + 'bucketName', + 'filename', + array( + 'acl' => '123abc', + 'body' => 'some content' + ) + ) ->shouldBeCalled() - ->willReturn(new \CFResponse(array(), 'created', 500)); + ->willReturn(new \CFResponse(array('x-aws-requestheaders' => array('Content-Length' => 12)), 'some content', 200)) + ; - $this->beConstructedWith($service, 'bucketName', array('create' => true)); - $this - ->shouldThrow(new \RuntimeException('Failed to create the configured bucket "bucketName".')) - ->duringExists('filename'); + $this->write('filename', 'some content')->shouldReturn(12); + $this->getAcl()->shouldBe('123abc'); } /** * @param \AmazonS3 $service */ - function it_should_set_region_when_region_option_is_set($service) + function its_file_metadata_acl_are_more_important_than_global_acl_config($service) { + $this->setAcl('123abc'); $service - ->set_region(\AmazonS3::REGION_EU_W1) - ->shouldBeCalled(); + ->if_bucket_exists('bucketName') + ->shouldBeCalled() + ->willReturn(true) + ; + $service + ->create_object( + 'bucketName', + 'filename', + array( + 'acl' => 'more important acl', + 'body' => 'some content' + ) + ) + ->shouldBeCalled() + ->willReturn(new \CFResponse(array('x-aws-requestheaders' => array('Content-Length' => 12)), 'some content', 200)) + ; - $this->beConstructedWith($service, 'bucketName', array('region' => \AmazonS3::REGION_EU_W1)); - $this->exists('filename'); + $this->setMetadata('filename', array('acl' => 'more important acl')); + $this->write('filename', 'some content')->shouldReturn(12); } } From 3adf649e4b8fd34a705fc1a63f774d21ebf1eecd Mon Sep 17 00:00:00 2001 From: l3l0 Date: Thu, 27 Jun 2013 11:47:04 +0200 Subject: [PATCH 014/426] Updated phpspec for newest version. Migrate specs. --- composer.json | 4 +- composer.lock | 323 ++++++++++-------- ...eAmazonS3.php => AclAwareAmazonS3Spec.php} | 60 ++-- .../{AmazonS3.php => AmazonS3Spec.php} | 56 ++- .../Adapter/{Apc.php => ApcSpec.php} | 19 +- .../Adapter/{Cache.php => CacheSpec.php} | 47 ++- ...{DoctrineDbal.php => DoctrineDbalSpec.php} | 68 ++-- .../Adapter/{Dropbox.php => DropboxSpec.php} | 85 +++-- .../Adapter/{Ftp.php => FtpSpec.php} | 49 ++- .../Adapter/{GridFS.php => GridFSSpec.php} | 152 ++++++--- .../{InMemory.php => InMemorySpec.php} | 24 +- ...es.php => LazyRackspaceCloudfilesSpec.php} | 32 +- .../Adapter/{Local.php => LocalSpec.php} | 85 +++-- .../{MogileFS.php => MogileFSSpec.php} | 9 +- ...hpseclibSftp.php => PhpseclibSftpSpec.php} | 49 ++- ...> AuthenticationConnectionFactorySpec.php} | 16 +- ...dfiles.php => RackspaceCloudfilesSpec.php} | 91 +++-- .../{SafeLocal.php => SafeLocalSpec.php} | 13 +- .../Adapter/{Sftp.php => SftpSpec.php} | 58 +++- .../Adapter/{Zip.php => ZipSpec.php} | 7 +- spec/Gaufrette/{File.php => FileSpec.php} | 52 ++- ...ilesystemMap.php => FilesystemMapSpec.php} | 24 +- .../{Filesystem.php => FilesystemSpec.php} | 91 ++--- .../{StreamMode.php => StreamModeSpec.php} | 8 +- ...treamWrapper.php => StreamWrapperSpec.php} | 75 ++-- .../Util/{Checksum.php => ChecksumSpec.php} | 8 +- .../Gaufrette/Util/{Path.php => PathSpec.php} | 8 +- .../Gaufrette/Util/{Size.php => SizeSpec.php} | 6 +- src/Gaufrette/Adapter/Cache.php | 3 +- 29 files changed, 877 insertions(+), 645 deletions(-) rename spec/Gaufrette/Adapter/{AclAwareAmazonS3.php => AclAwareAmazonS3Spec.php} (83%) rename spec/Gaufrette/Adapter/{AmazonS3.php => AmazonS3Spec.php} (88%) rename spec/Gaufrette/Adapter/{Apc.php => ApcSpec.php} (77%) rename spec/Gaufrette/Adapter/{Cache.php => CacheSpec.php} (74%) rename spec/Gaufrette/Adapter/{DoctrineDbal.php => DoctrineDbalSpec.php} (71%) rename spec/Gaufrette/Adapter/{Dropbox.php => DropboxSpec.php} (74%) rename spec/Gaufrette/Adapter/{Ftp.php => FtpSpec.php} (71%) rename spec/Gaufrette/Adapter/{GridFS.php => GridFSSpec.php} (65%) rename spec/Gaufrette/Adapter/{InMemory.php => InMemorySpec.php} (71%) rename spec/Gaufrette/Adapter/{LazyRackspaceCloudfiles.php => LazyRackspaceCloudfilesSpec.php} (73%) rename spec/Gaufrette/Adapter/{Local.php => LocalSpec.php} (68%) rename spec/Gaufrette/Adapter/{MogileFS.php => MogileFSSpec.php} (61%) rename spec/Gaufrette/Adapter/{PhpseclibSftp.php => PhpseclibSftpSpec.php} (80%) rename spec/Gaufrette/Adapter/RackspaceCloudfiles/{AuthenticationConnectionFactory.php => AuthenticationConnectionFactorySpec.php} (64%) rename spec/Gaufrette/Adapter/{RackspaceCloudfiles.php => RackspaceCloudfilesSpec.php} (75%) rename spec/Gaufrette/Adapter/{SafeLocal.php => SafeLocalSpec.php} (65%) rename spec/Gaufrette/Adapter/{Sftp.php => SftpSpec.php} (63%) rename spec/Gaufrette/Adapter/{Zip.php => ZipSpec.php} (54%) rename spec/Gaufrette/{File.php => FileSpec.php} (73%) rename spec/Gaufrette/{FilesystemMap.php => FilesystemMapSpec.php} (69%) rename spec/Gaufrette/{Filesystem.php => FilesystemSpec.php} (79%) rename spec/Gaufrette/{StreamMode.php => StreamModeSpec.php} (95%) rename spec/Gaufrette/{StreamWrapper.php => StreamWrapperSpec.php} (71%) rename spec/Gaufrette/Util/{Checksum.php => ChecksumSpec.php} (75%) rename spec/Gaufrette/Util/{Path.php => PathSpec.php} (79%) rename spec/Gaufrette/Util/{Size.php => SizeSpec.php} (67%) diff --git a/composer.json b/composer.json index ba2f53574..6078e46b9 100644 --- a/composer.json +++ b/composer.json @@ -43,7 +43,7 @@ "require-dev": { "amazonwebservices/aws-sdk-for-php": "1.5.*", "rackspace/php-opencloud" : "dev-master", - "phpspec/phpspec2": "dev-master", + "phpspec/phpspec": "2.0.*", "phpseclib/phpseclib": "dev-master", "rackspace/php-cloudfiles": "*", "doctrine/dbal": ">=2.3", @@ -52,7 +52,7 @@ "phpunit/phpunit": "3.7.*" }, "suggest": { - "knplabs/knp-gaufrette-bundle": "*", + "knplabs/knp-gaufrette-bundle": "to use with Symfony2", "dropbox-php/dropbox-php": "to use the Dropbox adapter", "rackspace/php-opencloud" : "to use Opencloud adapter", "amazonwebservices/aws-sdk-for-php": "to use the Amazon S3 adapter", diff --git a/composer.lock b/composer.lock index 3eea5c3f6..088b55124 100644 --- a/composer.lock +++ b/composer.lock @@ -3,7 +3,7 @@ "This file locks the dependencies of your project to a known state", "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" ], - "hash": "eb71255413a0b25ca71e82c7dd1e2acf", + "hash": "6c9e68f35722f69905b4c179167e6302", "packages": [ ], @@ -116,7 +116,7 @@ { "name": "Johannes Schmitt", "email": "schmittjoh@gmail.com", - "homepage": "https://github.com/schmittjoh", + "homepage": "http://jmsyst.com", "role": "Developer of wrapped JMSSerializerBundle" } ], @@ -135,12 +135,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/cache.git", - "reference": "45123145f70dd79618963a72a5271b4f389712e4" + "reference": "89493d2c6e1362f581f9de1c1871cc52eb29c030" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/45123145f70dd79618963a72a5271b4f389712e4", - "reference": "45123145f70dd79618963a72a5271b4f389712e4", + "url": "https://api.github.com/repos/doctrine/cache/zipball/89493d2c6e1362f581f9de1c1871cc52eb29c030", + "reference": "89493d2c6e1362f581f9de1c1871cc52eb29c030", "shasum": "" }, "require": { @@ -196,7 +196,7 @@ "cache", "caching" ], - "time": "2013-05-13 02:51:07" + "time": "2013-06-07 14:54:47" }, { "name": "doctrine/collections", @@ -252,7 +252,7 @@ { "name": "Johannes Schmitt", "email": "schmittjoh@gmail.com", - "homepage": "https://github.com/schmittjoh", + "homepage": "http://jmsyst.com", "role": "Developer of wrapped JMSSerializerBundle" } ], @@ -271,12 +271,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/common.git", - "reference": "2169b0ce1d253d448c60b7d40bbe4e4b5afe22fe" + "reference": "c4255b9fbd63ee1fe52697839318af5937fced9b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/common/zipball/2169b0ce1d253d448c60b7d40bbe4e4b5afe22fe", - "reference": "2169b0ce1d253d448c60b7d40bbe4e4b5afe22fe", + "url": "https://api.github.com/repos/doctrine/common/zipball/c4255b9fbd63ee1fe52697839318af5937fced9b", + "reference": "c4255b9fbd63ee1fe52697839318af5937fced9b", "shasum": "" }, "require": { @@ -322,7 +322,7 @@ "email": "kontakt@beberlei.de" }, { - "name": "Johannes Schmitt", + "name": "Johannes M. Schmitt", "email": "schmittjoh@gmail.com", "homepage": "http://jmsyst.com", "role": "Developer of wrapped JMSSerializerBundle" @@ -337,7 +337,7 @@ "persistence", "spl" ], - "time": "2013-05-27 19:11:46" + "time": "2013-06-21 12:11:28" }, { "name": "doctrine/dbal", @@ -345,12 +345,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "da9564df54d248de03f9fd3d31e5eade86f8f747" + "reference": "3253f7a1c13a2177d925bd0f6ee8387c5dd9c57a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/da9564df54d248de03f9fd3d31e5eade86f8f747", - "reference": "da9564df54d248de03f9fd3d31e5eade86f8f747", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/3253f7a1c13a2177d925bd0f6ee8387c5dd9c57a", + "reference": "3253f7a1c13a2177d925bd0f6ee8387c5dd9c57a", "shasum": "" }, "require": { @@ -407,7 +407,7 @@ "persistence", "queryobject" ], - "time": "2013-05-27 19:17:11" + "time": "2013-06-25 17:49:29" }, { "name": "doctrine/inflector", @@ -463,7 +463,7 @@ { "name": "Johannes Schmitt", "email": "schmittjoh@gmail.com", - "homepage": "https://github.com/schmittjoh", + "homepage": "http://jmsyst.com", "role": "Developer of wrapped JMSSerializerBundle" } ], @@ -522,7 +522,7 @@ { "name": "Johannes Schmitt", "email": "schmittjoh@gmail.com", - "homepage": "https://github.com/schmittjoh", + "homepage": "http://jmsyst.com", "role": "Developer of wrapped JMSSerializerBundle" } ], @@ -643,71 +643,18 @@ ], "time": "2013-05-24 20:02:36" }, - { - "name": "mockery/mockery", - "version": "0.7.2", - "source": { - "type": "git", - "url": "git://github.com/padraic/mockery.git", - "reference": "0.7.2" - }, - "dist": { - "type": "zip", - "url": "https://github.com/padraic/mockery/zipball/0.7.2", - "reference": "0.7.2", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "suggest": { - "Hamcrest": "1.0.0" - }, - "type": "library", - "autoload": { - "psr-0": { - "Mockery": "library/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "New BSD" - ], - "authors": [ - { - "name": "Pádraic Brady", - "email": "padraic.brady@gmail.com", - "homepage": "http://blog.astrumfutura.com" - } - ], - "description": "Mockery is a simple yet flexible PHP mock object framework for use in unit testing with PHPUnit, PHPSpec or any other testing framework. Its core goal is to offer a test double framework with a succint API capable of clearly defining all possible object operations and interactions using a human readable Domain Specific Language (DSL). Designed as a drop in alternative to PHPUnit's phpunit-mock-objects library, Mockery is easy to integrate with PHPUnit and can operate alongside phpunit-mock-objects without the World ending.", - "homepage": "http://github.com/padraic/mockery", - "keywords": [ - "BDD", - "TDD", - "library", - "mock", - "mock objects", - "mockery", - "stub", - "test", - "test double", - "testing" - ], - "time": "2012-01-24 12:22:39" - }, { "name": "phpseclib/phpseclib", "version": "dev-master", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "f596c577cf3a666be3818316562db90aced09330" + "reference": "f102ce62e202f0d3c26e862755c872696bbd2cf0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/f596c577cf3a666be3818316562db90aced09330", - "reference": "f596c577cf3a666be3818316562db90aced09330", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/f102ce62e202f0d3c26e862755c872696bbd2cf0", + "reference": "f102ce62e202f0d3c26e862755c872696bbd2cf0", "shasum": "" }, "require": { @@ -768,11 +715,11 @@ "x.509", "x509" ], - "time": "2013-05-21 14:01:10" + "time": "2013-06-26 06:15:04" }, { "name": "phpspec/php-diff", - "version": "dev-master", + "version": "v1.0.1", "source": { "type": "git", "url": "https://github.com/phpspec/php-diff.git", @@ -804,34 +751,43 @@ "time": "2012-11-08 08:55:45" }, { - "name": "phpspec/phpspec2", + "name": "phpspec/phpspec", "version": "dev-master", "source": { "type": "git", - "url": "https://github.com/phpspec/phpspec2.git", - "reference": "c6140c098d79a4ed10eba2411817a32f72d4a367" + "url": "https://github.com/phpspec/phpspec.git", + "reference": "56819ea76a8d59e615e0a9c099451c012338a72d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/phpspec2/zipball/c6140c098d79a4ed10eba2411817a32f72d4a367", - "reference": "c6140c098d79a4ed10eba2411817a32f72d4a367", + "url": "https://api.github.com/repos/phpspec/phpspec/zipball/56819ea76a8d59e615e0a9c099451c012338a72d", + "reference": "56819ea76a8d59e615e0a9c099451c012338a72d", "shasum": "" }, "require": { - "mockery/mockery": "0.7.*", - "php": ">=5.3.1", - "phpspec/php-diff": "*@dev", - "symfony/console": ">=2.1.0,<2.2.0", - "symfony/event-dispatcher": ">=2.1.0,<2.2.0", - "symfony/finder": ">=2.1.0,<2.2.0" + "php": ">=5.3.3", + "phpspec/php-diff": ">=1.0,<1.1", + "phpspec/prophecy": ">=1.0.2,<1.1", + "symfony/console": ">=2.1,<3.0", + "symfony/event-dispatcher": ">=2.1,<3.0", + "symfony/finder": ">=2.1,<3.0", + "symfony/yaml": ">=2.1,<3.0" + }, + "suggest": { + "whatthejeff/nyancat-scoreboard": "~1.1 – Enables the Nyan Cat formatter" }, "bin": [ "bin/phpspec" ], "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, "autoload": { "psr-0": { - "PHPSpec2": "src/" + "PhpSpec": "src/" } }, "notification-url": "https://packagist.org/downloads/", @@ -849,16 +805,73 @@ "homepage": "http://marcelloduarte.net/" } ], - "description": "Specification-oriented BDD framework for PHP 5.4", + "description": "Specification-oriented BDD framework for PHP 5.3+", "homepage": "http://phpspec.net/", "keywords": [ "BDD", "SpecBDD", "TDD", "spec", - "specification" + "specification", + "testing", + "tests" ], - "time": "2013-02-08 11:00:28" + "time": "2013-06-24 08:45:19" + }, + { + "name": "phpspec/prophecy", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/phpspec/prophecy.git", + "reference": "593c8c679369c0ea49616cd812a0fb2984084757" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/593c8c679369c0ea49616cd812a0fb2984084757", + "reference": "593c8c679369c0ea49616cd812a0fb2984084757", + "shasum": "" + }, + "require-dev": { + "phpspec/phpspec": "2.0.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "Prophecy\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Konstantin Kudryashov", + "email": "ever.zet@gmail.com", + "homepage": "http://everzet.com" + }, + { + "name": "Marcello Duarte", + "email": "marcello.duarte@gmail.com" + } + ], + "description": "Highly opinionated mocking framework for PHP 5.3+", + "homepage": "http://phpspec.org", + "keywords": [ + "Double", + "Dummy", + "fake", + "mock", + "spy", + "stub" + ], + "time": "2013-06-19 15:13:15" }, { "name": "phpunit/php-code-coverage", @@ -866,12 +879,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "1.2.11" + "reference": "58e6aa238f270d67187d16a50e14503bd9d02bee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/1.2.11", - "reference": "1.2.11", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/58e6aa238f270d67187d16a50e14503bd9d02bee", + "reference": "58e6aa238f270d67187d16a50e14503bd9d02bee", "shasum": "" }, "require": { @@ -881,13 +894,18 @@ "phpunit/php-token-stream": ">=1.1.3@stable" }, "require-dev": { - "phpunit/phpunit": "3.7.*" + "phpunit/phpunit": "3.7.*@dev" }, "suggest": { "ext-dom": "*", "ext-xdebug": ">=2.0.5" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, "autoload": { "classmap": [ "PHP/" @@ -914,7 +932,7 @@ "testing", "xunit" ], - "time": "2013-05-23 18:23:24" + "time": "2013-06-24 11:56:39" }, { "name": "phpunit/php-file-iterator", @@ -1100,12 +1118,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "3.7.21" + "reference": "30110580b640440f6442a58322ee657216d2d8ba" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3.7.21", - "reference": "3.7.21", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/30110580b640440f6442a58322ee657216d2d8ba", + "reference": "30110580b640440f6442a58322ee657216d2d8ba", "shasum": "" }, "require": { @@ -1114,11 +1132,11 @@ "ext-reflection": "*", "ext-spl": "*", "php": ">=5.3.3", - "phpunit/php-code-coverage": ">=1.2.1,<1.3.0", + "phpunit/php-code-coverage": ">=1.2.1,<1.3", "phpunit/php-file-iterator": ">=1.3.1", "phpunit/php-text-template": ">=1.1.1", - "phpunit/php-timer": ">=1.0.2,<1.1.0", - "phpunit/phpunit-mock-objects": ">=1.2.0,<1.3.0", + "phpunit/php-timer": ">=1.0.2,<1.1", + "phpunit/phpunit-mock-objects": ">=1.2,<1.3", "symfony/yaml": ">=2.0,<3.0" }, "require-dev": { @@ -1166,7 +1184,7 @@ "testing", "xunit" ], - "time": "2013-05-23 18:54:29" + "time": "2013-06-23 07:31:03" }, { "name": "phpunit/phpunit-mock-objects", @@ -1174,22 +1192,31 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "8e9d897b39ca6310ccc2d81b8f29cabd5ce12786" + "reference": "977c5327c5f9e2a0f08728a59389ee604e5b2f47" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/8e9d897b39ca6310ccc2d81b8f29cabd5ce12786", - "reference": "8e9d897b39ca6310ccc2d81b8f29cabd5ce12786", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/977c5327c5f9e2a0f08728a59389ee604e5b2f47", + "reference": "977c5327c5f9e2a0f08728a59389ee604e5b2f47", "shasum": "" }, "require": { "php": ">=5.3.3", "phpunit/php-text-template": ">=1.1.1@stable" }, + "require-dev": { + "pear-pear/pear": "1.9.4", + "phpunit/phpunit": "3.7.*@dev" + }, "suggest": { "ext-soap": "*" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2.x-dev" + } + }, "autoload": { "classmap": [ "PHPUnit/" @@ -1215,7 +1242,7 @@ "mock", "xunit" ], - "time": "2013-05-07 12:11:45" + "time": "2013-06-23 05:41:31" }, { "name": "rackspace/php-cloudfiles", @@ -1245,12 +1272,12 @@ "source": { "type": "git", "url": "https://github.com/rackspace/php-opencloud.git", - "reference": "v1.5.7" + "reference": "d6218f4a8bfaed12b0a711246b7d51a13e27b9a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rackspace/php-opencloud/zipball/v1.5.7", - "reference": "v1.5.7", + "url": "https://api.github.com/repos/rackspace/php-opencloud/zipball/d6218f4a8bfaed12b0a711246b7d51a13e27b9a0", + "reference": "d6218f4a8bfaed12b0a711246b7d51a13e27b9a0", "shasum": "" }, "require": { @@ -1267,35 +1294,56 @@ "license": [ "MIT" ], + "authors": [ + { + "name": "Glen Campbell", + "email": "glen.campbell@rackspace.com" + }, + { + "name": "Jamie Hannaford", + "email": "jamie@limetree.org" + } + ], "description": "Rackspace open cloud php library", "keywords": [ "cloudfiles", "opencloud" ], - "time": "2013-05-23 19:22:19" + "time": "2013-06-22 19:45:44" }, { "name": "symfony/console", - "version": "2.1.x-dev", + "version": "dev-master", "target-dir": "Symfony/Component/Console", "source": { "type": "git", "url": "https://github.com/symfony/Console.git", - "reference": "4709ede4eb34e02d9349b6bf2809bd76be008404" + "reference": "b68fd0262ebae63f796d188f48eaa470044861fd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Console/zipball/4709ede4eb34e02d9349b6bf2809bd76be008404", - "reference": "4709ede4eb34e02d9349b6bf2809bd76be008404", + "url": "https://api.github.com/repos/symfony/Console/zipball/b68fd0262ebae63f796d188f48eaa470044861fd", + "reference": "b68fd0262ebae63f796d188f48eaa470044861fd", "shasum": "" }, "require": { "php": ">=5.3.3" }, + "require-dev": { + "symfony/event-dispatcher": ">=2.1,<3.0" + }, + "suggest": { + "symfony/event-dispatcher": "" + }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.4-dev" + } + }, "autoload": { "psr-0": { - "Symfony\\Component\\Console": "" + "Symfony\\Component\\Console\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -1314,37 +1362,42 @@ ], "description": "Symfony Console Component", "homepage": "http://symfony.com", - "time": "2013-05-26 18:42:07" + "time": "2013-06-23 08:16:33" }, { "name": "symfony/event-dispatcher", - "version": "2.1.x-dev", + "version": "dev-master", "target-dir": "Symfony/Component/EventDispatcher", "source": { "type": "git", "url": "https://github.com/symfony/EventDispatcher.git", - "reference": "v2.1.10" + "reference": "4b34aad44d0a2d91f301ee967151ce0980f39930" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/v2.1.10", - "reference": "v2.1.10", + "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/4b34aad44d0a2d91f301ee967151ce0980f39930", + "reference": "4b34aad44d0a2d91f301ee967151ce0980f39930", "shasum": "" }, "require": { "php": ">=5.3.3" }, "require-dev": { - "symfony/dependency-injection": "2.1.*" + "symfony/dependency-injection": ">=2.0,<3.0" }, "suggest": { - "symfony/dependency-injection": "2.1.*", - "symfony/http-kernel": "2.1.*" + "symfony/dependency-injection": "", + "symfony/http-kernel": "" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.4-dev" + } + }, "autoload": { "psr-0": { - "Symfony\\Component\\EventDispatcher": "" + "Symfony\\Component\\EventDispatcher\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -1363,30 +1416,35 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "http://symfony.com", - "time": "2013-02-11 11:26:14" + "time": "2013-05-16 07:54:39" }, { "name": "symfony/finder", - "version": "2.1.x-dev", + "version": "dev-master", "target-dir": "Symfony/Component/Finder", "source": { "type": "git", "url": "https://github.com/symfony/Finder.git", - "reference": "0dbc61194b58bc513e003789853ddfe0aea8cf33" + "reference": "d8da924d4f7d9f325ebd72feefee40c17a3e9104" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Finder/zipball/0dbc61194b58bc513e003789853ddfe0aea8cf33", - "reference": "0dbc61194b58bc513e003789853ddfe0aea8cf33", + "url": "https://api.github.com/repos/symfony/Finder/zipball/d8da924d4f7d9f325ebd72feefee40c17a3e9104", + "reference": "d8da924d4f7d9f325ebd72feefee40c17a3e9104", "shasum": "" }, "require": { "php": ">=5.3.3" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.4-dev" + } + }, "autoload": { "psr-0": { - "Symfony\\Component\\Finder": "" + "Symfony\\Component\\Finder\\": "" } }, "notification-url": "https://packagist.org/downloads/", @@ -1405,7 +1463,7 @@ ], "description": "Symfony Finder Component", "homepage": "http://symfony.com", - "time": "2013-05-25 15:47:15" + "time": "2013-06-23 08:16:33" }, { "name": "symfony/yaml", @@ -1461,7 +1519,6 @@ "minimum-stability": "dev", "stability-flags": { "rackspace/php-opencloud": 20, - "phpspec/phpspec2": 20, "phpseclib/phpseclib": 20 }, "platform": { diff --git a/spec/Gaufrette/Adapter/AclAwareAmazonS3.php b/spec/Gaufrette/Adapter/AclAwareAmazonS3Spec.php similarity index 83% rename from spec/Gaufrette/Adapter/AclAwareAmazonS3.php rename to spec/Gaufrette/Adapter/AclAwareAmazonS3Spec.php index 4a833d26d..8898c0d03 100644 --- a/spec/Gaufrette/Adapter/AclAwareAmazonS3.php +++ b/spec/Gaufrette/Adapter/AclAwareAmazonS3Spec.php @@ -2,9 +2,9 @@ namespace spec\Gaufrette\Adapter; -use PHPSpec2\ObjectBehavior; +use PhpSpec\ObjectBehavior; -class AclAwareAmazonS3 extends ObjectBehavior +class AclAwareAmazonS3Spec extends ObjectBehavior { /** * @param \Gaufrette\Adapter $adapter @@ -15,9 +15,8 @@ function let($adapter, $service) $this->beConstructedWith($adapter, $service, 'bucketName'); } - function it_is_initializable() + function it_is_adapter() { - $this->shouldHaveType('Gaufrette\Adapter\AclAwareAmazonS3'); $this->shouldHaveType('Gaufrette\Adapter'); } @@ -47,14 +46,17 @@ function it_delegates_rename_and_update_acl($adapter, $service) $service ->set_object_acl('bucketName', 'filename2', \AmazonS3::ACL_PRIVATE) ->shouldBeCalled() - ->willReturn(new \CFResponse(array(), '', 200)); + ->willReturn(new \CFResponse(array(), '', 200)) + ; $adapter ->rename('filename', 'filename2') ->shouldBeCalled() - ->willReturn(true); + ->willReturn(true) + ; $adapter ->delete('filename') - ->shouldNotBeCalled(); + ->shouldNotBeCalled() + ; $this->rename('filename', 'filename2')->shouldReturn(true); } @@ -89,10 +91,12 @@ function it_updates_acl_with_users_array_when_rename($adapter, $service) $service ->set_object_acl('bucketName', 'filename2', array(array('id' => 'someId', 'permission' => \AmazonS3::GRANT_READ))) ->shouldBeCalled() - ->willReturn(new \CFResponse(array(), '', 200)); + ->willReturn(new \CFResponse(array(), '', 200)) + ; $adapter ->rename('filename', 'filename2') - ->willReturn(true); + ->willReturn(true) + ; $this->setUsers(array(array('id' => 'someId', 'permission' => 'read'))); $this->rename('filename', 'filename2')->shouldReturn(true); @@ -107,14 +111,17 @@ function it_delegates_write_and_update_acl($adapter, $service) $service ->set_object_acl('bucketName', 'filename', \AmazonS3::ACL_PRIVATE) ->shouldBeCalled() - ->willReturn(new \CFResponse(array(), '', 200)); + ->willReturn(new \CFResponse(array(), '', 200)) + ; $adapter ->write('filename', 'some content') ->shouldBeCalled() - ->willReturn(12); + ->willReturn(12) + ; $adapter ->delete('filename') - ->shouldNotBeCalled(); + ->shouldNotBeCalled() + ; $this->write('filename', 'some content')->shouldReturn(12); } @@ -128,14 +135,17 @@ function it_does_not_write_when_cannot_update_acl($adapter, $service) $service ->set_object_acl('bucketName', 'filename', \AmazonS3::ACL_PRIVATE) ->shouldBeCalled() - ->willReturn(new \CFResponse(array(), '', 500)); + ->willReturn(new \CFResponse(array(), '', 500)) + ; $adapter ->write('filename', 'some content') ->shouldBeCalled() - ->willReturn(12); + ->willReturn(12) + ; $adapter ->delete('filename') - ->shouldBeCalled(); + ->shouldBeCalled() + ; $this->write('filename', 'some content')->shouldReturn(false); } @@ -149,10 +159,12 @@ function it_updates_acl_with_users_array_when_write($adapter, $service) $service ->set_object_acl('bucketName', 'filename', array(array('id' => 'someId', 'permission' => \AmazonS3::GRANT_READ))) ->shouldBeCalled() - ->willReturn(new \CFResponse(array(), '', 200)); + ->willReturn(new \CFResponse(array(), '', 200)) + ; $adapter ->write('filename', 'some content') - ->willReturn(12); + ->willReturn(12) + ; $this->setUsers(array(array('id' => 'someId', 'permission' => 'read'))); $this->write('filename', 'some content')->shouldReturn(12); @@ -199,7 +211,7 @@ function it_delegates_directory_check($adapter) */ function it_delegates_keys($adapter) { - $adapter->keys->willReturn(array('filename', 'filename2')); + $adapter->keys()->willReturn(array('filename', 'filename2')); $this->keys()->shouldReturn(array('filename', 'filename2')); } @@ -218,18 +230,6 @@ function it_delegates_metadata_handling($extendedAdapter, $service) $this->setMetadata('filename', array('some')); $this->getMetadata('filename')->shouldReturn(array('some2')); } - - /** - * @param \Gaufrette\Adapter $adapter - */ - function it_does_not_delegate_metadata_when_delegate_object_does_not_support_it($adapter) - { - $adapter->setMetadata('filename', array('some'))->shouldNotBeCalled(); - $adapter->getMetadata('filename')->shouldNotBeCalled(); - - $this->setMetadata('filename', array('some')); - $this->getMetadata('filename')->shouldReturn(array()); - } } interface TestDelegateAdapter extends \Gaufrette\Adapter, diff --git a/spec/Gaufrette/Adapter/AmazonS3.php b/spec/Gaufrette/Adapter/AmazonS3Spec.php similarity index 88% rename from spec/Gaufrette/Adapter/AmazonS3.php rename to spec/Gaufrette/Adapter/AmazonS3Spec.php index 5504631d6..ef108c38b 100644 --- a/spec/Gaufrette/Adapter/AmazonS3.php +++ b/spec/Gaufrette/Adapter/AmazonS3Spec.php @@ -2,9 +2,10 @@ namespace spec\Gaufrette\Adapter; -use PHPSpec2\ObjectBehavior; +use PhpSpec\ObjectBehavior; +use Prophecy\Argument; -class AmazonS3 extends ObjectBehavior +class AmazonS3Spec extends ObjectBehavior { /** * @param \AmazonS3 $service @@ -14,9 +15,8 @@ function let($service) $this->beConstructedWith($service, 'bucketName'); } - function it_is_amazon_adapter() + function it_is_adapter() { - $this->shouldHaveType('Gaufrette\Adapter\AmazonS3'); $this->shouldHaveType('Gaufrette\Adapter'); } @@ -51,6 +51,7 @@ function it_reads_file($service) ->shouldBeCalled() ->willReturn(new \CFResponse('header', 'some content', 200)) ; + $service->set_region(Argument::any())->shouldBeCalled(); $this->setMetadata('filename', $options); $this->read('filename')->shouldReturn('some content'); @@ -75,6 +76,7 @@ function it_returns_false_when_cannot_read($service) ->shouldBeCalled() ->willReturn(new \CFResponse('header', 'some content', 500)) ; + $service->set_region(Argument::any())->shouldBeCalled(); $this->read('filename')->shouldReturn(false); } @@ -97,6 +99,7 @@ function it_is_verbose_and_throws_exceptions_when_read($service) ) ->willThrow(new \RuntimeException('read')) ; + $service->set_region(Argument::any())->shouldBeCalled(); $this->shouldThrow(new \RuntimeException('read'))->duringRead('filename'); } @@ -111,6 +114,7 @@ function it_rename_file($service) ->shouldBeCalled() ->willReturn(true) ; + $service->set_region(Argument::any())->shouldBeCalled(); $service ->copy_object( array( @@ -130,7 +134,7 @@ function it_rename_file($service) ->delete_object( 'bucketName', 'filename1', - ANY_ARGUMENT + Argument::any() ) ->shouldBeCalled() ->willReturn(new \CFResponse(array(), 'some', 200)) @@ -145,13 +149,14 @@ function it_rename_file($service) */ function it_is_verbose_and_throws_exceptions_when_rename($service) { + $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() ->willReturn(true) ; $service - ->copy_object(ANY_ARGUMENT, ANY_ARGUMENT, ANY_ARGUMENT) + ->copy_object(Argument::cetera()) ->willThrow(new \RuntimeException('rename')) ; @@ -163,6 +168,7 @@ function it_is_verbose_and_throws_exceptions_when_rename($service) */ function it_returns_false_when_cannot_rename($service) { + $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() @@ -192,6 +198,7 @@ function it_returns_false_when_cannot_rename($service) */ function it_should_write_file($service) { + $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() @@ -219,6 +226,7 @@ function it_should_write_file($service) */ function it_returns_false_when_cannot_write($service) { + $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() @@ -245,13 +253,14 @@ function it_returns_false_when_cannot_write($service) */ function it_is_verbose_and_throws_exceptions_when_write($service) { + $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() ->willReturn(true) ; $service - ->create_object(ANY_ARGUMENT, ANY_ARGUMENT, ANY_ARGUMENT) + ->create_object(Argument::cetera()) ->willThrow(new \RuntimeException('write')) ; @@ -263,6 +272,7 @@ function it_is_verbose_and_throws_exceptions_when_write($service) */ function it_should_check_if_file_exists($service) { + $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() @@ -281,6 +291,7 @@ function it_should_check_if_file_exists($service) */ function it_is_verbose_and_throws_exceptions_when_file_exists($service) { + $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() @@ -299,6 +310,7 @@ function it_is_verbose_and_throws_exceptions_when_file_exists($service) */ function it_should_get_file_mtime($service) { + $service->set_region(Argument::any())->shouldBeCalled(); $metadata = array('acl' => \AmazonS3::ACL_PUBLIC); $service ->if_bucket_exists('bucketName') @@ -325,6 +337,7 @@ function it_should_get_file_mtime($service) */ function it_returns_false_when_cannot_fetch_mtime($service) { + $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() @@ -349,13 +362,14 @@ function it_returns_false_when_cannot_fetch_mtime($service) */ function it_is_verbose_and_throws_exceptions_when_fetch_mtime($service) { + $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() ->willReturn(true) ; $service - ->get_object_metadata('bucketName', 'filename', ANY_ARGUMENT) + ->get_object_metadata('bucketName', 'filename', Argument::any()) ->willThrow(new \RuntimeException('mtime')) ; @@ -369,6 +383,7 @@ function it_should_delete_file($service) { $metadata = array('acl' => \AmazonS3::ACL_PRIVATE); + $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() @@ -392,6 +407,7 @@ function it_should_delete_file($service) */ function it_is_verbose_and_throws_exceptions_when_fetch_delete($service) { + $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->willReturn(true) @@ -400,7 +416,7 @@ function it_is_verbose_and_throws_exceptions_when_fetch_delete($service) ->delete_object( 'bucketName', 'filename', - ANY_ARGUMENT + Argument::any() ) ->willThrow(new \RuntimeException('delete')) ; @@ -413,6 +429,7 @@ function it_is_verbose_and_throws_exceptions_when_fetch_delete($service) */ function it_returns_false_when_cannot_delete($service) { + $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() @@ -435,6 +452,7 @@ function it_returns_false_when_cannot_delete($service) */ function it_should_get_keys($service) { + $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() @@ -454,6 +472,7 @@ function it_should_get_keys($service) */ function it_is_verbose_and_throws_exceptions_when_fetch_keys($service) { + $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->willReturn(true) @@ -471,6 +490,7 @@ function it_is_verbose_and_throws_exceptions_when_fetch_keys($service) */ function it_should_handle_dirs($service) { + $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->willReturn(true) @@ -498,6 +518,7 @@ function it_should_handle_dirs($service) */ function it_should_fail_when_bucket_does_not_exist($service) { + $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->willReturn(false) @@ -537,6 +558,7 @@ function it_should_fail_when_bucket_does_not_exist($service) */ function it_creates_bucket_if_create_mode_is_enabled($service) { + $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->willReturn(false) @@ -560,6 +582,7 @@ function it_creates_bucket_if_create_mode_is_enabled($service) */ function it_fails_when_cannot_create_bucket($service) { + $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->willReturn(false) @@ -582,9 +605,19 @@ function it_fails_when_cannot_create_bucket($service) */ function it_allows_to_configure_reqion($service) { + $service->set_region(Argument::any())->shouldBeCalled(); + $service + ->if_bucket_exists('bucketName') + ->willReturn(true) + ; $service ->set_region(\AmazonS3::REGION_EU_W1) - ->shouldBeCalled(); + ->shouldBeCalled() + ; + $service + ->if_object_exists('bucketName', 'filename') + ->willReturn(true) + ; $this->beConstructedWith($service, 'bucketName', array('region' => \AmazonS3::REGION_EU_W1)); $this->exists('filename'); @@ -595,6 +628,7 @@ function it_allows_to_configure_reqion($service) */ function it_allows_to_configure_region_for_bucket($service) { + $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->willReturn(false) @@ -619,6 +653,7 @@ function it_allows_to_configure_region_for_bucket($service) function it_allows_to_configure_acl($service) { $this->setAcl('123abc'); + $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() @@ -647,6 +682,7 @@ function it_allows_to_configure_acl($service) function its_file_metadata_acl_are_more_important_than_global_acl_config($service) { $this->setAcl('123abc'); + $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() diff --git a/spec/Gaufrette/Adapter/Apc.php b/spec/Gaufrette/Adapter/ApcSpec.php similarity index 77% rename from spec/Gaufrette/Adapter/Apc.php rename to spec/Gaufrette/Adapter/ApcSpec.php index 30f85e284..6a3ae43cc 100644 --- a/spec/Gaufrette/Adapter/Apc.php +++ b/spec/Gaufrette/Adapter/ApcSpec.php @@ -5,9 +5,9 @@ //hack - mock php built-in functions require_once 'functions.php'; -use PHPSpec2\ObjectBehavior; +use PhpSpec\ObjectBehavior; -class Apc extends ObjectBehavior +class ApcSpec extends ObjectBehavior { function let() { @@ -23,44 +23,43 @@ function letgo() $extensionLoaded = null; } - function it_should_be_initializable() + function it_is_adapter() { - $this->shouldHaveType('Gaufrette\Adapter\Apc'); $this->shouldHaveType('Gaufrette\Adapter'); } - function it_should_read_file() + function it_reads_file() { $this->read('filename')->shouldReturn('prefix-apc-test/filename content'); $this->read('filename2')->shouldReturn('prefix-apc-test/filename2 content'); } - function it_should_write_file() + function it_writes_file() { $this->write('filename', 'some content')->shouldReturn(12); $this->write('invalid', 'some content')->shouldReturn(false); } - function it_should_delete_file() + function it_deletes_file() { $this->delete('filename')->shouldReturn(true); $this->delete('invalid')->shouldReturn(false); } - function it_should_rename_file() + function it_rename_file() { $this->rename('filename', 'aaa/filename2')->shouldReturn(true); $this->rename('filename', 'invalid')->shouldReturn(false); $this->rename('invalid', 'somename')->shouldReturn(false); } - function it_should_check_if_file_exists() + function it_checks_if_file_exists() { $this->exists('filename')->shouldReturn(true); $this->exists('invalid')->shouldReturn(false); } - function it_should_not_handles_directory() + function it_does_not_handles_directory() { $this->isDirectory('filename')->shouldReturn(false); $this->isDirectory('invalid')->shouldReturn(false); diff --git a/spec/Gaufrette/Adapter/Cache.php b/spec/Gaufrette/Adapter/CacheSpec.php similarity index 74% rename from spec/Gaufrette/Adapter/Cache.php rename to spec/Gaufrette/Adapter/CacheSpec.php index 593b2b302..01873696c 100644 --- a/spec/Gaufrette/Adapter/Cache.php +++ b/spec/Gaufrette/Adapter/CacheSpec.php @@ -2,9 +2,9 @@ namespace spec\Gaufrette\Adapter; -use PHPSpec2\ObjectBehavior; +use PhpSpec\ObjectBehavior; -class Cache extends ObjectBehavior +class CacheSpec extends ObjectBehavior { /** * @param \Gaufrette\Adapter $source @@ -15,10 +15,13 @@ function let($source, $cache) $this->beConstructedWith($source, $cache); } - function it_should_be_initializable() + function it_is_adapter() { - $this->shouldHaveType('Gaufrette\Adapter\Cache'); $this->shouldHaveType('Gaufrette\Adapter'); + } + + function it_supports_metadata() + { $this->shouldHaveType('Gaufrette\Adapter\MetadataSupporter'); } @@ -26,7 +29,7 @@ function it_should_be_initializable() * @param \spec\Gaufrette\Adapter\CacheTestExtendedAdapter $extendedSource * @param \spec\Gaufrette\Adapter\CacheTestExtendedAdapter $extendedCache */ - function it_should_handle_metadata_when_cached_adapters_supports_metadata($extendedSource, $extendedCache) + function it_handles_metadata_when_cached_adapters_supports_metadata($extendedSource, $extendedCache) { $extendedSource->setMetadata('filename', array('metadata'))->shouldBeCalled(); $extendedCache->setMetadata('filename', array('metadata'))->shouldBeCalled(); @@ -37,24 +40,10 @@ function it_should_handle_metadata_when_cached_adapters_supports_metadata($exten $this->getMetadata('filename')->shouldReturn(array('someMetadata')); } - /** - * @param \Gaufrette\Adapter $source - * @param \Gaufrette\Adapter $cache - */ - function it_should_ignore_metadata_when_cached_adapters_does_not_support_metadata($source, $cache) - { - $source->setMetadata('filename', array('metadata'))->shouldNotBeCalled(); - $cache->setMetadata('filename', array('metadata'))->shouldNotBeCalled(); - $source->getMetadata('filename')->shouldNotBeCalled(); - - $this->setMetadata('filename', array('metadata')); - $this->getMetadata('filename')->shouldReturn(false); - } - /** * @param \Gaufrette\Adapter $source */ - function it_should_delegate_is_directory_to_source($source) + function it_delegates_is_directory_check_to_source($source) { $source->isDirectory('filename')->shouldBeCalled()->willReturn(true); @@ -65,12 +54,13 @@ function it_should_delegate_is_directory_to_source($source) * @param \Gaufrette\Adapter $source * @param \Gaufrette\Adapter $cache */ - function it_should_read_from_cache($source, $cache) + function it_reads_from_cache_adapter($source, $cache) { $source->read('filename')->shouldNotBeCalled(); $cache->read('filename')->shouldBeCalled()->willReturn('some content'); $source->mtime('filename')->willReturn(strtotime('2010-10-11')); $cache->mtime('filename')->willReturn(strtotime('2010-10-12')); + $cache->exists('filename')->willReturn(true); $this->read('filename')->shouldReturn('some content'); } @@ -79,13 +69,14 @@ function it_should_read_from_cache($source, $cache) * @param \Gaufrette\Adapter $source * @param \Gaufrette\Adapter $cache */ - function it_should_read_from_source_and_write_to_cache($source, $cache) + function it_update_cache_adapter_when_source_file_is_modified($source, $cache) { $source->read('filename')->shouldBeCalled()->willReturn('some other content'); $cache->read('filename')->shouldNotBeCalled(); $cache->write('filename', 'some other content')->shouldBeCalled(); $source->mtime('filename')->willReturn(strtotime('2010-10-11')); $cache->mtime('filename')->willReturn(strtotime('2010-10-10')); + $cache->exists('filename')->willReturn(true); $this->read('filename')->shouldReturn('some other content'); } @@ -94,7 +85,7 @@ function it_should_read_from_source_and_write_to_cache($source, $cache) * @param \Gaufrette\Adapter $source * @param \Gaufrette\Adapter $cache */ - function it_should_rename_file_in_source_and_cache($source, $cache) + function it_rename_file_in_source_and_cache($source, $cache) { $source->rename('filename', 'filename2')->shouldBeCalled(); $cache->rename('filename', 'filename2')->shouldBeCalled()->willReturn(true); @@ -106,7 +97,7 @@ function it_should_rename_file_in_source_and_cache($source, $cache) * @param \Gaufrette\Adapter $source * @param \Gaufrette\Adapter $cache */ - function it_should_write_file_to_source_and_cache($source, $cache) + function it_writes_file_to_source_and_cache($source, $cache) { $source->write('filename', 'some content')->shouldBeCalled(); $cache->write('filename', 'some content')->shouldBeCalled()->willReturn(12); @@ -118,7 +109,7 @@ function it_should_write_file_to_source_and_cache($source, $cache) * @param \Gaufrette\Adapter $source * @param \Gaufrette\Adapter $cache */ - function it_should_delete_file_from_source_and_cache($source, $cache) + function it_deletes_file_from_source_and_cache($source, $cache) { $source->delete('filename')->shouldBeCalled()->willReturn(true); $cache->delete('filename')->shouldBeCalled()->willReturn(true); @@ -129,7 +120,7 @@ function it_should_delete_file_from_source_and_cache($source, $cache) /** * @param \Gaufrette\Adapter $source */ - function it_should_check_if_exists_in_source($source) + function it_check_if_exists_in_source($source) { $source->exists('filename')->shouldBeCalled()->willReturn(true); @@ -139,7 +130,7 @@ function it_should_check_if_exists_in_source($source) /** * @param \Gaufrette\Adapter $source */ - function it_should_get_mtime_from_source($source) + function it_get_mtime_from_source($source) { $source->mtime('filename')->shouldBeCalled()->willReturn(1234); @@ -149,7 +140,7 @@ function it_should_get_mtime_from_source($source) /** * @param \Gaufrette\Adapter $source */ - function it_should_get_keys_from_source($source) + function it_get_keys_from_source($source) { $source->keys()->willReturn(array('filename2', 'filename1', 'filename')); diff --git a/spec/Gaufrette/Adapter/DoctrineDbal.php b/spec/Gaufrette/Adapter/DoctrineDbalSpec.php similarity index 71% rename from spec/Gaufrette/Adapter/DoctrineDbal.php rename to spec/Gaufrette/Adapter/DoctrineDbalSpec.php index 66247e41e..81f7fe221 100644 --- a/spec/Gaufrette/Adapter/DoctrineDbal.php +++ b/spec/Gaufrette/Adapter/DoctrineDbalSpec.php @@ -5,9 +5,10 @@ //hack - mock php built-in functions require_once 'functions.php'; -use PHPSpec2\ObjectBehavior; +use PhpSpec\ObjectBehavior; +use Prophecy\Argument; -class DoctrineDbal extends ObjectBehavior +class DoctrineDbalSpec extends ObjectBehavior { /** * @param \Doctrine\DBAL\Connection $connection @@ -17,14 +18,17 @@ function let($connection) $this->beConstructedWith($connection, 'someTableName'); } - function it_should_be_initializable() + function it_is_adapter() { - $this->shouldHaveType('Gaufrette\Adapter\DoctrineDbal'); $this->shouldHaveType('Gaufrette\Adapter'); + } + + function it_is_checksum_calculator() + { $this->shouldHaveType('Gaufrette\Adapter\ChecksumCalculator'); } - function it_should_not_handle_directories() + function it_does_not_handle_directories() { $this->isDirectory('filename')->shouldReturn(false); } @@ -32,11 +36,11 @@ function it_should_not_handle_directories() /** * @param \Doctrine\DBAL\Connection $connection */ - function it_should_check_if_file_exists($connection) + function it_checks_if_file_exists($connection) { $connection - ->quoteIdentifier(ANY_ARGUMENT) - ->willReturnUsing(function ($argument) { return sprintf('"%s"', $argument); }); + ->quoteIdentifier(Argument::any()) + ->will(function ($argument) { return sprintf('"%s"', $argument[0]); }); $connection ->fetchColumn('SELECT COUNT("key") FROM "someTableName" WHERE "key" = :key', array('key' => 'filename')) @@ -52,11 +56,11 @@ function it_should_check_if_file_exists($connection) /** * @param \Doctrine\DBAL\Connection $connection */ - function it_should_write_file($connection) + function it_writes_to_new_file($connection) { $connection - ->quoteIdentifier(ANY_ARGUMENT) - ->willReturnUsing(function ($argument) { return sprintf('"%s"', $argument); }); + ->quoteIdentifier(Argument::any()) + ->will(function ($argument) { return sprintf('"%s"', $argument[0]); }); $connection ->fetchColumn('SELECT COUNT("key") FROM "someTableName" WHERE "key" = :key', array('key' => 'filename')) ->willReturn(false); @@ -77,11 +81,11 @@ function it_should_write_file($connection) /** * @param \Doctrine\DBAL\Connection $connection */ - function it_should_update_file($connection) + function it_write_file($connection) { $connection - ->quoteIdentifier(ANY_ARGUMENT) - ->willReturnUsing(function ($argument) { return sprintf('"%s"', $argument); }); + ->quoteIdentifier(Argument::any()) + ->will(function ($argument) { return sprintf('"%s"', $argument[0]); }); $connection ->fetchColumn('SELECT COUNT("key") FROM "someTableName" WHERE "key" = :key', array('key' => 'filename')) ->willReturn(true); @@ -104,11 +108,11 @@ function it_should_update_file($connection) /** * @param \Doctrine\DBAL\Connection $connection */ - function it_should_read_file($connection) + function it_reads_file($connection) { $connection - ->quoteIdentifier(ANY_ARGUMENT) - ->willReturnUsing(function ($argument) { return sprintf('"%s"', $argument); }); + ->quoteIdentifier(Argument::any()) + ->will(function ($argument) { return sprintf('"%s"', $argument[0]); }); $connection ->fetchColumn('SELECT "content" FROM "someTableName" WHERE "key" = :key', array('key' => 'filename')) ->willReturn('some content'); @@ -119,11 +123,11 @@ function it_should_read_file($connection) /** * @param \Doctrine\DBAL\Connection $connection */ - function it_should_calculate_checksum($connection) + function it_calculates_checksum($connection) { $connection - ->quoteIdentifier(ANY_ARGUMENT) - ->willReturnUsing(function ($argument) { return sprintf('"%s"', $argument); }); + ->quoteIdentifier(Argument::any()) + ->will(function ($argument) { return sprintf('"%s"', $argument[0]); }); $connection ->fetchColumn('SELECT "checksum" FROM "someTableName" WHERE "key" = :key', array('key' => 'filename')) ->willReturn(1234); @@ -134,11 +138,11 @@ function it_should_calculate_checksum($connection) /** * @param \Doctrine\DBAL\Connection $connection */ - function it_should_get_mtime($connection) + function it_gets_mtime($connection) { $connection - ->quoteIdentifier(ANY_ARGUMENT) - ->willReturnUsing(function ($argument) { return sprintf('"%s"', $argument); }); + ->quoteIdentifier(Argument::any()) + ->will(function ($argument) { return sprintf('"%s"', $argument[0]); }); $connection ->fetchColumn('SELECT "mtime" FROM "someTableName" WHERE "key" = :key', array('key' => 'filename')) ->willReturn(1234); @@ -149,11 +153,11 @@ function it_should_get_mtime($connection) /** * @param \Doctrine\DBAL\Connection $connection */ - function it_should_rename_file($connection) + function it_renames_file($connection) { $connection - ->quoteIdentifier(ANY_ARGUMENT) - ->willReturnUsing(function ($argument) { return sprintf('"%s"', $argument); }); + ->quoteIdentifier(Argument::any()) + ->will(function ($argument) { return sprintf('"%s"', $argument[0]); }); $connection ->update( 'someTableName', @@ -173,12 +177,12 @@ function it_should_rename_file($connection) * @param \Doctrine\DBAL\Connection $connection * @param \Doctrine\DBAL\Statement $stmt */ - function it_should_get_keys($connection, $stmt) + function it_get_keys($connection, $stmt) { $stmt->fetchAll(\PDO::FETCH_COLUMN)->willReturn(array('filename', 'filename1', 'filename2')); $connection - ->quoteIdentifier(ANY_ARGUMENT) - ->willReturnUsing(function ($argument) { return sprintf('"%s"', $argument); }); + ->quoteIdentifier(Argument::any()) + ->will(function ($argument) { return sprintf('"%s"', $argument[0]); }); $connection ->executeQuery('SELECT "key" FROM "someTableName"') ->willReturn($stmt); @@ -189,11 +193,11 @@ function it_should_get_keys($connection, $stmt) /** * @param \Doctrine\DBAL\Connection $connection */ - function it_should_delete_file($connection) + function it_deletes_file($connection) { $connection - ->quoteIdentifier(ANY_ARGUMENT) - ->willReturnUsing(function ($argument) { return sprintf('"%s"', $argument); }); + ->quoteIdentifier(Argument::any()) + ->will(function ($argument) { return sprintf('"%s"', $argument[0]); }); $connection ->delete('someTableName', array('"key"' => 'filename')) ->shouldBeCalled() diff --git a/spec/Gaufrette/Adapter/Dropbox.php b/spec/Gaufrette/Adapter/DropboxSpec.php similarity index 74% rename from spec/Gaufrette/Adapter/Dropbox.php rename to spec/Gaufrette/Adapter/DropboxSpec.php index e4ac1b7a4..7aa78571c 100644 --- a/spec/Gaufrette/Adapter/Dropbox.php +++ b/spec/Gaufrette/Adapter/DropboxSpec.php @@ -2,9 +2,10 @@ namespace spec\Gaufrette\Adapter; -use PHPSpec2\ObjectBehavior; +use PhpSpec\ObjectBehavior; +use Prophecy\Argument; -class Dropbox extends ObjectBehavior +class DropboxSpec extends ObjectBehavior { /** * @param \Dropbox_API $dropbox @@ -14,15 +15,15 @@ function let($dropbox) $this->beConstructedWith($dropbox); } - function it_should_be_initializable() + function it_is_adapter() { - $this->shouldHaveType('Gaufrette\Adapter\Dropbox'); + $this->shouldHaveType('Gaufrette\Adapter'); } /** * @param \Dropbox_API $dropbox */ - function it_should_read_file($dropbox) + function it_reads_file($dropbox) { $dropbox->getFile('filename')->willReturn('some content'); @@ -32,7 +33,7 @@ function it_should_read_file($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_should_not_mask_exception_from_client_during_read($dropbox) + function it_does_not_mask_exception_from_client_during_read($dropbox) { $dropbox->getFile('filename')->willThrow(new \RuntimeException('read')); @@ -42,7 +43,7 @@ function it_should_not_mask_exception_from_client_during_read($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_should_not_read_file($dropbox) + function it_does_not_read_file($dropbox) { $dropbox ->getFile('filename') @@ -54,7 +55,7 @@ function it_should_not_read_file($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_should_check_if_file_exists($dropbox) + function it_checks_if_file_exists($dropbox) { $dropbox ->getMetaData('filename', false) @@ -91,7 +92,7 @@ function it_should_check_if_file_exists($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_should_not_mask_exception_from_client_during_check_if_exists($dropbox) + function it_does_not_mask_exception_from_client_during_check_if_exists($dropbox) { $dropbox ->getMetaData('filename', false) @@ -103,7 +104,7 @@ function it_should_not_mask_exception_from_client_during_check_if_exists($dropbo /** * @param \Dropbox_API $dropbox */ - function it_should_get_keys($dropbox) + function it_gets_keys($dropbox) { $dropbox ->getMetaData('/', true) @@ -120,11 +121,12 @@ function it_should_get_keys($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_should_not_mask_exception_from_client_during_getting_keys($dropbox) + function it_does_not_mask_exception_from_client_during_getting_keys($dropbox) { $dropbox ->getMetaData('/', true) - ->willThrow(new \RuntimeException('keys')); + ->willThrow(new \RuntimeException('keys')) + ; $this->shouldThrow(new \RuntimeException('keys'))->duringKeys(); } @@ -132,13 +134,14 @@ function it_should_not_mask_exception_from_client_during_getting_keys($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_should_check_is_directory($dropbox) + function it_checks_if_given_key_is_directory($dropbox) { $dropbox ->getMetaData('filename', false) ->willReturn(array( "is_dir" => true - )); + )) + ; $this->isDirectory('filename')->shouldReturn(true); @@ -154,11 +157,12 @@ function it_should_check_is_directory($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_should_write_file($dropbox) + function it_writes_file($dropbox) { $dropbox - ->putFile('filename', ANY_ARGUMENT) - ->shouldBeCalled(); + ->putFile('filename', Argument::any()) + ->shouldBeCalled() + ; $this->write('filename', 'some content')->shouldReturn(12); } @@ -166,11 +170,12 @@ function it_should_write_file($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_should_not_mask_exception_from_client_during_write($dropbox) + function it_does_not_mask_exception_from_client_during_write($dropbox) { $dropbox - ->putFile('filename', ANY_ARGUMENT) - ->willThrow(new \RuntimeException('write')); + ->putFile('filename', Argument::any()) + ->willThrow(new \RuntimeException('write')) + ; $this->shouldThrow(new \RuntimeException('write'))->duringWrite('filename', 'some content'); } @@ -178,11 +183,12 @@ function it_should_not_mask_exception_from_client_during_write($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_should_delete_file($dropbox) + function it_deletes_file($dropbox) { $dropbox ->delete('filename') - ->shouldBeCalled(); + ->shouldBeCalled() + ; $this->delete('filename')->shouldReturn(true); } @@ -190,11 +196,12 @@ function it_should_delete_file($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_should_not_delete_file($dropbox) + function it_does_not_delete_file($dropbox) { $dropbox ->delete('filename') - ->willThrow(new \Dropbox_Exception_NotFound()); + ->willThrow(new \Dropbox_Exception_NotFound()) + ; $this->delete('filename')->shouldReturn(false); } @@ -202,11 +209,12 @@ function it_should_not_delete_file($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_should_rename_file($dropbox) + function it_renames_file($dropbox) { $dropbox ->move('filename', 'filename2') - ->shouldBeCalled(); + ->shouldBeCalled() + ; $this->rename('filename', 'filename2')->shouldReturn(true); } @@ -214,11 +222,12 @@ function it_should_rename_file($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_should_not_rename_file($dropbox) + function it_does_not_rename_file($dropbox) { $dropbox ->move('filename', 'filename2') - ->willThrow(new \Dropbox_Exception_NotFound()); + ->willThrow(new \Dropbox_Exception_NotFound()) + ; $this->rename('filename', 'filename2')->shouldReturn(false); } @@ -226,13 +235,14 @@ function it_should_not_rename_file($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_should_get_mtime($dropbox) + function it_fetches_mtime($dropbox) { $dropbox ->getMetaData('filename', false) ->willReturn(array( "modified" => "Tue, 19 Jul 2011 21:55:38 +0000", - )); + )) + ; $this->mtime('filename')->shouldReturn(1311112538); } @@ -240,11 +250,12 @@ function it_should_get_mtime($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_should_not_fetch_mtime_when_file_not_found($dropbox) + function it_does_not_fetch_mtime_when_file_not_found($dropbox) { $dropbox ->getMetaData('filename', false) - ->willThrow(new \Dropbox_Exception_NotFound()); + ->willThrow(new \Dropbox_Exception_NotFound()) + ; $this->mtime('filename')->shouldReturn(false); } @@ -252,11 +263,12 @@ function it_should_not_fetch_mtime_when_file_not_found($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_should_not_check_if_key_is_dir_when_file_not_found($dropbox) + function it_does_not_check_if_key_is_dir_when_file_not_found($dropbox) { $dropbox ->getMetaData('filename', false) - ->willThrow(new \Dropbox_Exception_NotFound()); + ->willThrow(new \Dropbox_Exception_NotFound()) + ; $this->isDirectory('filename')->shouldReturn(false); } @@ -264,11 +276,12 @@ function it_should_not_check_if_key_is_dir_when_file_not_found($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_should_fail_checking_if_key_is_dir_when_dropbox_throws_exception($dropbox) + function it_fails_checking_if_key_is_dir_when_dropbox_throws_exception($dropbox) { $dropbox ->getMetaData('filename', false) - ->willThrow(new \RuntimeException('some exception')); + ->willThrow(new \RuntimeException('some exception')) + ; $this->shouldThrow(new \RuntimeException('some exception'))->duringIsDirectory('filename'); } diff --git a/spec/Gaufrette/Adapter/Ftp.php b/spec/Gaufrette/Adapter/FtpSpec.php similarity index 71% rename from spec/Gaufrette/Adapter/Ftp.php rename to spec/Gaufrette/Adapter/FtpSpec.php index 48c8333fd..6c3489aa9 100644 --- a/spec/Gaufrette/Adapter/Ftp.php +++ b/spec/Gaufrette/Adapter/FtpSpec.php @@ -5,28 +5,27 @@ //hack - mock php built-in functions require_once 'functions.php'; -use PHPSpec2\ObjectBehavior; +use PhpSpec\ObjectBehavior; -class Ftp extends ObjectBehavior +class FtpSpec extends ObjectBehavior { function let() { $this->beConstructedWith('/home/l3l0', 'localhost'); } - function it_should_be_initializable() + function it_is_adapter() { - $this->shouldHaveType('Gaufrette\Adapter\Ftp'); $this->shouldHaveType('Gaufrette\Adapter'); } - function it_should_check_if_file_exists_for_absolute_path() + function it_checks_if_file_exists_for_absolute_path() { $this->exists('filename')->shouldReturn(true); $this->exists('aa/filename')->shouldReturn(false); } - function it_should_check_if_file_exists_for_relative_path() + function it_checks_if_file_exists_for_relative_path() { $this->beConstructedWith('/home/l3l0/relative', 'localhost'); @@ -36,57 +35,57 @@ function it_should_check_if_file_exists_for_relative_path() $this->exists('some/otherfilename')->shouldReturn(true); } - function it_should_read_file() + function it_reads_file() { $this->read('filename')->shouldReturn('some content'); } - function it_should_not_read_file() + function it_does_not_read_file() { $this->read('filename2')->shouldReturn(false); } - function it_should_write_file() + function it_writes_file() { $this->write('filename', 'some content')->shouldReturn(12); } - function it_should_not_write_file() + function it_does_not_write_file() { $this->write('filename2', 'some content')->shouldReturn(false); } - function it_should_rename_file() + function it_renames_file() { $this->rename('filename', 'filename2')->shouldReturn(true); } - function it_should_not_rename_file() + function it_does_not_not_rename_file_when_target_file_is_invalid() { $this->rename('filename', 'invalid')->shouldReturn(false); } - function it_should_fetch_keys_without_directories_dots() + function it_fetches_keys_without_directories_dots() { $this->keys()->shouldReturn(array('filename', 'filename.exe', '.htaccess', 'aaa', 'aaa/filename')); } - function it_should_get_mtime() + function it_fetches_mtime() { $this->mtime('filename')->shouldReturn(strtotime('2010-10-10 23:10:10')); } - function it_should_throw_excention_when_mtime_is_not_supported_by_server() + function it_throws_exception_when_mtime_is_not_supported_by_server() { $this->shouldThrow(new \RuntimeException('Server does not support ftp_mdtm function.'))->duringMtime('invalid'); } - function it_should_delete_file() + function it_deletes_file() { $this->delete('filename')->shouldReturn(true); } - function it_should_not_delete_file() + function it_does_not_delete_file() { $this->delete('invalid')->shouldReturn(false); } @@ -94,7 +93,7 @@ function it_should_not_delete_file() /** * @param \Gaufrette\Filesystem $filesystem */ - function it_should_create_file($filesystem) + function it_creates_file($filesystem) { $this->createFile('filename', $filesystem)->shouldReturnAnInstanceOf('\Gaufrette\File'); } @@ -102,32 +101,32 @@ function it_should_create_file($filesystem) /** * @param \Gaufrette\Filesystem $filesystem */ - function it_should_create_file_in_not_existing_directory($filesystem) + function it_creates_file_in_not_existing_directory($filesystem) { $this->createFile('bb/cc/dd/filename', $filesystem)->shouldReturnAnInstanceOf('\Gaufrette\File'); } - function it_should_check_is_directory() + function it_checks_if_given_key_is_directory() { $this->isDirectory('aaa')->shouldReturn(true); $this->isDirectory('filename')->shouldReturn(false); } - function it_should_fetch_keys_with_hidden_files() + function it_fetches_keys_with_hidden_files() { $this->beConstructedWith('/home/l3l1', 'localhost'); $this->keys()->shouldReturn(array('filename', '.htaccess')); } - function it_should_check_if_hidden_file_exists() + function it_checks_if_hidden_file_exists() { $this->beConstructedWith('/home/l3l1', 'localhost'); $this->exists('.htaccess')->shouldReturn(true); } - function it_should_create_base_directory_without_warning() + function it_creates_base_directory_without_warning() { global $createdDirectory; $createdDirectory = ''; @@ -137,7 +136,7 @@ function it_should_create_base_directory_without_warning() $this->listDirectory()->shouldReturn(array('keys' => array(), 'dirs' => array())); } - function it_should_not_create_base_directory_and_should_throw_exception() + function it_does_not_create_base_directory_and_should_throw_exception() { global $createdDirectory; $createdDirectory = ''; @@ -147,7 +146,7 @@ function it_should_not_create_base_directory_and_should_throw_exception() $this->shouldThrow(new \RuntimeException("The directory '/home/l3l0/new' does not exist."))->during('listDirectory', array()); } - function it_should_fetch_keys_for_windows() + function it_fetches_keys_for_windows() { $this->beConstructedWith('C:\Ftp', 'localhost'); diff --git a/spec/Gaufrette/Adapter/GridFS.php b/spec/Gaufrette/Adapter/GridFSSpec.php similarity index 65% rename from spec/Gaufrette/Adapter/GridFS.php rename to spec/Gaufrette/Adapter/GridFSSpec.php index 926b6e712..9c2c20893 100644 --- a/spec/Gaufrette/Adapter/GridFS.php +++ b/spec/Gaufrette/Adapter/GridFSSpec.php @@ -2,12 +2,10 @@ namespace spec\Gaufrette\Adapter; -use PHPSpec2\ObjectBehavior; +use PhpSpec\ObjectBehavior; +use Prophecy\Argument; -//Hack cause of new version of mongo-ext https://github.com/padraic/mockery/issues/110 -\Mockery::getConfiguration()->setInternalClassMethodParamMap("MongoCollection", "aggregate", array('$pipeline', '$op = null', '$some = null')); - -class GridFS extends ObjectBehavior +class GridFSSpec extends ObjectBehavior { /** * @param \MongoGridFS $gridFs @@ -17,25 +15,41 @@ function let($gridFs) $this->beConstructedWith($gridFs); } - function it_should_be_initializable() + function it_is_adapter() { - $this->shouldHaveType('Gaufrette\Adapter\GridFS'); $this->shouldHaveType('Gaufrette\Adapter'); } + function it_is_checksum_calculator() + { + $this->shouldHaveType('Gaufrette\Adapter\ChecksumCalculator'); + } + + function it_supports_metadata() + { + $this->shouldHaveType('Gaufrette\Adapter\MetadataSupporter'); + } + + function it_supports_native_list_keys() + { + $this->shouldHaveType('Gaufrette\Adapter\ListKeysAware'); + } + /** * @param \MongoGridFS $gridFs * @param \MongoGridFSFile $file */ - function it_should_read_file($gridFs, $file) + function it_reads_file($gridFs, $file) { $file ->getBytes() - ->willReturn('some content'); + ->willReturn('some content') + ; $gridFs ->findOne('filename', array()) ->shouldBeCalled() - ->willReturn($file); + ->willReturn($file) + ; $this->read('filename')->shouldReturn('some content'); } @@ -43,12 +57,13 @@ function it_should_read_file($gridFs, $file) /** * @param \MongoGridFS $gridFs */ - function it_should_not_fail_when_cannot_read($gridFs) + function it_does_not_fail_when_cannot_read($gridFs) { $gridFs ->findOne('filename', array()) ->shouldBeCalled() - ->willReturn(null); + ->willReturn(null) + ; $this->read('filename')->shouldReturn(false); } @@ -57,14 +72,16 @@ function it_should_not_fail_when_cannot_read($gridFs) * @param \MongoGridFS $gridFs * @param \MongoGridFSFile $file */ - function it_should_check_if_exists($gridFs, $file) + function it_checks_if_file_exists($gridFs, $file) { $gridFs ->findOne('filename', array()) - ->willReturn($file); + ->willReturn($file) + ; $gridFs ->findOne('filename2', array()) - ->willReturn(null); + ->willReturn(null) + ; $this->exists('filename')->shouldReturn(true); $this->exists('filename2')->shouldReturn(false); @@ -73,16 +90,18 @@ function it_should_check_if_exists($gridFs, $file) /** * @param \MongoGridFS $gridFs */ - function it_should_delete_file($gridFs) + function it_deletes_file($gridFs) { $file = new \stdClass; $file->file = array('_id' => 123); $gridFs ->findOne('filename', array('_id')) - ->willReturn($file); + ->willReturn($file) + ; $gridFs ->delete(123) - ->willReturn(true); + ->willReturn(true) + ; $this->delete('filename')->shouldReturn(true); } @@ -90,22 +109,24 @@ function it_should_delete_file($gridFs) /** * @param \MongoGridFS $gridFs */ - function it_should_not_delete_file($gridFs) + function it_does_not_delete_file($gridFs) { $file = new \stdClass; $file->file = array('_id' => 123); $gridFs ->findOne('filename', array('_id')) - ->willReturn($file); - + ->willReturn($file) + ; $gridFs ->delete(123) - ->willReturn(false); + ->willReturn(false) + ; $this->delete('filename')->shouldReturn(false); $gridFs ->findOne('filename', array('_id')) - ->willReturn(null); + ->willReturn(null) + ; $this->delete('filename')->shouldReturn(false); } @@ -113,98 +134,118 @@ function it_should_not_delete_file($gridFs) * @param \MongoGridFS $gridFs * @param \MongoGridFSFile $file */ - function it_should_write_file($gridFs, $file) + function it_writes_file($gridFs, $file) { $file ->getSize() - ->willReturn(12); + ->willReturn(12) + ; $gridFs ->findOne('filename', array()) - ->willReturn(null); + ->willReturn(null) + ; $gridFs ->storeBytes('some content', array('date' => 1234, 'someother' => 'metadata', 'filename' => 'filename')) - ->willReturn('someId'); + ->willReturn('someId') + ; $gridFs ->findOne(array('_id' => 'someId')) - ->willReturn($file); + ->willReturn($file) + ; $this->setMetadata('filename', array('date' => 1234, 'someother' => 'metadata')); $this ->write('filename', 'some content') - ->shouldReturn(12); + ->shouldReturn(12) + ; } /** * @param \MongoGridFS $gridFs * @param \MongoGridFSFile $file */ - function it_should_update_file($gridFs, $file) + function it_updates_file($gridFs, $file) { $someFile = new \stdClass; $someFile->file = array('_id' => 123); $gridFs ->findOne('filename', array('_id')) ->shouldBeCalled() - ->willReturn($someFile); + ->willReturn($someFile) + ; $gridFs ->delete(123) ->shouldBeCalled() - ->willReturn(true); + ->willReturn(true) + ; $file ->getSize() - ->willReturn(12); + ->willReturn(12) + ; $gridFs ->findOne('filename', array()) - ->willReturn($file); + ->willReturn($file) + ; $gridFs - ->storeBytes(ANY_ARGUMENT, ANY_ARGUMENT) - ->willReturn('someId'); + ->storeBytes(Argument::any(), Argument::any()) + ->willReturn('someId') + ; $gridFs ->findOne(array('_id' => 'someId')) - ->willReturn($file); + ->willReturn($file) + ; $this ->write('filename', 'some content') - ->shouldReturn(12); + ->shouldReturn(12) + ; } /** * @param \MongoGridFS $gridFs * @param \MongoGridFSFile $file */ - function it_should_rename_file($gridFs, $file) + function it_renames_file($gridFs, $file) { $file ->getBytes() - ->willReturn('some content'); + ->willReturn('some content') + ; $file ->getSize() - ->willReturn(12); + ->willReturn(12) + ; $gridFs ->findOne('otherFilename', array()) - ->willReturn(null); + ->willReturn(null) + ; $gridFs ->findOne('filename', array()) ->shouldBeCalled() - ->willReturn($file); + ->willReturn($file) + ; $gridFs ->storeBytes('some content', array('date' => 1234, 'filename' => 'otherFilename')) ->shouldBeCalled() - ->willReturn('someId'); + ->willReturn('someId') + ; $fileToDelete = new \stdClass; $fileToDelete->file = array('_id' => 123); $gridFs ->findOne('filename', array('_id')) - ->willReturn($fileToDelete); + ->willReturn($fileToDelete) + ; $gridFs ->findOne(array('_id' => 'someId')) - ->willReturn($file); + ->willReturn($file) + ; $gridFs ->delete(123) ->shouldBeCalled() - ->willReturn(true); + ->willReturn(true) + ; $this->setMetadata('otherFilename', array('date' => 1234)); $this->rename('filename', 'otherFilename')->shouldReturn(true); @@ -213,11 +254,12 @@ function it_should_rename_file($gridFs, $file) /** * @param \MongoGridFS $gridFs */ - function it_should_get_keys($gridFs, $file, $otherFile) + function it_fetches_keys($gridFs, $file, $otherFile) { $gridFs ->find(array(), array('filename')) - ->willReturn(array(new File('filename'), new File('otherFilename'))); + ->willReturn(array(new TestFile('filename'), new TestFile('otherFilename'))) + ; $this->keys()->shouldReturn(array('filename', 'otherFilename')); } @@ -225,7 +267,7 @@ function it_should_get_keys($gridFs, $file, $otherFile) /** * @param \MongoGridFS $gridFs */ - function it_should_get_mtime($gridFs) + function it_fetches_mtime($gridFs) { $time = new \stdClass; $time->sec = 12345; @@ -234,7 +276,8 @@ function it_should_get_mtime($gridFs) $someFile->file = array('date' => $time); $gridFs ->findOne('filename', array('date')) - ->willReturn($someFile); + ->willReturn($someFile) + ; $this->mtime('filename')->shouldReturn(12345); } @@ -242,19 +285,20 @@ function it_should_get_mtime($gridFs) /** * @param \MongoGridFS $gridFs */ - function it_should_calculate_checksum($gridFs) + function it_calculates_checksum($gridFs) { $someFile = new \stdClass; $someFile->file = array('md5' => 'md5123'); $gridFs ->findOne('filename', array('md5')) - ->willReturn($someFile); + ->willReturn($someFile) + ; $this->checksum('filename')->shouldReturn('md5123'); } } -class File +class TestFile { private $filename; diff --git a/spec/Gaufrette/Adapter/InMemory.php b/spec/Gaufrette/Adapter/InMemorySpec.php similarity index 71% rename from spec/Gaufrette/Adapter/InMemory.php rename to spec/Gaufrette/Adapter/InMemorySpec.php index d2c67e810..fea6610df 100644 --- a/spec/Gaufrette/Adapter/InMemory.php +++ b/spec/Gaufrette/Adapter/InMemorySpec.php @@ -2,9 +2,9 @@ namespace spec\Gaufrette\Adapter; -use PHPSpec2\ObjectBehavior; +use PhpSpec\ObjectBehavior; -class InMemory extends ObjectBehavior +class InMemorySpec extends ObjectBehavior { function let() { @@ -14,51 +14,51 @@ function let() )); } - function it_should_be_initializable() + function it_is_adapter() { - $this->shouldHaveType('Gaufrette\Adapter\InMemory'); + $this->shouldHaveType('Gaufrette\Adapter'); } - function it_should_read_file() + function it_reads_file() { $this->read('filename')->shouldReturn('content'); } - function it_should_write_file() + function it_writes_file() { $this->write('filename', 'some content')->shouldReturn(12); } - function it_should_rename_file() + function it_renames_file() { $this->rename('filename', 'aaa/filename2')->shouldReturn(true); $this->exists('filename')->shouldReturn(false); $this->exists('aaa/filename2')->shouldReturn(true); } - function it_should_check_if_file_exists() + function it_checks_if_file_exists() { $this->exists('filename')->shouldReturn(true); $this->exists('filenameTest')->shouldReturn(false); } - function it_should_get_keys() + function it_fetches_keys() { $this->keys()->shouldReturn(array('filename', 'filename2')); } - function it_should_get_mtime() + function it_fetches_mtime() { $this->mtime('filename')->shouldReturn(12345); } - function it_should_delete_file() + function it_deletes_file() { $this->delete('filename')->shouldReturn(true); $this->exists('filename')->shouldReturn(false); } - function it_should_not_handle_dirs() + function it_does_not_handle_dirs() { $this->isDirectory('filename')->shouldReturn(false); $this->isDirectory('filename2')->shouldReturn(false); diff --git a/spec/Gaufrette/Adapter/LazyRackspaceCloudfiles.php b/spec/Gaufrette/Adapter/LazyRackspaceCloudfilesSpec.php similarity index 73% rename from spec/Gaufrette/Adapter/LazyRackspaceCloudfiles.php rename to spec/Gaufrette/Adapter/LazyRackspaceCloudfilesSpec.php index 87ce815de..0d0451641 100644 --- a/spec/Gaufrette/Adapter/LazyRackspaceCloudfiles.php +++ b/spec/Gaufrette/Adapter/LazyRackspaceCloudfilesSpec.php @@ -2,9 +2,10 @@ namespace spec\Gaufrette\Adapter; -use PHPSpec2\ObjectBehavior; +use PhpSpec\ObjectBehavior; +use Prophecy\Argument; -class LazyRackspaceCloudfiles extends ObjectBehavior +class LazyRackspaceCloudfilesSpec extends ObjectBehavior { /** * @param \Gaufrette\Adapter\RackspaceCloudfiles\ConnectionFactoryInterface $connectionFactory @@ -16,9 +17,8 @@ function let($connectionFactory, $connection) $this->beConstructedWith($connectionFactory, 'my_container'); } - function it_should_be_initializable() + function it_is_rackspace_adapter() { - $this->shouldHaveType('\Gaufrette\Adapter\LazyRackspaceCloudfiles'); $this->shouldHaveType('\Gaufrette\Adapter\RackspaceCloudfiles'); } @@ -26,18 +26,17 @@ function it_should_be_initializable() * @param \CF_Container $container * @param \CF_Object $object */ - function it_should_lazily_fetch_container_before_read($connection, $container, $object) + function it_lazily_fetches_container_before_read($connection, $container, $object) { - $connection->get_container('my_container')->willReturn($container)->shouldBeCalled(); - $connection->create_container(ANY_ARGUMENTS)->shouldNotBeCalled(); + $connection->get_container('my_container')->willReturn($container)->shouldBeCalledTimes(1); + $connection->create_container(Argument::cetera())->shouldNotBeCalled(); $object->read()->willReturn('some content'); $container->get_object('filename')->willReturn($object); $container->get_object('filename1')->willReturn($object); $this->read('filename')->shouldReturn('some content'); - //should not be called second time - $connection->get_container('my_container')->willReturn($container)->shouldNotBeCalled(); + //get_container() should not be called second time $this->read('filename1')->shouldReturn('some content'); } @@ -45,11 +44,11 @@ function it_should_lazily_fetch_container_before_read($connection, $container, $ * @param \CF_Container $container * @param \CF_Object $object */ - function it_should_lazily_create_container_before_read($connectionFactory, $connection, $container, $object) + function it_lazily_creates_container_before_read($connectionFactory, $connection, $container, $object) { $this->beConstructedWith($connectionFactory, 'my_container', true); - $connection->get_container(ANY_ARGUMENT)->shouldNotBeCalled(); + $connection->get_container(Argument::any())->shouldNotBeCalled(); $connection->create_container('my_container')->willReturn($container)->shouldBeCalled(); $object->read()->willReturn('some content'); $container->get_object('filename')->willReturn($object); @@ -61,7 +60,7 @@ function it_should_lazily_create_container_before_read($connectionFactory, $conn * @param \CF_Container $container * @param \CF_Object $object */ - function it_should_lazily_fetch_container_before_write($connection, $container, $object) + function it_lazily_fetches_container_before_write($connection, $container, $object) { $connection->get_container('my_container')->willReturn($container)->shouldBeCalled(); $object @@ -82,7 +81,7 @@ function it_should_lazily_fetch_container_before_write($connection, $container, * @param \CF_Container $container * @param \CF_Object $object */ - function it_should_lazily_fetch_container_before_exists($connection, $container, $object) + function it_lazily_fetches_container_before_exists($connection, $container, $object) { $connection->get_container('my_container')->willReturn($container)->shouldBeCalled(); $container @@ -96,7 +95,7 @@ function it_should_lazily_fetch_container_before_exists($connection, $container, /** * @param \CF_Container $container */ - function it_should_lazily_fetch_container_before_keys($connection, $container) + function it_lazily_fetches_container_before_keys($connection, $container) { $connection->get_container('my_container')->willReturn($container)->shouldBeCalled(); $container->list_objects(0, null, null)->willReturn(array('filename2', 'filename1')); @@ -108,7 +107,7 @@ function it_should_lazily_fetch_container_before_keys($connection, $container) * @param \CF_Container $container * @param \CF_Object $object */ - function it_should_lazily_fetch_container_before_checksum($connection, $container, $object) + function it_lazily_fetches_container_before_checksum($connection, $container, $object) { $connection->get_container('my_container')->willReturn($container)->shouldBeCalled(); @@ -121,7 +120,7 @@ function it_should_lazily_fetch_container_before_checksum($connection, $containe /** * @param \CF_Container $container */ - function it_should_lazily_fetch_container_before_delete($connection, $container) + function it_lazily_fetches_container_before_delete($connection, $container) { $connection->get_container('my_container')->willReturn($container)->shouldBeCalled(); $container->delete_object('filename')->shouldBeCalled(); @@ -129,4 +128,3 @@ function it_should_lazily_fetch_container_before_delete($connection, $container) $this->delete('filename')->shouldReturn(true); } } - diff --git a/spec/Gaufrette/Adapter/Local.php b/spec/Gaufrette/Adapter/LocalSpec.php similarity index 68% rename from spec/Gaufrette/Adapter/Local.php rename to spec/Gaufrette/Adapter/LocalSpec.php index 86779ffae..6390d4e65 100644 --- a/spec/Gaufrette/Adapter/Local.php +++ b/spec/Gaufrette/Adapter/LocalSpec.php @@ -5,9 +5,9 @@ //hack - mock php built-in functions require_once 'functions.php'; -use PHPSpec2\ObjectBehavior; +use PhpSpec\ObjectBehavior; -class Local extends ObjectBehavior +class LocalSpec extends ObjectBehavior { function let() { @@ -20,33 +20,43 @@ function letgo() $iteratorToArray = array(); } - function it_should_be_initializable() + function it_is_adapter() { - $this->shouldHaveType('Gaufrette\Adapter\Local'); + $this->shouldHaveType('Gaufrette\Adapter'); } - function it_should_read_file() + function it_is_checksum_calculator() + { + $this->shouldHaveType('Gaufrette\Adapter\ChecksumCalculator'); + } + + function it_is_stream_factory() + { + $this->shouldHaveType('Gaufrette\Adapter\StreamFactory'); + } + + function it_reads_file() { $this->read('filename')->shouldReturn('/home/somedir/filename content'); } - function it_should_write_file() + function it_writes_file() { $this->write('filename', 'some content')->shouldReturn(12); } - function it_should_rename_file() + function it_renames_file() { $this->rename('filename', 'aaa/filename2')->shouldReturn('/home/somedir/filename to /home/somedir/aaa/filename2'); } - function it_should_check_if_file_exists() + function it_checks_if_file_exists() { $this->exists('filename')->shouldReturn(true); $this->exists('filename1')->shouldReturn(false); } - function it_should_get_keys() + function it_fetches_keys() { global $iteratorToArray; $iteratorToArray = array('/home/somedir/filename', '/home/somedir/filename1', '/home/somedir/aaa/filename'); @@ -54,90 +64,97 @@ function it_should_get_keys() $this->keys()->shouldReturn(array('aaa', 'aaa/filename', 'filename', 'filename1')); } - function it_should_get_mtime() + function it_fetches_mtime() { $this->mtime('filename')->shouldReturn(12345); } - function it_should_delete_file() + function it_deletes_file() { $this->delete('filename')->shouldReturn(true); $this->delete('filename1')->shouldReturn(false); } - function it_should_check_if_key_is_dir() + function it_checks_if_given_key_is_directory() { $this->isDirectory('dir')->shouldReturn(true); $this->isDirectory('filename')->shouldReturn(false); } - function it_should_create_local_stream() + function it_creates_local_stream() { $this->createStream('filename')->shouldReturnAnInstanceOf('Gaufrette\Stream\Local'); } - function it_should_accepts_symbolic_links() + function it_allows_to_work_with_symbolic_links() { $this->beConstructedWith('symbolicLink'); $this->read('filename')->shouldReturn('/home/somedir/filename content'); } - function it_should_not_allow_to_read_path_above_main_file_directory() + function it_does_not_allow_to_read_path_above_main_file_directory() { $this ->shouldThrow(new \OutOfBoundsException('The path "/home/filename" is out of the filesystem.')) - ->duringRead('../filename'); + ->duringRead('../filename') + ; $this ->shouldThrow(new \OutOfBoundsException('The path "/home/filename" is out of the filesystem.')) - ->duringExists('../filename'); + ->duringExists('../filename') + ; } - function it_should_fail_when_directory_does_not_exists() + function it_fails_when_directory_does_not_exists() { $this->beConstructedWith('/home/other'); $this ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) - ->duringRead('filename'); + ->duringRead('filename') + ; $this ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) - ->duringWrite('filename', 'some content'); + ->duringWrite('filename', 'some content') + ; $this ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) - ->duringRename('filename', 'otherFilename'); + ->duringRename('filename', 'otherFilename') + ; $this ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) - ->duringExists('filename'); + ->duringExists('filename') + ; $this ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) - ->duringKeys(); + ->duringKeys() + ; $this ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) - ->duringMtime('filename'); + ->duringMtime('filename') + ; $this ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) - ->duringDelete('filename'); + ->duringDelete('filename') + ; $this ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) - ->duringIsDirectory('filename'); + ->duringIsDirectory('filename') + ; $this ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) - ->duringCreateStream('filename'); + ->duringCreateStream('filename') + ; $this ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) - ->duringChecksum('filename'); + ->duringChecksum('filename') + ; } - function it_should_create_directory_when_does_not_exists() + function it_creates_directory_when_does_not_exists() { $this->beConstructedWith('/home/other', true); $this->read('filename')->shouldReturn('/home/other/filename content'); } - - function it_should_be_able_to_calculate_checksum() - { - $this->shouldHaveType('Gaufrette\Adapter\ChecksumCalculator'); - } } diff --git a/spec/Gaufrette/Adapter/MogileFS.php b/spec/Gaufrette/Adapter/MogileFSSpec.php similarity index 61% rename from spec/Gaufrette/Adapter/MogileFS.php rename to spec/Gaufrette/Adapter/MogileFSSpec.php index 641a80966..09f8ce967 100644 --- a/spec/Gaufrette/Adapter/MogileFS.php +++ b/spec/Gaufrette/Adapter/MogileFSSpec.php @@ -2,22 +2,21 @@ namespace spec\Gaufrette\Adapter; -use PHPSpec2\ObjectBehavior; +use PhpSpec\ObjectBehavior; -class MogileFS extends ObjectBehavior +class MogileFSSpec extends ObjectBehavior { function let() { $this->beConstructedWith('http://domain.com', array('localhost')); } - function it_should_be_initializable() + function it_is_adapter() { - $this->shouldHaveType('Gaufrette\Adapter\MogileFS'); $this->shouldHaveType('Gaufrette\Adapter'); } - function it_should_not_handle_mtime() + function it_does_not_handle_mtime() { $this->mtime('filename')->shouldReturn(false); $this->mtime('filename2')->shouldReturn(false); diff --git a/spec/Gaufrette/Adapter/PhpseclibSftp.php b/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php similarity index 80% rename from spec/Gaufrette/Adapter/PhpseclibSftp.php rename to spec/Gaufrette/Adapter/PhpseclibSftpSpec.php index ed6df7075..734133e48 100644 --- a/spec/Gaufrette/Adapter/PhpseclibSftp.php +++ b/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php @@ -10,18 +10,9 @@ define('NET_SFTP_TYPE_DIRECTORY', 2); } -use PHPSpec2\ObjectBehavior; +use PhpSpec\ObjectBehavior; -// @todo Remove after upgrade mockery (https://github.com/padraic/mockery/issues/139). -class Net_SFTP extends \Net_SFTP -{ - public function __construct($host, $port = 22, $timeout = 10) - { - parent::Net_SFTP($host, $port, $timeout); - } -} - -class PhpseclibSftp extends ObjectBehavior +class PhpseclibSftpSpec extends ObjectBehavior { /** * @param \spec\Gaufrette\Adapter\Net_SFTP $sftp @@ -31,17 +22,25 @@ function let($sftp) $this->beConstructedWith($sftp, '/home/l3l0'); } - function it_should_be_initializable() + function it_is_adapter() + { + $this->shouldHaveType('Gaufrette\Adapter'); + } + + function it_is_file_factory() { - $this->shouldHaveType('Gaufrette\Adapter\PhpseclibSftp'); $this->shouldHaveType('Gaufrette\Adapter\FileFactory'); + } + + function it_supports_native_list_keys() + { $this->shouldHaveType('Gaufrette\Adapter\ListKeysAware'); } /** * @param \spec\Gaufrette\Adapter\Net_SFTP $sftp */ - function it_should_get_keys($sftp) + function it_fetchs_keys($sftp) { $sftp ->rawlist('/home/l3l0/') @@ -62,7 +61,7 @@ function it_should_get_keys($sftp) /** * @param \spec\Gaufrette\Adapter\Net_SFTP $sftp */ - function it_should_read_file($sftp) + function it_reads_file($sftp) { $sftp->get('/home/l3l0/filename')->willReturn('some content'); @@ -72,8 +71,10 @@ function it_should_read_file($sftp) /** * @param \spec\Gaufrette\Adapter\Net_SFTP $sftp */ - function it_should_write_file($sftp) + function it_creates_and_writes_file($sftp) { + $sftp->pwd()->willReturn('/home/l3l0'); + $sftp->chdir('/home/l3l0')->willReturn(true); $sftp->put('/home/l3l0/filename', 'some content')->willReturn(true); $sftp->size('/home/l3l0/filename')->willReturn(12); @@ -83,11 +84,14 @@ function it_should_write_file($sftp) /** * @param \spec\Gaufrette\Adapter\Net_SFTP $sftp */ - function it_should_rename_file($sftp) + function it_renames_file($sftp) { + $sftp->pwd()->willReturn('/home/l3l0'); + $sftp->chdir('/home/l3l0')->willReturn(true); $sftp ->rename('/home/l3l0/filename', '/home/l3l0/filename1') - ->willReturn(true); + ->willReturn(true) + ; $this->rename('filename', 'filename1')->shouldReturn(true); } @@ -97,6 +101,8 @@ function it_should_rename_file($sftp) */ function it_should_check_if_file_exists($sftp) { + $sftp->pwd()->willReturn('/home/l3l0'); + $sftp->chdir('/home/l3l0')->willReturn(true); $sftp->stat('/home/l3l0/filename')->willReturn(array( 'name' => '/home/l3l0/filename' )); @@ -134,3 +140,10 @@ function it_should_create_file($sftp, $filesystem) $this->createFile('filename', $filesystem)->beAnInstanceOf('Gaufrette\File'); } } + +class Net_SFTP extends \Net_SFTP +{ + public function __construct() + { + } +} diff --git a/spec/Gaufrette/Adapter/RackspaceCloudfiles/AuthenticationConnectionFactory.php b/spec/Gaufrette/Adapter/RackspaceCloudfiles/AuthenticationConnectionFactorySpec.php similarity index 64% rename from spec/Gaufrette/Adapter/RackspaceCloudfiles/AuthenticationConnectionFactory.php rename to spec/Gaufrette/Adapter/RackspaceCloudfiles/AuthenticationConnectionFactorySpec.php index 893f2e6d5..a64f21bca 100644 --- a/spec/Gaufrette/Adapter/RackspaceCloudfiles/AuthenticationConnectionFactory.php +++ b/spec/Gaufrette/Adapter/RackspaceCloudfiles/AuthenticationConnectionFactorySpec.php @@ -2,9 +2,9 @@ namespace spec\Gaufrette\Adapter\RackspaceCloudfiles; -use PHPSpec2\ObjectBehavior; +use PhpSpec\ObjectBehavior; -class AuthenticationConnectionFactory extends ObjectBehavior +class AuthenticationConnectionFactorySpec extends ObjectBehavior { /** * @param \CF_Authentication $authentication @@ -14,22 +14,24 @@ function let($authentication) $this->beConstructedWith($authentication); } - function it_should_be_initializable() + function it_is_initializable() { $this->shouldHaveType('Gaufrette\Adapter\RackspaceCloudfiles\AuthenticationConnectionFactory'); $this->shouldHaveType('Gaufrette\Adapter\RackspaceCloudfiles\ConnectionFactoryInterface'); } - function it_should_create_cf_connection() + function it_creates_cf_connection($authentication) { + $authentication->authenticated()->willReturn(true); $this->create()->shouldReturnAnInstanceOf('\CF_Connection'); } - function it_should_authenticate_when_not_authenticated($authentication) + function it_authenticates_when_was_not_authenticated_before($authentication) { $authentication->authenticated()->willReturn(false); - $authentication->authenticate()->shouldBeCalled(); - $authentication->authenticated()->willReturn(true); + $authentication->authenticate()->shouldBeCalled()->will(function () { + $this->authenticated()->willReturn(true); + }); $this->create()->shouldReturnAnInstanceOf('\CF_Connection'); } diff --git a/spec/Gaufrette/Adapter/RackspaceCloudfiles.php b/spec/Gaufrette/Adapter/RackspaceCloudfilesSpec.php similarity index 75% rename from spec/Gaufrette/Adapter/RackspaceCloudfiles.php rename to spec/Gaufrette/Adapter/RackspaceCloudfilesSpec.php index a066ea83a..4702113b4 100644 --- a/spec/Gaufrette/Adapter/RackspaceCloudfiles.php +++ b/spec/Gaufrette/Adapter/RackspaceCloudfilesSpec.php @@ -2,9 +2,9 @@ namespace spec\Gaufrette\Adapter; -use PHPSpec2\ObjectBehavior; +use PhpSpec\ObjectBehavior; -class RackspaceCloudfiles extends ObjectBehavior +class RackspaceCloudfilesSpec extends ObjectBehavior { /** * @param \CF_Container $container @@ -14,10 +14,13 @@ function let($container) $this->beConstructedWith($container); } - function it_should_be_initializable() + function it_is_adapter() { - $this->shouldHaveType('Gaufrette\Adapter\RackspaceCloudfiles'); $this->shouldHaveType('Gaufrette\Adapter'); + } + + function it_is_checksum_calculator() + { $this->shouldHaveType('Gaufrette\Adapter\ChecksumCalculator'); } @@ -25,7 +28,7 @@ function it_should_be_initializable() * @param \CF_Container $container * @param \CF_Object $object */ - function it_should_read_file($container, $object) + function it_reads_file($container, $object) { $object->read()->willReturn('some content'); $container->get_object('filename')->willReturn($object); @@ -37,7 +40,7 @@ function it_should_read_file($container, $object) * @param \CF_Container $container * @param \CF_Object $object */ - function it_should_not_mask_exception_when_read($container, $object) + function it_does_not_mask_exception_when_read($container, $object) { $object->read()->willThrow(new \RuntimeException('read')); $container->get_object('filename')->willReturn($object); @@ -49,17 +52,20 @@ function it_should_not_mask_exception_when_read($container, $object) * @param \CF_Container $container * @param \CF_Object $object */ - function it_should_check_if_file_exists($container, $object) + function it_does_check_if_file_exists($container, $object) { $container ->get_object('filename') - ->willReturn($object); + ->willReturn($object) + ; $container ->get_object('filename2') - ->willReturn(false); + ->willReturn(false) + ; $container ->get_object('filename3') - ->willThrow(new \NoSuchObjectException); + ->willThrow(new \NoSuchObjectException) + ; $this->exists('filename')->shouldReturn(true); $this->exists('filename2')->shouldReturn(false); @@ -70,7 +76,7 @@ function it_should_check_if_file_exists($container, $object) * @param \CF_Container $container * @param \CF_Object $object */ - function it_should_not_mask_exception_when_check_if_exists($container, $object) + function it_does_not_mask_exception_when_check_if_exists($container, $object) { $container->get_object('filename')->willThrow(new \RuntimeException('exists')); @@ -86,11 +92,13 @@ function it_should_write_file($container, $object) $object ->write('some content') ->shouldBeCalled() - ->willReturn(true); + ->willReturn(true) + ; $container ->get_object('filename') ->shouldBeCalled() - ->willReturn($object); + ->willReturn($object) + ; $this->write('filename', 'some content')->shouldReturn(12); } @@ -99,16 +107,18 @@ function it_should_write_file($container, $object) * @param \CF_Container $container * @param \CF_Object $object */ - function it_should_not_write_file($container, $object) + function it_does_not_write_file($container, $object) { $object ->write('some content') ->shouldBeCalled() - ->willReturn(false); + ->willReturn(false) + ; $container ->get_object('filename') ->shouldBeCalled() - ->willReturn($object); + ->willReturn($object) + ; $this->write('filename', 'some content')->shouldReturn(false); } @@ -116,7 +126,7 @@ function it_should_not_write_file($container, $object) /** * @param \CF_Container $container */ - function it_should_not_mask_exception_when_write($container) + function it_does_not_mask_exception_when_write($container) { $container->get_object('filename')->willThrow(new \RuntimeException('write')); @@ -127,20 +137,23 @@ function it_should_not_mask_exception_when_write($container) * @param \CF_Container $container * @param \CF_Object $object */ - function it_should_create_object_when_write($container, $object) + function it_creates_object_when_write($container, $object) { $object ->write('some content') ->shouldBeCalled() - ->willReturn(true); + ->willReturn(true) + ; $container ->get_object('filename') ->shouldBeCalled() - ->willReturn(false); + ->willReturn(false) + ; $container ->create_object('filename') ->shouldBeCalled() - ->willReturn($object); + ->willReturn($object) + ; $this->write('filename', 'some content')->shouldReturn(12); } @@ -150,20 +163,28 @@ function it_should_create_object_when_write($container, $object) * @param \CF_Object $fromObject * @param \CF_Object $toObject */ - function it_should_rename_file($container, $fromObject, $toObject) + function it_renames_file($container, $fromObject, $toObject) { $fromObject ->read() - ->willReturn('some content'); + ->willReturn('some content') + ; $toObject ->write('some content') - ->willReturn(true); + ->willReturn(true) + ; + $container + ->delete_object('filename') + ->shouldBeCalled() + ; $container ->get_object('filename') - ->willReturn($fromObject); + ->willReturn($fromObject) + ; $container ->get_object('filename1') - ->willReturn($toObject); + ->willReturn($toObject) + ; $this->rename('filename', 'filename1')->shouldReturn(true); } @@ -171,7 +192,7 @@ function it_should_rename_file($container, $fromObject, $toObject) /** * @param \CF_Container $container */ - function it_should_not_mask_exception_when_rename($container) + function it_does_not_mask_exception_when_rename($container) { $container->get_object('filename')->willThrow(new \RuntimeException('rename')); @@ -181,7 +202,7 @@ function it_should_not_mask_exception_when_rename($container) /** * @param \CF_Container $container */ - function it_should_get_keys($container) + function it_fetches_keys($container) { $container->list_objects(0, null, null)->willReturn(array('filename2', 'filename1')); @@ -191,14 +212,14 @@ function it_should_get_keys($container) /** * @param \CF_Container $container */ - function it_should_not_mask_exception_when_get_keys($container) + function it_does_not_mask_exception_when_get_keys($container) { $container->list_objects(0, null, null)->willThrow(new \RuntimeException('keys')); $this->shouldThrow(new \RuntimeException('keys'))->duringKeys(); } - function it_should_not_support_mtime() + function it_does_not_support_mtime() { $this->mtime('filename')->shouldBe(false); $this->mtime('filename2')->shouldBe(false); @@ -208,7 +229,7 @@ function it_should_not_support_mtime() * @param \CF_Container $container * @param \CF_Object $object */ - function it_should_calculate_checksum($container, $object) + function it_calculates_checksum($container, $object) { $object->getETag()->willReturn('123m5'); $container->get_object('filename')->willReturn($object); @@ -219,7 +240,7 @@ function it_should_calculate_checksum($container, $object) /** * @param \CF_Container $container */ - function it_should_not_mask_exception_when_calculate_checksum($container) + function it_does_not_mask_exception_when_calculate_checksum($container) { $container->get_object('filename')->willThrow(new \RuntimeException('checksum')); @@ -229,7 +250,7 @@ function it_should_not_mask_exception_when_calculate_checksum($container) /** * @param \CF_Container $container */ - function it_should_delete_object($container) + function it_deletes_file($container) { $container->delete_object('filename')->shouldBeCalled(); @@ -239,7 +260,7 @@ function it_should_delete_object($container) /** * @param \CF_Container $container */ - function it_should_not_delete_object($container) + function it_does_not_delete_file_if_not_found($container) { $container->delete_object('filename')->willThrow(new \NoSuchObjectException); @@ -249,7 +270,7 @@ function it_should_not_delete_object($container) /** * @param \CF_Container $container */ - function it_should_not_mask_exception_when_delete($container) + function it_does_not_mask_exception_when_delete($container) { $container->delete_object('filename')->willThrow(new \RuntimeException('delete')); diff --git a/spec/Gaufrette/Adapter/SafeLocal.php b/spec/Gaufrette/Adapter/SafeLocalSpec.php similarity index 65% rename from spec/Gaufrette/Adapter/SafeLocal.php rename to spec/Gaufrette/Adapter/SafeLocalSpec.php index 3207dbe81..b964d4fd8 100644 --- a/spec/Gaufrette/Adapter/SafeLocal.php +++ b/spec/Gaufrette/Adapter/SafeLocalSpec.php @@ -5,27 +5,26 @@ //hack - mock php built-in functions require_once 'functions.php'; -use PHPSpec2\ObjectBehavior; +use PhpSpec\ObjectBehavior; -class SafeLocal extends ObjectBehavior +class SafeLocalSpec extends ObjectBehavior { function let() { $this->beConstructedWith('/home/l3l0'); } - function it_should_be_initializable() + function it_is_local_adapter() { - $this->shouldHaveType('Gaufrette\Adapter\SafeLocal'); - $this->shouldHaveType('Gaufrette\Adapter'); + $this->shouldHaveType('Gaufrette\Adapter\Local'); } - function it_should_compute_path_using_base64() + function it_computes_path_using_base64() { $this->read('filename')->shouldReturn('/home/l3l0/'.base64_encode('filename').' content'); } - function it_should_compute_key_back_using_base64() + function it_computes_key_back_using_base64() { global $iteratorToArray; $iteratorToArray = array('/home/l3l0/'.base64_encode('filename'), '/home/l3l0/'.base64_encode('filename1'), '/home/l3l0/'.base64_encode('aaa/filename')); diff --git a/spec/Gaufrette/Adapter/Sftp.php b/spec/Gaufrette/Adapter/SftpSpec.php similarity index 63% rename from spec/Gaufrette/Adapter/Sftp.php rename to spec/Gaufrette/Adapter/SftpSpec.php index 8229c93e3..14061e847 100644 --- a/spec/Gaufrette/Adapter/Sftp.php +++ b/spec/Gaufrette/Adapter/SftpSpec.php @@ -5,9 +5,9 @@ //hack - mock php built-in functions require_once 'functions.php'; -use PHPSpec2\ObjectBehavior; +use PhpSpec\ObjectBehavior; -class Sftp extends ObjectBehavior +class SftpSpec extends ObjectBehavior { /** * @param \Ssh\Sftp $sftp @@ -17,21 +17,29 @@ function let($sftp) $this->beConstructedWith($sftp, '/home/l3l0'); } - function it_should_be_initializable() + function it_is_adapter() { - $this->shouldHaveType('Gaufrette\Adapter\Sftp'); $this->shouldHaveType('Gaufrette\Adapter'); + } + + function it_is_checksum_calculator() + { $this->shouldHaveType('Gaufrette\Adapter\ChecksumCalculator'); } /** * @param \Ssh\Sftp $sftp */ - function it_should_get_keys($sftp) + function it_fetches_keys($sftp) { + $sftp + ->getUrl('/home/l3l0') + ->willReturn('ssh+ssl://localhost/home/l3l0') + ; $sftp ->listDirectory('/home/l3l0', true) - ->willReturn(array('files' => array('/home/l3l0/filename', '/home/l3l0/filename1', '/home/l3l0/aaa/filename'))); + ->willReturn(array('files' => array('/home/l3l0/filename', '/home/l3l0/filename1', '/home/l3l0/aaa/filename'))) + ; $this->keys()->shouldReturn(array('aaa', 'aaa/filename', 'filename', 'filename1')); } @@ -39,12 +47,17 @@ function it_should_get_keys($sftp) /** * @param \Ssh\Sftp $sftp */ - function it_should_read_file($sftp) + function it_reads_file($sftp) { + $sftp + ->getUrl('/home/l3l0') + ->willReturn('ssh+ssl://localhost/home/l3l0') + ; $sftp ->read('/home/l3l0/filename') ->shouldBeCalled() - ->willReturn('some content'); + ->willReturn('some content') + ; $this->read('filename')->shouldReturn('some content'); } @@ -52,12 +65,17 @@ function it_should_read_file($sftp) /** * @param \Ssh\Sftp $sftp */ - function it_should_write_file($sftp) + function it_writes_file($sftp) { + $sftp + ->getUrl('/home/l3l0') + ->willReturn('ssh+ssl://localhost/home/l3l0') + ; $sftp ->write('/home/l3l0/filename', 'some content') ->shouldBeCalled() - ->willReturn(12); + ->willReturn(12) + ; $this->write('filename', 'some content')->shouldReturn(12); } @@ -65,12 +83,17 @@ function it_should_write_file($sftp) /** * @param \Ssh\Sftp $sftp */ - function it_should_rename_file($sftp) + function it_renames_file($sftp) { + $sftp + ->getUrl('/home/l3l0') + ->willReturn('ssh+ssl://localhost/home/l3l0') + ; $sftp ->rename('/home/l3l0/filename', '/home/l3l0/filename1') ->shouldBeCalled() - ->willReturn(true); + ->willReturn(true) + ; $this->rename('filename', 'filename1')->shouldReturn(true); } @@ -78,19 +101,22 @@ function it_should_rename_file($sftp) /** * @param \Ssh\Sftp $sftp */ - function it_should_check_if_file_exists($sftp) + function it_checks_if_file_exists($sftp) { $sftp ->getUrl('/home/l3l0') - ->willReturn('ssh+ssl://localhost/home/l3l0'); + ->willReturn('ssh+ssl://localhost/home/l3l0') + ; $sftp ->getUrl('/home/l3l0/filename') ->shouldBeCalled() - ->willReturn('ssh+ssl://localhost/home/l3l0/filename'); + ->willReturn('ssh+ssl://localhost/home/l3l0/filename') + ; $sftp ->getUrl('/home/l3l0/filename1') ->shouldBeCalled() - ->willReturn('ssh+ssl://localhost/home/l3l0/filename1'); + ->willReturn('ssh+ssl://localhost/home/l3l0/filename1') + ; $this->exists('filename')->shouldReturn(true); $this->exists('filename1')->shouldReturn(false); diff --git a/spec/Gaufrette/Adapter/Zip.php b/spec/Gaufrette/Adapter/ZipSpec.php similarity index 54% rename from spec/Gaufrette/Adapter/Zip.php rename to spec/Gaufrette/Adapter/ZipSpec.php index 893e4e0ee..76bb1c13a 100644 --- a/spec/Gaufrette/Adapter/Zip.php +++ b/spec/Gaufrette/Adapter/ZipSpec.php @@ -2,18 +2,17 @@ namespace spec\Gaufrette\Adapter; -use PHPSpec2\ObjectBehavior; +use PhpSpec\ObjectBehavior; -class Zip extends ObjectBehavior +class ZipSpec extends ObjectBehavior { function let() { $this->beConstructedWith('somefile'); } - function it_should_be_initializable() + function it_is_adapter() { - $this->shouldHaveType('Gaufrette\Adapter\Zip'); $this->shouldHaveType('Gaufrette\Adapter'); } } diff --git a/spec/Gaufrette/File.php b/spec/Gaufrette/FileSpec.php similarity index 73% rename from spec/Gaufrette/File.php rename to spec/Gaufrette/FileSpec.php index 2c86a096a..86290b956 100644 --- a/spec/Gaufrette/File.php +++ b/spec/Gaufrette/FileSpec.php @@ -2,9 +2,9 @@ namespace spec\Gaufrette; -use PHPSpec2\ObjectBehavior; +use PhpSpec\ObjectBehavior; -class File extends ObjectBehavior +class FileSpec extends ObjectBehavior { /** * @param \Gaufrette\Filesystem $filesystem @@ -14,12 +14,12 @@ function let($filesystem) $this->beConstructedWith('filename', $filesystem); } - function it_should_be_initializable() + function it_is_initializable() { $this->shouldHaveType('Gaufrette\File'); } - function it_should_gives_access_to_key() + function it_gives_access_to_key() { $this->getKey()->shouldReturn('filename'); } @@ -27,7 +27,7 @@ function it_should_gives_access_to_key() /** * @param \Gaufrette\Filesystem $filesystem */ - function it_should_get_content($filesystem) + function it_gets_content($filesystem) { $filesystem->read('filename')->shouldBeCalled()->willReturn('Some content'); @@ -37,7 +37,7 @@ function it_should_get_content($filesystem) /** * @param \Gaufrette\Filesystem $filesystem */ - function it_should_get_mtime($filesystem) + function it_gets_mtime($filesystem) { $filesystem->mtime('filename')->shouldBeCalled()->willReturn(1358797854); @@ -48,10 +48,11 @@ function it_should_get_mtime($filesystem) * @param \Gaufrette\Filesystem $filesystem * @param \spec\Gaufrette\MetadataAdapter $adapter */ - function it_should_pass_metadata_when_write_content($filesystem, $adapter) + function it_pass_metadata_when_write_content($filesystem, $adapter) { $metadata = array('id' => '123'); $adapter->setMetadata('filename', $metadata)->shouldBeCalled(); + $filesystem->write('filename', 'some content', true)->willReturn(12); $filesystem->getAdapter()->willReturn($adapter); $this->setContent('some content', $metadata); @@ -61,10 +62,11 @@ function it_should_pass_metadata_when_write_content($filesystem, $adapter) * @param \Gaufrette\Filesystem $filesystem * @param \spec\Gaufrette\MetadataAdapter $adapter */ - function it_should_pass_metadata_when_read_content($filesystem, $adapter) + function it_pass_metadata_when_read_content($filesystem, $adapter) { $metadata = array('id' => '123'); $adapter->setMetadata('filename', $metadata)->shouldBeCalled(); + $filesystem->read('filename')->willReturn('some content'); $filesystem->getAdapter()->willReturn($adapter); $this->getContent($metadata); @@ -74,33 +76,21 @@ function it_should_pass_metadata_when_read_content($filesystem, $adapter) * @param \Gaufrette\Filesystem $filesystem * @param \spec\Gaufrette\MetadataAdapter $adapter */ - function it_should_pass_metadata_when_delete_content($filesystem, $adapter) + function it_pass_metadata_when_delete_content($filesystem, $adapter) { $metadata = array('id' => '123'); $adapter->setMetadata('filename', $metadata)->shouldBeCalled(); + $filesystem->delete('filename')->willReturn(true); $filesystem->getAdapter()->willReturn($adapter); $this->delete($metadata); } - /** - * @param \Gaufrette\Filesystem $filesystem - * @param \Gaufrette\Adapter $adapter - */ - function it_should_not_set_metadata_by_default($filesystem, $adapter) - { - $metadata = array('id' => '123'); - $adapter->setMetadata('filename', $metadata)->shouldNotBeCalled(); - $filesystem->getAdapter()->willReturn($adapter); - - $this->setContent('some content', $metadata); - } - /** * @param \Gaufrette\Filesystem $filesystem * @param \spec\Gaufrette\MetadataAdapter $adapter */ - function it_should_set_content($filesystem, $adapter) + function it_sets_content_of_file($filesystem, $adapter) { $adapter->setMetadata('filename', array())->shouldNotBeCalled(); $filesystem->getAdapter()->willReturn($adapter); @@ -110,12 +100,12 @@ function it_should_set_content($filesystem, $adapter) $this->getContent('filename')->shouldReturn('some content'); } - function it_should_set_key_as_name_by_default() + function it_sets_key_as_name_by_default() { $this->getName()->shouldReturn('filename'); } - function it_should_set_name() + function it_sets_name() { $this->setName('name'); $this->getName()->shouldReturn('name'); @@ -124,7 +114,7 @@ function it_should_set_name() /** * @param \Gaufrette\Filesystem $filesystem */ - function it_should_set_size_for_new_file($filesystem) + function it_sets_size_for_new_file($filesystem) { $filesystem->write('filename', 'some content', true)->shouldBeCalled()->willReturn(21); @@ -135,7 +125,7 @@ function it_should_set_size_for_new_file($filesystem) /** * @param \Gaufrette\Filesystem $filesystem */ - function it_should_calculate_size_when_is_not_set($filesystem) + function it_calculates_size_from_content($filesystem) { $filesystem->read('filename')->shouldBeCalled()->willReturn('some content'); @@ -145,7 +135,7 @@ function it_should_calculate_size_when_is_not_set($filesystem) /** * @param \Gaufrette\Filesystem $filesystem */ - function it_should_set_size($filesystem) + function it_allows_to_set_size($filesystem) { $filesystem->read('filename')->shouldNotBeCalled(); @@ -156,7 +146,7 @@ function it_should_set_size($filesystem) /** * @param \Gaufrette\Filesystem $filesystem */ - function it_should_get_zero_size_when_file_not_found($filesystem) + function it_gets_zero_size_when_file_not_found($filesystem) { $filesystem->read('filename')->willThrow(new \Gaufrette\Exception\FileNotFound('filename')); @@ -166,7 +156,7 @@ function it_should_get_zero_size_when_file_not_found($filesystem) /** * @param \Gaufrette\Filesystem $filesystem */ - function it_should_check_exists_in_filesystem($filesystem) + function it_check_if_file_with_key_exists_in_filesystem($filesystem) { $filesystem->has('filename')->willReturn(true); $this->exists()->shouldReturn(true); @@ -178,7 +168,7 @@ function it_should_check_exists_in_filesystem($filesystem) /** * @param \Gaufrette\Filesystem $filesystem */ - function it_should_delete_file_from_filesystem($filesystem) + function it_deletes_file_from_filesystem($filesystem) { $filesystem->delete('filename')->shouldBeCalled()->willReturn(true); $this->delete()->shouldReturn(true); diff --git a/spec/Gaufrette/FilesystemMap.php b/spec/Gaufrette/FilesystemMapSpec.php similarity index 69% rename from spec/Gaufrette/FilesystemMap.php rename to spec/Gaufrette/FilesystemMapSpec.php index a13cacc78..3d543f36c 100644 --- a/spec/Gaufrette/FilesystemMap.php +++ b/spec/Gaufrette/FilesystemMapSpec.php @@ -2,11 +2,11 @@ namespace spec\Gaufrette; -use PHPSpec2\ObjectBehavior; +use PhpSpec\ObjectBehavior; -class FilesystemMap extends ObjectBehavior +class FilesystemMapSpec extends ObjectBehavior { - function it_should_be_initializable() + function it_is_initializable() { $this->shouldHaveType('Gaufrette\FilesystemMap'); } @@ -14,7 +14,7 @@ function it_should_be_initializable() /** * @param Gaufrette\Filesystem $filesystem */ - function it_should_check_if_has_mapped_filesystem($filesystem) + function it_checks_if_has_mapped_filesystem($filesystem) { $this->set('some', $filesystem); $this->has('some')->shouldReturn(true); @@ -24,23 +24,24 @@ function it_should_check_if_has_mapped_filesystem($filesystem) /** * @param Gaufrette\Filesystem $filesystem */ - function it_should_set_mapped_filesystem($filesystem) + function it_sets_mapped_filesystem($filesystem) { $this->set('some', $filesystem); $this->get('some')->shouldReturn($filesystem); } - function it_should_fail_when_get_filesystem_which_was_not_mapped() + function it_fails_when_get_filesystem_which_was_not_mapped() { $this ->shouldThrow(new \InvalidArgumentException('There is no filesystem defined for the "some" domain.')) - ->duringGet('some'); + ->duringGet('some') + ; } /** * @param Gaufrette\Filesystem $filesystem */ - function it_should_remove_mapped_filesystem($filesystem) + function it_removes_mapped_filesystem($filesystem) { $this->set('some', $filesystem); $this->remove('some'); @@ -48,17 +49,18 @@ function it_should_remove_mapped_filesystem($filesystem) $this->has('some')->shouldReturn(false); } - function it_should_fail_when_remove_filesystem_which_was_not_mapped() + function it_fails_when_try_to_remove_filesystem_which_was_not_mapped() { $this ->shouldThrow(new \InvalidArgumentException('Cannot remove the "some" filesystem as it is not defined.')) - ->duringRemove('some'); + ->duringRemove('some') + ; } /** * @param Gaufrette\Filesystem $filesystem */ - function it_should_remove_all_filesystems($filesystem) + function it_removes_all_filesystems($filesystem) { $this->set('some', $filesystem); $this->set('other', $filesystem); diff --git a/spec/Gaufrette/Filesystem.php b/spec/Gaufrette/FilesystemSpec.php similarity index 79% rename from spec/Gaufrette/Filesystem.php rename to spec/Gaufrette/FilesystemSpec.php index f76ddb42f..61d07b946 100644 --- a/spec/Gaufrette/Filesystem.php +++ b/spec/Gaufrette/FilesystemSpec.php @@ -2,9 +2,10 @@ namespace spec\Gaufrette; -use PHPSpec2\ObjectBehavior; +use PhpSpec\ObjectBehavior; +use Prophecy\Argument; -class Filesystem extends ObjectBehavior +class FilesystemSpec extends ObjectBehavior { /** * @param \Gaufrette\Adapter $adapter @@ -14,7 +15,7 @@ function let($adapter) $this->beConstructedWith($adapter); } - function it_should_be_initializable() + function it_is_initializable() { $this->shouldBeAnInstanceOf('Gaufrette\Filesystem'); } @@ -30,7 +31,7 @@ function it_gives_access_to_adapter($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_should_check_is_file_exists_using_adapter($adapter) + function it_check_if_file_exists_using_adapter($adapter) { $adapter->exists('filename')->willReturn(true); $adapter->exists('otherFilename')->willReturn(false); @@ -42,7 +43,7 @@ function it_should_check_is_file_exists_using_adapter($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_should_rename_file($adapter) + function it_renames_file($adapter) { $adapter->exists('filename')->shouldBeCalled()->willReturn(true); $adapter->exists('otherFilename')->shouldBeCalled()->willReturn(false); @@ -54,32 +55,34 @@ function it_should_rename_file($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_should_fail_when_renamed_source_file_does_not_exist($adapter) + function it_fails_when_renamed_source_file_does_not_exist($adapter) { $adapter->exists('filename')->willReturn(false); $this ->shouldThrow(new \Gaufrette\Exception\FileNotFound('filename')) - ->duringRename('filename', 'otherFilename'); + ->duringRename('filename', 'otherFilename') + ; } /** * @param \Gaufrette\Adapter $adapter */ - function it_should_fail_when_renamed_target_file_exists($adapter) + function it_fails_when_renamed_target_file_exists($adapter) { $adapter->exists('filename')->willReturn(true); $adapter->exists('otherFilename')->willReturn(true); $this ->shouldThrow(new \Gaufrette\Exception\UnexpectedFile('otherFilename')) - ->duringRename('filename', 'otherFilename'); + ->duringRename('filename', 'otherFilename') + ; } /** * @param \Gaufrette\Adapter $adapter */ - function it_should_fail_when_rename_is_not_successful($adapter) + function it_fails_when_rename_is_not_successful($adapter) { $adapter->exists('filename')->willReturn(true); $adapter->exists('otherFilename')->willReturn(false); @@ -87,13 +90,14 @@ function it_should_fail_when_rename_is_not_successful($adapter) $this ->shouldThrow(new \RuntimeException('Could not rename the "filename" key to "otherFilename".')) - ->duringRename('filename', 'otherFilename'); + ->duringRename('filename', 'otherFilename') + ; } /** * @param \Gaufrette\Adapter $adapter */ - function it_should_get_file_object($adapter) + function it_creates_file_object_for_key($adapter) { $adapter->exists('filename')->willReturn(true); @@ -103,19 +107,20 @@ function it_should_get_file_object($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_should_not_get_file_object_when_file_does_not_exist($adapter) + function it_does_not_get_file_object_when_file_with_key_does_not_exist($adapter) { $adapter->exists('filename')->willReturn(false); $this ->shouldThrow(new \Gaufrette\Exception\FileNotFound('filename')) - ->duringGet('filename'); + ->duringGet('filename') + ; } /** * @param \Gaufrette\Adapter $adapter */ - function it_should_get_file_object_when_file_does_not_exist_but_can_be_created($adapter) + function it_gets_file_object_when_file_does_not_exist_but_can_be_created($adapter) { $adapter->exists('filename')->willReturn(false); @@ -126,7 +131,7 @@ function it_should_get_file_object_when_file_does_not_exist_but_can_be_created($ * @param \spec\Gaufrette\Adapter $extendedAdapter * @param \Gaufrette\File $file */ - function it_should_delegate_file_creation_to_adapter_when_adapter_is_file_factory($extendedAdapter, $file) + function it_delegates_file_creation_to_adapter_when_adapter_is_file_factory($extendedAdapter, $file) { $this->beConstructedWith($extendedAdapter); $extendedAdapter->exists('filename')->willReturn(true); @@ -138,7 +143,7 @@ function it_should_delegate_file_creation_to_adapter_when_adapter_is_file_factor /** * @param \Gaufrette\Adapter $adapter */ - function it_should_write_content_to_new_file($adapter) + function it_writes_content_to_new_file($adapter) { $adapter->exists('filename')->shouldBeCalled()->willReturn(false); $adapter->write('filename', 'some content to write')->shouldBeCalled()->willReturn(21); @@ -149,7 +154,7 @@ function it_should_write_content_to_new_file($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_should_update_content_of_file($adapter) + function it_updates_content_of_file($adapter) { $adapter->write('filename', 'some content to write')->shouldBeCalled()->willReturn(21); @@ -159,33 +164,35 @@ function it_should_update_content_of_file($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_should_not_update_content_of_file_when_file_cannot_be_overwrite($adapter) + function it_does_not_update_content_of_file_when_file_cannot_be_overwriten($adapter) { $adapter->exists('filename')->willReturn(true); $adapter->write('filename', 'some content to write')->shouldNotBeCalled(); $this ->shouldThrow(new \Gaufrette\Exception\FileAlreadyExists('filename')) - ->duringWrite('filename', 'some content to write'); + ->duringWrite('filename', 'some content to write') + ; } /** * @param \Gaufrette\Adapter $adapter */ - function it_should_fail_when_write_is_not_successful($adapter) + function it_fails_when_write_is_not_successful($adapter) { $adapter->exists('filename')->willReturn(false); $adapter->write('filename', 'some content to write')->shouldBeCalled()->willReturn(false); $this ->shouldThrow(new \RuntimeException('Could not write the "filename" key content.')) - ->duringWrite('filename', 'some content to write'); + ->duringWrite('filename', 'some content to write') + ; } /** * @param \Gaufrette\Adapter $adapter */ - function it_should_read_file($adapter) + function it_read_file($adapter) { $adapter->exists('filename')->shouldBeCalled()->willReturn(true); $adapter->read('filename')->shouldBeCalled()->willReturn('Some content'); @@ -196,7 +203,7 @@ function it_should_read_file($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_should_not_read_file_which_does_not_exist($adapter) + function it_does_not_read_file_which_does_not_exist($adapter) { $adapter->exists('filename')->willReturn(false); @@ -208,20 +215,21 @@ function it_should_not_read_file_which_does_not_exist($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_should_fail_when_read_is_not_successful($adapter) + function it_fails_when_read_is_not_successful($adapter) { $adapter->exists('filename')->willReturn(true); $adapter->read('filename')->willReturn(false); $this ->shouldThrow(new \RuntimeException('Could not read the "filename" key content.')) - ->duringRead('filename'); + ->duringRead('filename') + ; } /** * @param \Gaufrette\Adapter $adapter */ - function it_should_delete_file($adapter) + function it_deletes_file($adapter) { $adapter->exists('filename')->shouldBeCalled()->willReturn(true); $adapter->delete('filename')->shouldBeCalled()->willReturn(true); @@ -232,26 +240,28 @@ function it_should_delete_file($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_should_not_delete_file_which_does_not_exist($adapter) + function it_does_not_delete_file_which_does_not_exist($adapter) { $adapter->exists('filename')->willReturn(false); $this ->shouldThrow(new \Gaufrette\Exception\FileNotFound('filename')) - ->duringDelete('filename'); + ->duringDelete('filename') + ; } /** * @param \Gaufrette\Adapter $adapter */ - function it_should_fail_when_delete_is_not_successful($adapter) + function it_fails_when_delete_is_not_successful($adapter) { $adapter->exists('filename')->willReturn(true); $adapter->delete('filename')->willReturn(false); $this ->shouldThrow(new \RuntimeException('Could not remove the "filename" key.')) - ->duringDelete('filename'); + ->duringDelete('filename') + ; } /** @@ -268,12 +278,12 @@ function it_should_get_all_keys($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_should_match_listed_keys_using_specified_pattern($adapter) + function it_match_listed_keys_using_specified_pattern($adapter) { $keys = array('filename', 'filename1', 'filename2', 'testKey', 'KeyTest', 'testkey'); $adapter->keys()->willReturn($keys); - $adapter->isDirectory(ANY_ARGUMENT)->willReturn(false); + $adapter->isDirectory(Argument::any())->willReturn(false); $this->listKeys()->shouldReturn( array( @@ -298,7 +308,7 @@ function it_should_match_listed_keys_using_specified_pattern($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_should_decide_which_key_is_dir_using_adapter($adapter) + function it_listing_directories_using_adapter_is_directory_method($adapter) { $keys = array('filename', 'filename1', 'filename2', 'testKey', 'KeyTest', 'testkey'); @@ -334,7 +344,7 @@ function it_should_decide_which_key_is_dir_using_adapter($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_should_get_mtime_of_file_using_adapter($adapter) + function it_gets_mtime_of_file_using_adapter($adapter) { $adapter->exists('filename')->willReturn(true); $adapter->mtime('filename')->willReturn(1234567); @@ -345,19 +355,20 @@ function it_should_get_mtime_of_file_using_adapter($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_should_not_get_mtime_of_file_which_does_not_exist($adapter) + function it_does_not_get_mtime_of_file_which_does_not_exist($adapter) { $adapter->exists('filename')->willReturn(false); $this ->shouldThrow(new \Gaufrette\Exception\FileNotFound('filename')) - ->duringMtime('filename'); + ->duringMtime('filename') + ; } /** * @param \Gaufrette\Adapter $adapter */ - function it_should_calculate_file_checksum($adapter) + function it_calculates_file_checksum($adapter) { $adapter->exists('filename')->shouldBeCalled()->willReturn(true); $adapter->read('filename')->willReturn('some content'); @@ -368,7 +379,7 @@ function it_should_calculate_file_checksum($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_should_not_calculate_checksum_of_file_which_does_not_exist($adapter) + function it_does_not_calculate_checksum_of_file_which_does_not_exist($adapter) { $adapter->exists('filename')->shouldBeCalled()->willReturn(false); @@ -380,7 +391,7 @@ function it_should_not_calculate_checksum_of_file_which_does_not_exist($adapter) /** * @param \spec\Gaufrette\Adapter $extendedAdapter */ - function it_should_delegate_checksum_calculation_to_adapter_when_adapter_is_checksum_calculator($extendedAdapter) + function it_delegates_checksum_calculation_to_adapter_when_adapter_is_checksum_calculator($extendedAdapter) { $this->beConstructedWith($extendedAdapter); $extendedAdapter->exists('filename')->shouldBeCalled()->willReturn(true); diff --git a/spec/Gaufrette/StreamMode.php b/spec/Gaufrette/StreamModeSpec.php similarity index 95% rename from spec/Gaufrette/StreamMode.php rename to spec/Gaufrette/StreamModeSpec.php index de33f6ffb..1550bba23 100644 --- a/spec/Gaufrette/StreamMode.php +++ b/spec/Gaufrette/StreamModeSpec.php @@ -2,17 +2,17 @@ namespace spec\Gaufrette; -use PHPSpec2\ObjectBehavior; +use PhpSpec\ObjectBehavior; -class StreamMode extends ObjectBehavior +class StreamModeSpec extends ObjectBehavior { - function it_should_be_initializable() + function it_is_initializable() { $this->beConstructedWith('r'); $this->shouldHaveType('Gaufrette\StreamMode'); } - function it_should_gives_access_to_mode() + function it_gives_access_to_mode() { $this->beConstructedWith('r+'); $this->getMode()->shouldReturn('r+'); diff --git a/spec/Gaufrette/StreamWrapper.php b/spec/Gaufrette/StreamWrapperSpec.php similarity index 71% rename from spec/Gaufrette/StreamWrapper.php rename to spec/Gaufrette/StreamWrapperSpec.php index 7a61f4db2..9dd1131b8 100644 --- a/spec/Gaufrette/StreamWrapper.php +++ b/spec/Gaufrette/StreamWrapperSpec.php @@ -2,9 +2,10 @@ namespace spec\Gaufrette; -use PHPSpec2\ObjectBehavior; +use PhpSpec\ObjectBehavior; +use Prophecy\Argument; -class StreamWrapper extends ObjectBehavior +class StreamWrapperSpec extends ObjectBehavior { /** * @param \Gaufrette\FilesystemMap $map @@ -18,7 +19,7 @@ function let($map, $filesystem, $stream) $this->setFilesystemMap($map); } - function it_should_be_initializable() + function it_is_initializable() { $this->shouldHaveType('Gaufrette\StreamWrapper'); } @@ -26,28 +27,29 @@ function it_should_be_initializable() /** * @param \Gaufrette\Stream $stream */ - function it_should_open_stream($stream) + function it_opens_stream($stream) { - $stream->open(ANY_ARGUMENT)->willReturn(true); + $stream->open(Argument::any())->willReturn(true); $this->stream_open('gaufrette://some/filename', 'r+')->shouldReturn(true); } - function it_should_not_open_stream_when_key_is_not_defined() + function it_does_not_open_stream_when_key_is_not_defined() { $this ->shouldThrow(new \InvalidArgumentException('The specified path (gaufrette://some) is invalid.')) ->duringStream_open('gaufrette://some', 'r+'); } - function it_should_not_open_stream_when_host_is_not_defined() + function it_does_not_open_stream_when_host_is_not_defined() { $this ->shouldThrow(new \InvalidArgumentException('The specified path (gaufrette:///somefile) is invalid.')) - ->duringStream_open('gaufrette:///somefile', 'r+'); + ->duringStream_open('gaufrette:///somefile', 'r+') + ; } - function it_should_not_read_from_stream_when_is_not_opened() + function it_does_not_read_from_stream_when_is_not_opened() { $this->stream_read(10)->shouldReturn(false); } @@ -55,15 +57,16 @@ function it_should_not_read_from_stream_when_is_not_opened() /** * @param \Gaufrette\Stream $stream */ - function it_should_read_from_stream($stream) + function it_does_not_read_from_stream($stream) { + $stream->open(Argument::any())->willReturn(true); $stream->read(4)->willReturn('some'); $this->stream_open('gaufrette://some/filename', 'r+'); $this->stream_read(4)->shouldReturn('some'); } - function it_should_not_write_to_stream_when_is_not_opened() + function it_does_not_write_to_stream_when_is_not_opened() { $this->stream_write('some content')->shouldReturn(0); } @@ -71,8 +74,9 @@ function it_should_not_write_to_stream_when_is_not_opened() /** * @param \Gaufrette\Stream $stream */ - function it_should_write_to_stream($stream) + function it_writes_to_stream($stream) { + $stream->open(Argument::any())->willReturn(true); $stream->write('some content')->shouldBeCalled()->willReturn(12); $this->stream_open('gaufrette://some/filename', 'w+'); @@ -82,7 +86,7 @@ function it_should_write_to_stream($stream) /** * @param \Gaufrette\Stream $stream */ - function it_should_not_close_stream_when_is_not_opened($stream) + function it_does_not_close_stream_when_is_not_opened($stream) { $stream->close()->shouldNotBeCalled(); $this->stream_close(); @@ -91,8 +95,9 @@ function it_should_not_close_stream_when_is_not_opened($stream) /** * @param \Gaufrette\Stream $stream */ - function it_should_close_stream($stream) + function it_closes_stream($stream) { + $stream->open(Argument::any())->willReturn(true); $stream->close()->shouldBeCalled(); $this->stream_open('gaufrette://some/filename', 'w+'); $this->stream_close(); @@ -101,7 +106,7 @@ function it_should_close_stream($stream) /** * @param \Gaufrette\Stream $stream */ - function it_should_not_flush_stream_when_is_not_opened($stream) + function it_does_not_flush_stream_when_is_not_opened($stream) { $stream->flush()->shouldNotBeCalled(); $this->stream_flush(); @@ -110,8 +115,9 @@ function it_should_not_flush_stream_when_is_not_opened($stream) /** * @param \Gaufrette\Stream $stream */ - function it_should_flush_stream($stream) + function it_flushes_stream($stream) { + $stream->open(Argument::any())->willReturn(true); $stream->flush()->shouldBeCalled(); $this->stream_open('gaufrette://some/filename', 'w+'); $this->stream_flush(); @@ -120,7 +126,7 @@ function it_should_flush_stream($stream) /** * @param \Gaufrette\Stream $stream */ - function it_should_not_seek_in_stream_when_is_not_opened($stream) + function it_does_not_seek_in_stream_when_is_not_opened($stream) { $stream->seek(12, SEEK_SET)->shouldNotBeCalled(); $this->stream_seek(12, SEEK_SET); @@ -129,8 +135,9 @@ function it_should_not_seek_in_stream_when_is_not_opened($stream) /** * @param \Gaufrette\Stream $stream */ - function it_should_seek_in_stream($stream) + function it_seeks_in_stream($stream) { + $stream->open(Argument::any())->willReturn(true); $stream->seek(12, SEEK_SET)->shouldBeCalled()->willReturn(true); $this->stream_open('gaufrette://some/filename', 'w+'); $this->stream_seek(12, SEEK_SET)->shouldReturn(true); @@ -139,7 +146,7 @@ function it_should_seek_in_stream($stream) /** * @param \Gaufrette\Stream $stream */ - function it_should_not_tell_about_position_in_stream_when_is_not_opened($stream) + function it_does_not_tell_about_position_in_stream_when_is_not_opened($stream) { $stream->tell()->shouldNotBeCalled(); $this->stream_tell(); @@ -148,8 +155,9 @@ function it_should_not_tell_about_position_in_stream_when_is_not_opened($stream) /** * @param \Gaufrette\Stream $stream */ - function it_should_tell_about_position_in_stream($stream) + function it_does_tell_about_position_in_stream($stream) { + $stream->open(Argument::any())->willReturn(true); $stream->tell()->shouldBeCalled()->willReturn(12); $this->stream_open('gaufrette://some/filename', 'w+'); $this->stream_tell()->shouldReturn(12); @@ -158,7 +166,7 @@ function it_should_tell_about_position_in_stream($stream) /** * @param \Gaufrette\Stream $stream */ - function it_should_mark_as_eof_if_stream_is_not_opened($stream) + function it_does_not_mark_as_eof_if_stream_is_not_opened($stream) { $stream->eof()->shouldNotBeCalled(); $this->stream_eof(); @@ -167,8 +175,9 @@ function it_should_mark_as_eof_if_stream_is_not_opened($stream) /** * @param \Gaufrette\Stream $stream */ - function it_should_check_if_eof_in_stream($stream) + function it_checks_if_eof($stream) { + $stream->open(Argument::any())->willReturn(true); $this->stream_open('gaufrette://some/filename', 'w+'); $stream->eof()->willReturn(false); @@ -178,7 +187,7 @@ function it_should_check_if_eof_in_stream($stream) $this->stream_eof()->shouldReturn(true); } - function it_should_not_get_stat_when_is_not_open() + function it_does_not_get_stat_when_is_not_open() { $this->stream_stat()->shouldReturn(false); } @@ -186,7 +195,7 @@ function it_should_not_get_stat_when_is_not_open() /** * @param \Gaufrette\Stream $stream */ - function it_should_stat_file_from_stream($stream) + function it_stats_file($stream) { $stat = array( 'dev' => 1, @@ -203,6 +212,7 @@ function it_should_stat_file_from_stream($stream) 'blksize' => 5, 'blocks' => 1, ); + $stream->open(Argument::any())->willReturn(true); $stream->stat()->willReturn($stat); $this->stream_open('gaufrette://some/filename', 'w+'); @@ -229,6 +239,7 @@ function it_should_stat_from_url($stream) 'blksize' => 5, 'blocks' => 1, ); + $stream->open(Argument::any())->willReturn(true); $stream->stat()->willReturn($stat); $this->url_stat('gaufrette://some/filename', STREAM_URL_STAT_LINK)->shouldReturn($stat); @@ -237,32 +248,33 @@ function it_should_stat_from_url($stream) /** * @param \Gaufrette\Stream $stream */ - function it_should_not_stat_when_cannot_open($stream) + function it_does_not_stat_when_cannot_open($stream) { - $stream->open(ANY_ARGUMENT)->willThrow(new \RuntimeException); + $stream->open(Argument::any())->willThrow(new \RuntimeException); $this->url_stat('gaufrette://some/filename', STREAM_URL_STAT_LINK)->shouldReturn(false); } /** * @param \Gaufrette\Stream $stream */ - function it_should_not_unlink_when_cannot_open($stream) + function it_does_not_unlink_when_cannot_open($stream) { - $stream->open(ANY_ARGUMENT)->willThrow(new \RuntimeException); + $stream->open(Argument::any())->willThrow(new \RuntimeException); $this->unlink('gaufrette://some/filename')->shouldReturn(false); } /** * @param \Gaufrette\Stream $stream */ - function it_should_unlink($stream) + function it_unlinks_file($stream) { + $stream->open(Argument::any())->willReturn(true); $stream->unlink()->willReturn(true); $this->unlink('gaufrette://some/filename')->shouldReturn(true); } - function it_should_not_cast_stream_if_is_not_opened() + function it_does_not_cast_stream_if_is_not_opened() { $this->stream_cast(STREAM_CAST_FOR_SELECT)->shouldReturn(false); } @@ -270,8 +282,9 @@ function it_should_not_cast_stream_if_is_not_opened() /** * @param \Gaufrette\Stream $stream */ - function it_should_cast_stream($stream) + function it_casts_stream($stream) { + $stream->open(Argument::any())->willReturn(true); $stream->cast(STREAM_CAST_FOR_SELECT)->willReturn('resource'); $this->stream_open('gaufrette://some/filename', 'w+'); diff --git a/spec/Gaufrette/Util/Checksum.php b/spec/Gaufrette/Util/ChecksumSpec.php similarity index 75% rename from spec/Gaufrette/Util/Checksum.php rename to spec/Gaufrette/Util/ChecksumSpec.php index 27a66e0a1..f61d35f8e 100644 --- a/spec/Gaufrette/Util/Checksum.php +++ b/spec/Gaufrette/Util/ChecksumSpec.php @@ -2,9 +2,9 @@ namespace spec\Gaufrette\Util; -use PHPSpec2\ObjectBehavior; +use PHPSpec\ObjectBehavior; -class Checksum extends ObjectBehavior +class ChecksumSpec extends ObjectBehavior { function let() { @@ -12,12 +12,12 @@ function let() file_put_contents($path, 'some other content'); } - function it_should_calculate_checksum_from_content() + function it_calculates_checksum_from_content() { $this->fromContent('some content')->shouldReturn(md5('some content')); } - function it_should_calculate_checksum_from_filepath() + function it_calculates_checksum_from_filepath() { $path = __DIR__.DIRECTORY_SEPARATOR.'testFile'; $this->fromFile($path)->shouldReturn(md5('some other content')); diff --git a/spec/Gaufrette/Util/Path.php b/spec/Gaufrette/Util/PathSpec.php similarity index 79% rename from spec/Gaufrette/Util/Path.php rename to spec/Gaufrette/Util/PathSpec.php index 46430424e..e0a7e62f2 100644 --- a/spec/Gaufrette/Util/Path.php +++ b/spec/Gaufrette/Util/PathSpec.php @@ -2,18 +2,18 @@ namespace spec\Gaufrette\Util; -use PHPSpec2\ObjectBehavior; +use PhpSpec\ObjectBehavior; -class Path extends ObjectBehavior +class PathSpec extends ObjectBehavior { - function it_should_check_if_path_is_absolute() + function it_checks_if_path_is_absolute() { $this->isAbsolute('/home/path')->shouldBe(true); $this->isAbsolute('home/path')->shouldBe(false); $this->isAbsolute('../home/path')->shouldBe(false); } - function it_should_normalize_file_path() + function it_normalizes_file_path() { $this->normalize('C:\\some\other.txt')->shouldReturn('c:/some/other.txt'); $this->normalize('..\other.txt')->shouldReturn('../other.txt'); diff --git a/spec/Gaufrette/Util/Size.php b/spec/Gaufrette/Util/SizeSpec.php similarity index 67% rename from spec/Gaufrette/Util/Size.php rename to spec/Gaufrette/Util/SizeSpec.php index 47585e685..ef9f0a813 100644 --- a/spec/Gaufrette/Util/Size.php +++ b/spec/Gaufrette/Util/SizeSpec.php @@ -2,11 +2,11 @@ namespace spec\Gaufrette\Util; -use PHPSpec2\ObjectBehavior; +use PhpSpec\ObjectBehavior; -class Size extends ObjectBehavior +class SizeSpec extends ObjectBehavior { - function it_should_calculate_size_of_content() + function it_calculates_size_of_content() { $this->fromContent('some content')->shouldReturn(12); $this->fromContent('some other content')->shouldReturn(18); diff --git a/src/Gaufrette/Adapter/Cache.php b/src/Gaufrette/Adapter/Cache.php index 67384655b..2a3412bcc 100644 --- a/src/Gaufrette/Adapter/Cache.php +++ b/src/Gaufrette/Adapter/Cache.php @@ -197,12 +197,11 @@ public function needsReload($key) if ($this->cache->exists($key)) { try { $dateCache = $this->cache->mtime($key); + $needsReload = false; if (time() - $this->ttl >= $dateCache) { $dateSource = $this->source->mtime($key); $needsReload = $dateCache < $dateSource; - } else { - $needsReload = false; } } catch (\RuntimeException $e) { } } From d74743c65fb3c52174be4a12c380edc56763532f Mon Sep 17 00:00:00 2001 From: l3l0 Date: Thu, 27 Jun 2013 11:47:47 +0200 Subject: [PATCH 015/426] Try to fix travis builds. --- bin/configure_test_env.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bin/configure_test_env.sh b/bin/configure_test_env.sh index 7e3cafbb7..a6873a66a 100755 --- a/bin/configure_test_env.sh +++ b/bin/configure_test_env.sh @@ -1,9 +1,8 @@ #!/bin/bash -sudo apt-get update -qq sudo apt-get install -qq libssh2-1-dev libssh2-php pecl install mongo &> /dev/null touch .interactive -(pecl install -f ssh2-beta < .interactive) +(pecl install -f ssh2 < .interactive) cp tests/Gaufrette/Functional/adapters/DoctrineDbal.php.dist tests/Gaufrette/Functional/adapters/DoctrineDbal.php -f From b25179da3d7bcf367eb985c66be3ac239a881011 Mon Sep 17 00:00:00 2001 From: l3l0 Date: Thu, 27 Jun 2013 12:42:37 +0200 Subject: [PATCH 016/426] Configure build, with issue with php 5.3 at travis. --- bin/configure_test_env.sh | 2 +- .../AuthenticationConnectionFactorySpec.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/bin/configure_test_env.sh b/bin/configure_test_env.sh index a6873a66a..c525cfa24 100755 --- a/bin/configure_test_env.sh +++ b/bin/configure_test_env.sh @@ -1,7 +1,7 @@ #!/bin/bash sudo apt-get install -qq libssh2-1-dev libssh2-php -pecl install mongo &> /dev/null +pecl install -f mongo touch .interactive (pecl install -f ssh2 < .interactive) diff --git a/spec/Gaufrette/Adapter/RackspaceCloudfiles/AuthenticationConnectionFactorySpec.php b/spec/Gaufrette/Adapter/RackspaceCloudfiles/AuthenticationConnectionFactorySpec.php index a64f21bca..47549fb6b 100644 --- a/spec/Gaufrette/Adapter/RackspaceCloudfiles/AuthenticationConnectionFactorySpec.php +++ b/spec/Gaufrette/Adapter/RackspaceCloudfiles/AuthenticationConnectionFactorySpec.php @@ -29,8 +29,8 @@ function it_creates_cf_connection($authentication) function it_authenticates_when_was_not_authenticated_before($authentication) { $authentication->authenticated()->willReturn(false); - $authentication->authenticate()->shouldBeCalled()->will(function () { - $this->authenticated()->willReturn(true); + $authentication->authenticate()->shouldBeCalled()->will(function () use ($authentication) { + $authentication->authenticated()->willReturn(true); }); $this->create()->shouldReturnAnInstanceOf('\CF_Connection'); From d85f98070b44744ee52c85b90867de1ae89d9e13 Mon Sep 17 00:00:00 2001 From: l3l0 Date: Thu, 27 Jun 2013 14:06:35 +0200 Subject: [PATCH 017/426] Add "--no-interaction" to composer install command at travis. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 749fe81e2..9f3951441 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,7 +6,7 @@ php: before_script: - ./bin/configure_test_env.sh - - composer self-update && composer install --dev --prefer-source + - composer self-update && composer install --dev --prefer-source --no-interaction script: - php bin/phpspec run -fpretty --verbose From 828978becccf18a0f541719211a996c8162af7d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Haso=C5=88?= Date: Fri, 12 Apr 2013 22:52:53 +0200 Subject: [PATCH 018/426] Fixed Ftp::exists for hidden files --- spec/Gaufrette/Adapter/FtpSpec.php | 7 +++++ spec/Gaufrette/Adapter/functions.php | 41 ++++++++++++---------------- src/Gaufrette/Adapter/Ftp.php | 29 +++++++++----------- 3 files changed, 38 insertions(+), 39 deletions(-) diff --git a/spec/Gaufrette/Adapter/FtpSpec.php b/spec/Gaufrette/Adapter/FtpSpec.php index 6c3489aa9..5f7adf046 100644 --- a/spec/Gaufrette/Adapter/FtpSpec.php +++ b/spec/Gaufrette/Adapter/FtpSpec.php @@ -35,6 +35,13 @@ function it_checks_if_file_exists_for_relative_path() $this->exists('some/otherfilename')->shouldReturn(true); } + function it_checks_if_dir_exists_for_symlink() + { + $this->exists('www')->shouldReturn(true); + $this->exists('vendor')->shouldReturn(true); + $this->exists('bbb')->shouldReturn(false); + } + function it_reads_file() { $this->read('filename')->shouldReturn('some content'); diff --git a/spec/Gaufrette/Adapter/functions.php b/spec/Gaufrette/Adapter/functions.php index 6ab7b215b..4f9610b24 100644 --- a/spec/Gaufrette/Adapter/functions.php +++ b/spec/Gaufrette/Adapter/functions.php @@ -76,27 +76,6 @@ function ftp_mkdir($connection, $dirname) return false; } -function ftp_nlist($connection, $dirname) -{ - $arguments = explode(' ', $dirname); - switch (end($arguments)) { - case '/home/l3l0': - return array('/home/l3l0/filename'); - case '/home/l3l0/aaa': - return array('/home/l3l0/aaa/filename', '/home/l3l0/aaa/otherFilename'); - case '/home/l3l0/relative': - return array('filename', 'some'); - case '/home/l3l0/relative/some': - return array('otherfilename'); - } - - if ('/home/l3l1' === end($arguments)) { - return array('/home/l3l1/filename', '/home/l3l1/.htaccess'); - } - - return false; -} - function ftp_connect($host, $password) { if ('localhost' !== $host) { @@ -123,14 +102,30 @@ function ftp_rawlist($connection, $directory, $recursive = false) "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename", "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename.exe", "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .htaccess", - "lrwxrwxrwx 1 vincent vincent 11 Jul 12 12:16 www -> aaa" + "lrwxrwxrwx 1 vincent vincent 11 Jul 12 12:16 www -> aaa", + "lrwxrwxrwx 1 vincent vincent 11 Jul 12 12:16 vendor -> bbb", ); } if ('/home/l3l0/aaa' === end($arguments)) { return array( - "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename" + "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename", + ); + } + + if ('/home/l3l0/relative' === end($arguments)) + { + return array( + "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename", + "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 some", + ); + } + + if ('/home/l3l0/relative/some' === end($arguments)) + { + return array( + "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 otherfilename", ); } diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index 90e418cac..4d43a2dc3 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -116,9 +116,20 @@ public function exists($key) $this->ensureDirectoryExists($this->directory, $this->create); $file = $this->computePath($key); - $items = ftp_nlist($this->getConnection(), dirname($file)); + $lines = ftp_rawlist($this->getConnection(), '-al ' . dirname($file)); - return $items && (in_array($file, $items) || in_array(basename($file), $items)); + if (false === $lines) { + return false; + } + + $pattern = '{(?) '.preg_quote(basename($file)).'( -> |$)}m'; + foreach ($lines as $line) { + if (preg_match($pattern, $line)) { + return true; + } + } + + return false; } /** @@ -453,20 +464,6 @@ private function close() } } - /** - * Asserts the specified file exists - * - * @param string $key - * - * @throws Exception\FileNotFound if the file does not exist - */ - private function assertExists($key) - { - if (!$this->exists($key)) { - throw new Exception\FileNotFound($key); - } - } - private function isLinuxListing($info) { return count($info) >= 9; From 0d0f321c1fd6359fbc4e877a9f52e7b7e459c821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Haso=C5=88?= Date: Mon, 22 Apr 2013 18:58:50 +0200 Subject: [PATCH 019/426] [Ftp] Added tests for files and dirs with spaces and unicode chars in name --- spec/Gaufrette/Adapter/FtpSpec.php | 14 +++++++++++ spec/Gaufrette/Adapter/functions.php | 37 +++++++++++++++++++++------- 2 files changed, 42 insertions(+), 9 deletions(-) diff --git a/spec/Gaufrette/Adapter/FtpSpec.php b/spec/Gaufrette/Adapter/FtpSpec.php index 5f7adf046..cd80962df 100644 --- a/spec/Gaufrette/Adapter/FtpSpec.php +++ b/spec/Gaufrette/Adapter/FtpSpec.php @@ -42,6 +42,13 @@ function it_checks_if_dir_exists_for_symlink() $this->exists('bbb')->shouldReturn(false); } + function it_checks_if_dir_exists_with_special_and_unicode_chars_in_name() + { + $this->beConstructedWith('/home/l3l2', 'localhost'); + + $this->exists('a b c d -> žežulička')->shouldReturn(true); + } + function it_reads_file() { $this->read('filename')->shouldReturn('some content'); @@ -77,6 +84,13 @@ function it_fetches_keys_without_directories_dots() $this->keys()->shouldReturn(array('filename', 'filename.exe', '.htaccess', 'aaa', 'aaa/filename')); } + function it_fetches_keys_with_spaces_and_unicode_chars() + { + $this->beConstructedWith('/home/l3l2', 'localhost'); + + $this->keys()->shouldReturn(array('Žľuťoučký kůň.pdf', 'a b c d -> žežulička', 'a b c d -> žežulička/do re mi.pdf')); + } + function it_fetches_mtime() { $this->mtime('filename')->shouldReturn(strtotime('2010-10-10 23:10:10')); diff --git a/spec/Gaufrette/Adapter/functions.php b/spec/Gaufrette/Adapter/functions.php index 4f9610b24..d2712dae1 100644 --- a/spec/Gaufrette/Adapter/functions.php +++ b/spec/Gaufrette/Adapter/functions.php @@ -49,7 +49,7 @@ function ftp_fget($connection, &$fileResource, $path, $mode) function ftp_chdir($connection, $dirname) { - if (in_array($dirname, array('/home/l3l0', '/home/l3l0/aaa', '/home/l3l0/relative', '/home/l3l0/relative/some', '/home/l3l1', 'C:\Ftp'))) { + if (in_array($dirname, array('/home/l3l0', '/home/l3l0/aaa', '/home/l3l0/relative', '/home/l3l0/relative/some', '/home/l3l1', '/home/l3l2', '/home/l3l2/a b c d -> žežulička', 'C:\Ftp'))) { return true; } @@ -92,7 +92,7 @@ function ftp_close($connection) function ftp_rawlist($connection, $directory, $recursive = false) { - $arguments = explode(' ', $directory); + $arguments = explode(' ', $directory, 2); if ('/home/l3l0' === end($arguments)) { return array( @@ -132,19 +132,38 @@ function ftp_rawlist($connection, $directory, $recursive = false) if ('/home/l3l1' === end($arguments) && '-al' === reset($arguments)) { return array( - "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .", - "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..", - "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename", - "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .htaccess", + "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .", + "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..", + "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename", + "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .htaccess", ); } if ('/home/l3l1' === end($arguments) && '-al' != reset($arguments)) { return array( - "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .", - "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..", - "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename", + "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .", + "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..", + "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename", + ); + } + + if ('/home/l3l2' === end($arguments)) + { + return array( + "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .", + "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..", + "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 a b c d -> žežulička", + "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 Žľuťoučký kůň.pdf", + ); + } + + if ('/home/l3l2/a b c d -> žežulička' === end($arguments)) + { + return array( + "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .", + "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..", + "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 do re mi.pdf", ); } From 462ac0429d7b74e8185b032a1ce670e94f255bec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Haso=C5=88?= Date: Mon, 22 Apr 2013 21:39:51 +0200 Subject: [PATCH 020/426] [Ftp] Implements ListKeysAware interface and added support for parsing recursive rawlist - significant speed up --- spec/Gaufrette/Adapter/FtpSpec.php | 40 +++++++++++ spec/Gaufrette/Adapter/functions.php | 39 ++++++++++- src/Gaufrette/Adapter/Ftp.php | 101 +++++++++++++++++++++++---- 3 files changed, 164 insertions(+), 16 deletions(-) diff --git a/spec/Gaufrette/Adapter/FtpSpec.php b/spec/Gaufrette/Adapter/FtpSpec.php index cd80962df..1d02e8af8 100644 --- a/spec/Gaufrette/Adapter/FtpSpec.php +++ b/spec/Gaufrette/Adapter/FtpSpec.php @@ -19,6 +19,11 @@ function it_is_adapter() $this->shouldHaveType('Gaufrette\Adapter'); } + function it_supports_native_list_keys() + { + $this->shouldHaveType('Gaufrette\Adapter\ListKeysAware'); + } + function it_checks_if_file_exists_for_absolute_path() { $this->exists('filename')->shouldReturn(true); @@ -91,6 +96,41 @@ function it_fetches_keys_with_spaces_and_unicode_chars() $this->keys()->shouldReturn(array('Žľuťoučký kůň.pdf', 'a b c d -> žežulička', 'a b c d -> žežulička/do re mi.pdf')); } + function it_fetches_keys_recursive() + { + $this->beConstructedWith('/home/l3l3', 'localhost'); + + $this->keys()->shouldReturn(array('filename', 'filename.exe', '.htaccess', 'aaa', 'www', 'aaa/filename', 'www/filename', 'www/some', 'www/some/otherfilename')); + } + + function it_lists_keys() + { + $this->listKeys()->shouldReturn(array( + 'keys' => array('filename', 'filename.exe', '.htaccess', 'aaa/filename'), + 'dirs' => array('aaa') + )); + + $this->listKeys('file')->shouldReturn(array( + 'keys' => array('filename', 'filename.exe'), + 'dirs' => array() + )); + + $this->listKeys('name')->shouldReturn(array( + 'keys' => array(), + 'dirs' => array() + )); + + $this->listKeys('aaa')->shouldReturn(array( + 'keys' => array('aaa/filename'), + 'dirs' => array('aaa') + )); + + $this->listKeys('aaa/')->shouldReturn(array( + 'keys' => array('aaa/filename'), + 'dirs' => array() + )); + } + function it_fetches_mtime() { $this->mtime('filename')->shouldReturn(strtotime('2010-10-10 23:10:10')); diff --git a/spec/Gaufrette/Adapter/functions.php b/spec/Gaufrette/Adapter/functions.php index d2712dae1..e89b03ba8 100644 --- a/spec/Gaufrette/Adapter/functions.php +++ b/spec/Gaufrette/Adapter/functions.php @@ -49,7 +49,7 @@ function ftp_fget($connection, &$fileResource, $path, $mode) function ftp_chdir($connection, $dirname) { - if (in_array($dirname, array('/home/l3l0', '/home/l3l0/aaa', '/home/l3l0/relative', '/home/l3l0/relative/some', '/home/l3l1', '/home/l3l2', '/home/l3l2/a b c d -> žežulička', 'C:\Ftp'))) { + if (in_array($dirname, array('/home/l3l0', '/home/l3l0/aaa', '/home/l3l0/relative', '/home/l3l0/relative/some', '/home/l3l1', '/home/l3l2', '/home/l3l2/a b c d -> žežulička', '/home/l3l3', 'C:\Ftp'))) { return true; } @@ -129,7 +129,7 @@ function ftp_rawlist($connection, $directory, $recursive = false) ); } - if ('/home/l3l1' === end($arguments) && '-al' === reset($arguments)) + if ('/home/l3l1' === end($arguments) && 0 === strpos(reset($arguments), '-al')) { return array( "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .", @@ -139,7 +139,7 @@ function ftp_rawlist($connection, $directory, $recursive = false) ); } - if ('/home/l3l1' === end($arguments) && '-al' != reset($arguments)) + if ('/home/l3l1' === end($arguments) && false === strpos(reset($arguments), '-al')) { return array( "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .", @@ -167,6 +167,39 @@ function ftp_rawlist($connection, $directory, $recursive = false) ); } + if ('/home/l3l3' === end($arguments) && '-alR' === reset($arguments)) + { + return array( + "/home/l3l3:", + "total: 12", + "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .", + "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..", + "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 aaa", + "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename", + "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename.exe", + "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .htaccess", + "drwxrwxrwx 1 vincent vincent 11 Jul 12 12:16 www", + "lrwxrwxrwx 1 vincent vincent 11 Jul 12 12:16 vendor -> bbb", + "", + "/home/l3l3/aaa:", + "total: 8", + "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename", + "", + "/home/l3l3/www:", + "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .", + "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..", + "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename", + "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 some", + "", + "/home/l3l3/www/some:", + "total 5", + "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .", + "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..", + "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 otherfilename", + ); + } + + // https://github.com/KnpLabs/Gaufrette/issues/147 if ('C:\Ftp' === end($arguments)) { diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index 4d43a2dc3..e7d021fa9 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -14,7 +14,8 @@ * @author Antoine Hérault */ class Ftp implements Adapter, - FileFactory + FileFactory, + ListKeysAware { protected $connection = null; protected $directory; @@ -139,7 +140,38 @@ public function keys() { $this->ensureDirectoryExists($this->directory, $this->create); - return $this->fetchKeys(); + $keys = $this->fetchKeys(); + + return $keys['keys']; + } + + /** + * {@inheritDoc} + */ + public function listKeys($prefix = '') + { + $this->ensureDirectoryExists($this->directory, $this->create); + + preg_match('/(.*?)[^\/]*$/', $prefix, $match); + $directory = rtrim($match[1], '/'); + + $keys = $this->fetchKeys($directory, false); + + if ($directory === $prefix) { + return $keys; + } + + $filteredKeys = array(); + foreach (array('keys', 'dirs') as $hash) { + $filteredKeys[$hash] = array(); + foreach ($keys[$hash] as $key) { + if (0 === strpos($key, $prefix)) { + $filteredKeys[$hash][] = $key; + } + } + } + + return $filteredKeys; } /** @@ -318,21 +350,64 @@ private function isDir($directory) return true; } - /** - * Fetch all Keys recursive - * - * @param string $directory - */ - private function fetchKeys($directory = '') + private function fetchKeys($directory = '', $onlyKeys = true) { - $items = $this->listDirectory($directory); + $directory = preg_replace('/^[\/]*([^\/].*)$/', '/$1', $directory); + + $lines = ftp_rawlist($this->getConnection(), '-alR '. $this->directory . $directory); + + if (false === $lines) { + return array(); + } + + $regexDir = '/'.preg_quote($this->directory . $directory, '/').'\/?(.+):$/u'; + $regexItem = '/^(?:([d\-\d])\S+)\s+\S+(?:(?:\s+\S+){5})?\s+(\S+)\s+(.+?)$/'; + + $prevLine = null; + $directories = array(); + $keys = array('keys' => array(), 'dirs' => array()); + + foreach ((array) $lines as $line) { + if ('' === $prevLine && preg_match($regexDir, $line, $match)) { + $directory = $match[1]; + unset($directories[$directory]); + if ($onlyKeys) { + $keys = array( + 'keys' => array_merge($keys['keys'], $keys['dirs']), + 'dirs' => array() + ); + } + } elseif (preg_match($regexItem, $line, $tokens)) { + $name = $tokens[3]; + + if ('.' === $name || '..' === $name) { + continue; + } + + $path = ltrim($directory . '/' . $name, '/'); + + if ('d' === $tokens[1] || '' === $tokens[2]) { + $keys['dirs'][] = $path; + $directories[$path] = true; + } else { + $keys['keys'][] = $path; + } + } + $prevLine = $line; + } + + if ($onlyKeys) { + $keys = array( + 'keys' => array_merge($keys['keys'], $keys['dirs']), + 'dirs' => array() + ); + } - $keys = $items['dirs']; - foreach ($items['dirs'] as $dir) { - $keys = array_merge($keys, $this->fetchKeys($dir)); + foreach (array_keys($directories) as $directory) { + $keys = array_merge_recursive($keys, $this->fetchKeys($directory, $onlyKeys)); } - return array_merge($items['keys'], $keys); + return $keys; } /** From 3216bd564e2d0d81b8140a042d5f905869623a05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Czy=C5=BCewski?= Date: Sat, 29 Jun 2013 12:06:59 +0200 Subject: [PATCH 021/426] Ensuring single File instance for real file, when using createFile() --- src/Gaufrette/Filesystem.php | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/Gaufrette/Filesystem.php b/src/Gaufrette/Filesystem.php index 42928615e..cde5df182 100644 --- a/src/Gaufrette/Filesystem.php +++ b/src/Gaufrette/Filesystem.php @@ -13,6 +13,13 @@ class Filesystem { protected $adapter; + + /** + * Contains File objects created with $this->createFile() method + * + * @var array + */ + protected $fileRegister = array(); /** * Constructor @@ -68,6 +75,11 @@ public function rename($sourceKey, $targetKey) if (! $this->adapter->rename($sourceKey, $targetKey)) { throw new \RuntimeException(sprintf('Could not rename the "%s" key to "%s".', $sourceKey, $targetKey)); } + + if(array_key_exists($sourceKey, $this->fileRegister)) { + $this->fileRegister[$targetKey] = $this->fileRegister[$sourceKey]; + unset($this->fileRegister[$sourceKey]); + } return true; } @@ -151,6 +163,9 @@ public function delete($key) $this->assertHasFile($key); if ($this->adapter->delete($key)) { + if(array_key_exists($key, $this->fileRegister)) { + unset($this->fileRegister[$key]); + } return true; } @@ -251,11 +266,16 @@ public function createStream($key) */ public function createFile($key) { - if ($this->adapter instanceof Adapter\FileFactory) { - return $this->adapter->createFile($key, $this); + if(array_key_exists($key, $this->fileRegister)) { + return $this->fileRegister[$key]; + } else { + if ($this->adapter instanceof Adapter\FileFactory) { + $this->fileRegister[$key] = $this->adapter->createFile($key, $this); + } else { + $this->fileRegister[$key] = new File($key, $this); + } + return $this->fileRegister[$key]; } - - return new File($key, $this); } /** @@ -268,4 +288,4 @@ private function assertHasFile($key) throw new Exception\FileNotFound($key); } } -} +} \ No newline at end of file From 8547670546c565deea5f6cf6c6e69f64dca15cdf Mon Sep 17 00:00:00 2001 From: Christoph Date: Mon, 1 Jul 2013 03:12:41 +0200 Subject: [PATCH 022/426] fixed size detection Without the explicit charset, PHP detects UTF BOM when there are none (like in the middle of files). --- src/Gaufrette/Util/Size.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Gaufrette/Util/Size.php b/src/Gaufrette/Util/Size.php index 635c01459..a6cb81376 100644 --- a/src/Gaufrette/Util/Size.php +++ b/src/Gaufrette/Util/Size.php @@ -20,6 +20,8 @@ class Size */ public static function fromContent($content) { - return mb_strlen($content); + // Make sure to get the real length in byte and not + // accidentally mistake some bytes as a UTF BOM. + return mb_strlen($content, '8bit'); } } From 6e980d2a9e458a5230011e0ca7b7af04d3c27f5b Mon Sep 17 00:00:00 2001 From: Machiel Molenaar Date: Tue, 2 Jul 2013 18:30:17 +0200 Subject: [PATCH 023/426] Fixed namespacing of OpenCloud Exceptions --- src/Gaufrette/Adapter/OpenCloud.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Gaufrette/Adapter/OpenCloud.php b/src/Gaufrette/Adapter/OpenCloud.php index 4e1016571..343c06631 100644 --- a/src/Gaufrette/Adapter/OpenCloud.php +++ b/src/Gaufrette/Adapter/OpenCloud.php @@ -5,8 +5,8 @@ use Gaufrette\Adapter; use OpenCloud\ObjectStore\Container; use OpenCloud\ObjectStore\Service; -use OpenCloud\Base\Exceptions\CreateUpdateError; -use OpenCloud\Base\Exceptions\ObjFetchError; +use OpenCloud\Common\Exceptions\CreateUpdateError; +use OpenCloud\Common\Exceptions\ObjFetchError; /** * OpenCloud adapter From 0ee022e6c03b0a07ddf0ab8ace452f8ac04a78c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Czy=C5=BCewski?= Date: Wed, 3 Jul 2013 16:15:22 +0200 Subject: [PATCH 024/426] Filesystem->createFile code refactoring --- src/Gaufrette/Filesystem.php | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Gaufrette/Filesystem.php b/src/Gaufrette/Filesystem.php index cde5df182..b4384b851 100644 --- a/src/Gaufrette/Filesystem.php +++ b/src/Gaufrette/Filesystem.php @@ -266,16 +266,15 @@ public function createStream($key) */ public function createFile($key) { - if(array_key_exists($key, $this->fileRegister)) { - return $this->fileRegister[$key]; - } else { + if(false === array_key_exists($key, $this->fileRegister)) { if ($this->adapter instanceof Adapter\FileFactory) { $this->fileRegister[$key] = $this->adapter->createFile($key, $this); } else { $this->fileRegister[$key] = new File($key, $this); } - return $this->fileRegister[$key]; } + + return $this->fileRegister[$key]; } /** From d102dfd69debcf252b63c81171a189a63f7ccbae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konrad=20Podg=C3=B3rski?= Date: Mon, 22 Jul 2013 16:19:35 +0200 Subject: [PATCH 025/426] Handle empty key in Filesystem --- src/Gaufrette/Filesystem.php | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/Gaufrette/Filesystem.php b/src/Gaufrette/Filesystem.php index 42928615e..cdedf94f6 100644 --- a/src/Gaufrette/Filesystem.php +++ b/src/Gaufrette/Filesystem.php @@ -259,12 +259,18 @@ public function createFile($key) } /** - * @param $key - * @throws Exception\FileNotFound + * Checks if matching file by given key exists in the filesystem + * + * Key must be non empty string, otherwise it will throw Exception\FileNotFound + * {@see http://php.net/manual/en/function.empty.php} + * + * @param string $key + * + * @throws Exception\FileNotFound when sourceKey does not exist */ private function assertHasFile($key) { - if (! $this->has($key)) { + if (! empty($key) && ! $this->has($key)) { throw new Exception\FileNotFound($key); } } From cb4d198a4cbd1a6f9d038ae302680ffec00cc2a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Konrad=20Podg=C3=B3rski?= Date: Wed, 24 Jul 2013 16:50:14 +0200 Subject: [PATCH 026/426] Added apt-get update before running install to fix outdated packages --- bin/configure_test_env.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/bin/configure_test_env.sh b/bin/configure_test_env.sh index c525cfa24..d199aef11 100755 --- a/bin/configure_test_env.sh +++ b/bin/configure_test_env.sh @@ -1,5 +1,6 @@ #!/bin/bash +sudo apt-get update -qq sudo apt-get install -qq libssh2-1-dev libssh2-php pecl install -f mongo touch .interactive From 806efdd930f84555ee1e73e6737825e37db5123f Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Fri, 26 Jul 2013 14:31:32 -0700 Subject: [PATCH 027/426] Adding an Amazon S3 adapter that uses the AWS SDK for PHP 2 The new SDK detects content-types by default. Closes #189 Allows the setting of a custom Content-Type. Closes #187 Allows a method to retrieve an object URL. Closes #149 Use a private canned ACL by default. Closes #132 --- README.markdown | 33 +- composer.json | 4 +- composer.lock | 316 +++++++++++++----- src/Gaufrette/Adapter/AclAwareAmazonS3.php | 6 +- src/Gaufrette/Adapter/AmazonS3.php | 8 +- src/Gaufrette/Adapter/AwsS3.php | 234 +++++++++++++ .../Functional/Adapter/AwsS3Test.php | 91 +++++ 7 files changed, 588 insertions(+), 104 deletions(-) create mode 100644 src/Gaufrette/Adapter/AwsS3.php create mode 100644 tests/Gaufrette/Functional/Adapter/AwsS3Test.php diff --git a/README.markdown b/README.markdown index 340764a64..cec928bc3 100644 --- a/README.markdown +++ b/README.markdown @@ -100,14 +100,19 @@ The third parameter of the cache adapter is the time to live of the cache. Using Amazon S3 --------------- -You will need to specify a CA certificate to be able to talk to Amazon servers -in https. You can use the one which is shipped with the SDK by defining before -creating the ``\AmazonS3`` object: +When using the legacy Amazon S3 adapters, you will need to specify a CA +certificate to be able to talk to Amazon servers in https. You can use +the one which is shipped with the SDK by defining before creating the +``\AmazonS3`` object: ```php define("AWS_CERTIFICATE_AUTHORITY", true); ``` +Specifying a custom CA certificate is not required when using the +`Gaufrette\Adapter\AmazonS3` adapter because it uses the newest version of the +AWS SDK for PHP. + Using OpenCloud --------------- To use the OpenCloud adapter you will need to create a connection using the [OpenCloud SDK](https://github.com/rackspace/php-opencloud). @@ -183,21 +188,17 @@ In your Symfony2 project, add to ``deps``: # if you want to use Amazon S3 [aws-sdk] - git=https://github.com/amazonwebservices/aws-sdk-for-php -``` - -and to ``app/autoload.php``, at the end: - -```php -// AWS SDK needs a special autoloader -require_once __DIR__.'/../vendor/aws-sdk/sdk.class.php'; + git=https://github.com/aws/aws-sdk-php ``` And then, you can simply add them as services of your dependency injection container. As an example, here is services declaration to use Amazon S3: ```xml - + %acme.aws_key% %acme.aws_secret_key% @@ -214,14 +215,6 @@ As an example, here is services declaration to use Amazon S3: ``` -Don't forget to set the constant to tell the AWS SDK to use its CA cert (somewhere -that will be executed before creating the ``\AmazonS3`` object): -```php -define("AWS_CERTIFICATE_AUTHORITY", true); -$fs = $container->get('acme.fs'); -// use $fs -``` - Streaming Files --------------- diff --git a/composer.json b/composer.json index 6078e46b9..c583589bd 100644 --- a/composer.json +++ b/composer.json @@ -41,6 +41,7 @@ "php": ">=5.3.2" }, "require-dev": { + "aws/aws-sdk-php": "~2", "amazonwebservices/aws-sdk-for-php": "1.5.*", "rackspace/php-opencloud" : "dev-master", "phpspec/phpspec": "2.0.*", @@ -55,7 +56,8 @@ "knplabs/knp-gaufrette-bundle": "to use with Symfony2", "dropbox-php/dropbox-php": "to use the Dropbox adapter", "rackspace/php-opencloud" : "to use Opencloud adapter", - "amazonwebservices/aws-sdk-for-php": "to use the Amazon S3 adapter", + "aws/aws-sdk-php": "to use the Amazon S3 adapter", + "amazonwebservices/aws-sdk-for-php": "to use the legacy Amazon S3 adapters", "phpseclib/phpseclib": "to use the SFTP", "doctrine/dbal": "to use the Doctrine DBAL adapter", "ext-zip": "to use the Zip adapter", diff --git a/composer.lock b/composer.lock index 088b55124..8ace9a55d 100644 --- a/composer.lock +++ b/composer.lock @@ -3,7 +3,7 @@ "This file locks the dependencies of your project to a known state", "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" ], - "hash": "6c9e68f35722f69905b4c179167e6302", + "hash": "d04605350d94fc82797b0ed9bf72dd7d", "packages": [ ], @@ -58,18 +58,84 @@ ], "time": "2012-11-26 23:59:55" }, + { + "name": "aws/aws-sdk-php", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/aws/aws-sdk-php.git", + "reference": "cd986623f0c5631337a30159d0d1c8953602c933" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/cd986623f0c5631337a30159d0d1c8953602c933", + "reference": "cd986623f0c5631337a30159d0d1c8953602c933", + "shasum": "" + }, + "require": { + "guzzle/guzzle": "~3.7.0", + "php": ">=5.3.3" + }, + "require-dev": { + "doctrine/cache": "~1.0", + "ext-openssl": "*", + "monolog/monolog": "1.4.*", + "phpunit/phpunit": "3.7.*", + "symfony/class-loader": "2.*", + "symfony/yaml": "2.*" + }, + "suggest": { + "doctrine/cache": "Adds support for caching of credentials and responses", + "ext-apc": "Allows service description opcode caching, request and response caching, and credentials caching", + "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", + "monolog/monolog": "Adds support for logging HTTP requests and responses", + "symfony/yaml": "Eases the ability to write manifests for creating jobs in AWS Import/Export" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.4.x-dev" + } + }, + "autoload": { + "psr-0": { + "Aws": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Amazon Web Services", + "homepage": "http://aws.amazon.com" + } + ], + "description": "AWS SDK for PHP", + "homepage": "http://aws.amazon.com/sdkforphp2", + "keywords": [ + "amazon", + "aws", + "dynamodb", + "ec2", + "s3", + "sdk" + ], + "time": "2013-07-22 21:22:56" + }, { "name": "doctrine/annotations", - "version": "v1.1.1", + "version": "v1.1.2", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "v1.1.1" + "reference": "v1.1.2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/v1.1.1", - "reference": "v1.1.1", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/v1.1.2", + "reference": "v1.1.2", "shasum": "" }, "require": { @@ -127,7 +193,7 @@ "docblock", "parser" ], - "time": "2013-04-20 08:30:17" + "time": "2013-06-16 21:33:03" }, { "name": "doctrine/cache", @@ -135,12 +201,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/cache.git", - "reference": "89493d2c6e1362f581f9de1c1871cc52eb29c030" + "reference": "dc3438804c6303f1b5892f184a79aa8e3ce5d81d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/89493d2c6e1362f581f9de1c1871cc52eb29c030", - "reference": "89493d2c6e1362f581f9de1c1871cc52eb29c030", + "url": "https://api.github.com/repos/doctrine/cache/zipball/dc3438804c6303f1b5892f184a79aa8e3ce5d81d", + "reference": "dc3438804c6303f1b5892f184a79aa8e3ce5d81d", "shasum": "" }, "require": { @@ -196,7 +262,7 @@ "cache", "caching" ], - "time": "2013-06-07 14:54:47" + "time": "2013-07-04 15:48:06" }, { "name": "doctrine/collections", @@ -204,12 +270,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "3db3ab843ff76774bee4679d4cb3a10cffb0a935" + "reference": "623caf42551b416460df786154d89bc50e3d1798" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/3db3ab843ff76774bee4679d4cb3a10cffb0a935", - "reference": "3db3ab843ff76774bee4679d4cb3a10cffb0a935", + "url": "https://api.github.com/repos/doctrine/collections/zipball/623caf42551b416460df786154d89bc50e3d1798", + "reference": "623caf42551b416460df786154d89bc50e3d1798", "shasum": "" }, "require": { @@ -263,7 +329,7 @@ "collections", "iterator" ], - "time": "2013-05-26 05:21:22" + "time": "2013-07-23 08:27:26" }, { "name": "doctrine/common", @@ -271,12 +337,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/common.git", - "reference": "c4255b9fbd63ee1fe52697839318af5937fced9b" + "reference": "6a39bb947b20d12a16820f2709509e41e28af005" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/common/zipball/c4255b9fbd63ee1fe52697839318af5937fced9b", - "reference": "c4255b9fbd63ee1fe52697839318af5937fced9b", + "url": "https://api.github.com/repos/doctrine/common/zipball/6a39bb947b20d12a16820f2709509e41e28af005", + "reference": "6a39bb947b20d12a16820f2709509e41e28af005", "shasum": "" }, "require": { @@ -322,7 +388,7 @@ "email": "kontakt@beberlei.de" }, { - "name": "Johannes M. Schmitt", + "name": "Johannes Schmitt", "email": "schmittjoh@gmail.com", "homepage": "http://jmsyst.com", "role": "Developer of wrapped JMSSerializerBundle" @@ -337,7 +403,7 @@ "persistence", "spl" ], - "time": "2013-06-21 12:11:28" + "time": "2013-07-02 11:26:20" }, { "name": "doctrine/dbal", @@ -345,12 +411,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "3253f7a1c13a2177d925bd0f6ee8387c5dd9c57a" + "reference": "2.4.0-RC2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/3253f7a1c13a2177d925bd0f6ee8387c5dd9c57a", - "reference": "3253f7a1c13a2177d925bd0f6ee8387c5dd9c57a", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/2.4.0-RC2", + "reference": "2.4.0-RC2", "shasum": "" }, "require": { @@ -569,7 +635,7 @@ { "name": "Evert Pot", "email": "evert@rooftopsolutions.nl", - "homepage": "http://www.rooftopsolutions.nl/", + "homepage": "http://evertpot.com/", "role": "Developer" }, { @@ -594,6 +660,98 @@ ], "time": "2013-03-29 22:25:23" }, + { + "name": "guzzle/guzzle", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/guzzle/guzzle.git", + "reference": "9faeeaf4baa2c9e1c1c4848eea62818eeaa54f02" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/9faeeaf4baa2c9e1c1c4848eea62818eeaa54f02", + "reference": "9faeeaf4baa2c9e1c1c4848eea62818eeaa54f02", + "shasum": "" + }, + "require": { + "ext-curl": "*", + "php": ">=5.3.2", + "symfony/event-dispatcher": ">=2.1" + }, + "replace": { + "guzzle/batch": "self.version", + "guzzle/cache": "self.version", + "guzzle/common": "self.version", + "guzzle/http": "self.version", + "guzzle/inflection": "self.version", + "guzzle/iterator": "self.version", + "guzzle/log": "self.version", + "guzzle/parser": "self.version", + "guzzle/plugin": "self.version", + "guzzle/plugin-async": "self.version", + "guzzle/plugin-backoff": "self.version", + "guzzle/plugin-cache": "self.version", + "guzzle/plugin-cookie": "self.version", + "guzzle/plugin-curlauth": "self.version", + "guzzle/plugin-error-response": "self.version", + "guzzle/plugin-history": "self.version", + "guzzle/plugin-log": "self.version", + "guzzle/plugin-md5": "self.version", + "guzzle/plugin-mock": "self.version", + "guzzle/plugin-oauth": "self.version", + "guzzle/service": "self.version", + "guzzle/stream": "self.version" + }, + "require-dev": { + "doctrine/cache": "*", + "monolog/monolog": "1.*", + "phpunit/phpunit": "3.7.*", + "psr/log": "1.0.*", + "symfony/class-loader": "*", + "zendframework/zend-cache": "2.0.*", + "zendframework/zend-log": "2.0.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.7-dev" + } + }, + "autoload": { + "psr-0": { + "Guzzle\\Tests": "tests/", + "Guzzle": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Michael Dowling", + "email": "mtdowling@gmail.com", + "homepage": "https://github.com/mtdowling" + }, + { + "name": "Guzzle Community", + "homepage": "https://github.com/guzzle/guzzle/contributors" + } + ], + "description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients", + "homepage": "http://guzzlephp.org/", + "keywords": [ + "client", + "curl", + "framework", + "http", + "http client", + "rest", + "web service" + ], + "time": "2013-07-18 20:50:44" + }, { "name": "herzult/php-ssh", "version": "dev-master", @@ -649,12 +807,12 @@ "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "f102ce62e202f0d3c26e862755c872696bbd2cf0" + "reference": "bf6da217c19514506060d98797a53dc1a78c807e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/f102ce62e202f0d3c26e862755c872696bbd2cf0", - "reference": "f102ce62e202f0d3c26e862755c872696bbd2cf0", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/bf6da217c19514506060d98797a53dc1a78c807e", + "reference": "bf6da217c19514506060d98797a53dc1a78c807e", "shasum": "" }, "require": { @@ -715,7 +873,7 @@ "x.509", "x509" ], - "time": "2013-06-26 06:15:04" + "time": "2013-07-23 04:53:45" }, { "name": "phpspec/php-diff", @@ -756,22 +914,22 @@ "source": { "type": "git", "url": "https://github.com/phpspec/phpspec.git", - "reference": "56819ea76a8d59e615e0a9c099451c012338a72d" + "reference": "00cbb43e1db5e015330f9f754a239459b99af1de" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/phpspec/zipball/56819ea76a8d59e615e0a9c099451c012338a72d", - "reference": "56819ea76a8d59e615e0a9c099451c012338a72d", + "url": "https://api.github.com/repos/phpspec/phpspec/zipball/00cbb43e1db5e015330f9f754a239459b99af1de", + "reference": "00cbb43e1db5e015330f9f754a239459b99af1de", "shasum": "" }, "require": { "php": ">=5.3.3", - "phpspec/php-diff": ">=1.0,<1.1", - "phpspec/prophecy": ">=1.0.2,<1.1", - "symfony/console": ">=2.1,<3.0", - "symfony/event-dispatcher": ">=2.1,<3.0", - "symfony/finder": ">=2.1,<3.0", - "symfony/yaml": ">=2.1,<3.0" + "phpspec/php-diff": "~1.0.0", + "phpspec/prophecy": "~1.0.2", + "symfony/console": "~2.1", + "symfony/event-dispatcher": "~2.1", + "symfony/finder": "~2.1", + "symfony/yaml": "~2.1" }, "suggest": { "whatthejeff/nyancat-scoreboard": "~1.1 – Enables the Nyan Cat formatter" @@ -816,7 +974,7 @@ "testing", "tests" ], - "time": "2013-06-24 08:45:19" + "time": "2013-07-15 16:25:26" }, { "name": "phpspec/prophecy", @@ -824,12 +982,12 @@ "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "593c8c679369c0ea49616cd812a0fb2984084757" + "reference": "27f73e6ed956c401f9464b9215d5000ce82c9d42" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/593c8c679369c0ea49616cd812a0fb2984084757", - "reference": "593c8c679369c0ea49616cd812a0fb2984084757", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/27f73e6ed956c401f9464b9215d5000ce82c9d42", + "reference": "27f73e6ed956c401f9464b9215d5000ce82c9d42", "shasum": "" }, "require-dev": { @@ -871,7 +1029,7 @@ "spy", "stub" ], - "time": "2013-06-19 15:13:15" + "time": "2013-07-04 22:33:32" }, { "name": "phpunit/php-code-coverage", @@ -879,12 +1037,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "58e6aa238f270d67187d16a50e14503bd9d02bee" + "reference": "d274ffb5a9899ab71c769924071fb8aa09852fb6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/58e6aa238f270d67187d16a50e14503bd9d02bee", - "reference": "58e6aa238f270d67187d16a50e14503bd9d02bee", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/d274ffb5a9899ab71c769924071fb8aa09852fb6", + "reference": "d274ffb5a9899ab71c769924071fb8aa09852fb6", "shasum": "" }, "require": { @@ -932,7 +1090,7 @@ "testing", "xunit" ], - "time": "2013-06-24 11:56:39" + "time": "2013-07-13 13:31:16" }, { "name": "phpunit/php-file-iterator", @@ -1118,12 +1276,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "30110580b640440f6442a58322ee657216d2d8ba" + "reference": "3.7.22" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/30110580b640440f6442a58322ee657216d2d8ba", - "reference": "30110580b640440f6442a58322ee657216d2d8ba", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3.7.22", + "reference": "3.7.22", "shasum": "" }, "require": { @@ -1132,12 +1290,12 @@ "ext-reflection": "*", "ext-spl": "*", "php": ">=5.3.3", - "phpunit/php-code-coverage": ">=1.2.1,<1.3", + "phpunit/php-code-coverage": "~1.2.1", "phpunit/php-file-iterator": ">=1.3.1", "phpunit/php-text-template": ">=1.1.1", - "phpunit/php-timer": ">=1.0.2,<1.1", - "phpunit/phpunit-mock-objects": ">=1.2,<1.3", - "symfony/yaml": ">=2.0,<3.0" + "phpunit/php-timer": "~1.0.2", + "phpunit/phpunit-mock-objects": "~1.2.0", + "symfony/yaml": "~2.0" }, "require-dev": { "pear-pear/pear": "1.9.4" @@ -1184,7 +1342,7 @@ "testing", "xunit" ], - "time": "2013-06-23 07:31:03" + "time": "2013-07-06 06:29:15" }, { "name": "phpunit/phpunit-mock-objects", @@ -1192,12 +1350,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "977c5327c5f9e2a0f08728a59389ee604e5b2f47" + "reference": "3e40f3b3f18c044a24688fe406440d7fd537744a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/977c5327c5f9e2a0f08728a59389ee604e5b2f47", - "reference": "977c5327c5f9e2a0f08728a59389ee604e5b2f47", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/3e40f3b3f18c044a24688fe406440d7fd537744a", + "reference": "3e40f3b3f18c044a24688fe406440d7fd537744a", "shasum": "" }, "require": { @@ -1242,7 +1400,7 @@ "mock", "xunit" ], - "time": "2013-06-23 05:41:31" + "time": "2013-07-23 04:42:59" }, { "name": "rackspace/php-cloudfiles", @@ -1272,12 +1430,12 @@ "source": { "type": "git", "url": "https://github.com/rackspace/php-opencloud.git", - "reference": "d6218f4a8bfaed12b0a711246b7d51a13e27b9a0" + "reference": "80b4929d600a6638b4f31d5a8f6a455d2ac5f645" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rackspace/php-opencloud/zipball/d6218f4a8bfaed12b0a711246b7d51a13e27b9a0", - "reference": "d6218f4a8bfaed12b0a711246b7d51a13e27b9a0", + "url": "https://api.github.com/repos/rackspace/php-opencloud/zipball/80b4929d600a6638b4f31d5a8f6a455d2ac5f645", + "reference": "80b4929d600a6638b4f31d5a8f6a455d2ac5f645", "shasum": "" }, "require": { @@ -1309,7 +1467,7 @@ "cloudfiles", "opencloud" ], - "time": "2013-06-22 19:45:44" + "time": "2013-07-23 18:28:55" }, { "name": "symfony/console", @@ -1318,19 +1476,19 @@ "source": { "type": "git", "url": "https://github.com/symfony/Console.git", - "reference": "b68fd0262ebae63f796d188f48eaa470044861fd" + "reference": "470446b34c029c7cd1933a55c9bdd887b9f5ec0d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Console/zipball/b68fd0262ebae63f796d188f48eaa470044861fd", - "reference": "b68fd0262ebae63f796d188f48eaa470044861fd", + "url": "https://api.github.com/repos/symfony/Console/zipball/470446b34c029c7cd1933a55c9bdd887b9f5ec0d", + "reference": "470446b34c029c7cd1933a55c9bdd887b9f5ec0d", "shasum": "" }, "require": { "php": ">=5.3.3" }, "require-dev": { - "symfony/event-dispatcher": ">=2.1,<3.0" + "symfony/event-dispatcher": "~2.1" }, "suggest": { "symfony/event-dispatcher": "" @@ -1362,7 +1520,7 @@ ], "description": "Symfony Console Component", "homepage": "http://symfony.com", - "time": "2013-06-23 08:16:33" + "time": "2013-07-21 20:19:01" }, { "name": "symfony/event-dispatcher", @@ -1371,19 +1529,19 @@ "source": { "type": "git", "url": "https://github.com/symfony/EventDispatcher.git", - "reference": "4b34aad44d0a2d91f301ee967151ce0980f39930" + "reference": "b8bca82c9347de810aa00898684adbf9b601eb5d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/4b34aad44d0a2d91f301ee967151ce0980f39930", - "reference": "4b34aad44d0a2d91f301ee967151ce0980f39930", + "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/b8bca82c9347de810aa00898684adbf9b601eb5d", + "reference": "b8bca82c9347de810aa00898684adbf9b601eb5d", "shasum": "" }, "require": { "php": ">=5.3.3" }, "require-dev": { - "symfony/dependency-injection": ">=2.0,<3.0" + "symfony/dependency-injection": "~2.0" }, "suggest": { "symfony/dependency-injection": "", @@ -1416,7 +1574,7 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "http://symfony.com", - "time": "2013-05-16 07:54:39" + "time": "2013-07-21 20:19:01" }, { "name": "symfony/finder", @@ -1425,12 +1583,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/Finder.git", - "reference": "d8da924d4f7d9f325ebd72feefee40c17a3e9104" + "reference": "6b6564a27b23daa5fa3dc12db7a4b0eeabf13f1f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Finder/zipball/d8da924d4f7d9f325ebd72feefee40c17a3e9104", - "reference": "d8da924d4f7d9f325ebd72feefee40c17a3e9104", + "url": "https://api.github.com/repos/symfony/Finder/zipball/6b6564a27b23daa5fa3dc12db7a4b0eeabf13f1f", + "reference": "6b6564a27b23daa5fa3dc12db7a4b0eeabf13f1f", "shasum": "" }, "require": { @@ -1463,7 +1621,7 @@ ], "description": "Symfony Finder Component", "homepage": "http://symfony.com", - "time": "2013-06-23 08:16:33" + "time": "2013-07-21 20:19:01" }, { "name": "symfony/yaml", @@ -1472,12 +1630,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/Yaml.git", - "reference": "82100c7732c95452702d11433aad50988700d36c" + "reference": "4d80b4f4c447e8ad7cab8b3afb0b51aad0431f7f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Yaml/zipball/82100c7732c95452702d11433aad50988700d36c", - "reference": "82100c7732c95452702d11433aad50988700d36c", + "url": "https://api.github.com/repos/symfony/Yaml/zipball/4d80b4f4c447e8ad7cab8b3afb0b51aad0431f7f", + "reference": "4d80b4f4c447e8ad7cab8b3afb0b51aad0431f7f", "shasum": "" }, "require": { @@ -1510,7 +1668,7 @@ ], "description": "Symfony Yaml Component", "homepage": "http://symfony.com", - "time": "2013-05-16 07:54:39" + "time": "2013-07-21 20:19:01" } ], "aliases": [ diff --git a/src/Gaufrette/Adapter/AclAwareAmazonS3.php b/src/Gaufrette/Adapter/AclAwareAmazonS3.php index 2873edfd6..ab0fdaccf 100644 --- a/src/Gaufrette/Adapter/AclAwareAmazonS3.php +++ b/src/Gaufrette/Adapter/AclAwareAmazonS3.php @@ -6,7 +6,11 @@ use Gaufrette\Adapter; /** - * Makes the AmazonS3 adapter ACL aware. + * Makes the AmazonS3 adapter ACL aware. Uses the AWS SDK for PHP v1.x + * + * See the AwsS3 adapter for using the AWS SDK for PHP v2.x. There is + * no distinction in the AwsS3 adapter between an ACL aware adapter + * and regular adapter. * * @package Gaufrette * @author Johannes M. Schmitt diff --git a/src/Gaufrette/Adapter/AmazonS3.php b/src/Gaufrette/Adapter/AmazonS3.php index b123ac16c..2543cc01b 100644 --- a/src/Gaufrette/Adapter/AmazonS3.php +++ b/src/Gaufrette/Adapter/AmazonS3.php @@ -6,7 +6,9 @@ use Gaufrette\Adapter; /** - * Amazon S3 adapter + * Amazon S3 adapter using the AWS SDK for PHP v1.x. + * + * See the AwsS3 adapter for using the AWS SDK for PHP v2.x. * * @package Gaufrette * @author Antoine Hérault @@ -31,7 +33,7 @@ public function __construct(AmazonClient $service, $bucket, $options = array()) ); } - /** + /** * Set the acl used when writing files * * @param string $acl @@ -43,7 +45,7 @@ public function setAcl($acl) /** * Get the acl used when writing files - * + * * @return string */ public function getAcl() diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php new file mode 100644 index 000000000..e45765247 --- /dev/null +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -0,0 +1,234 @@ + + */ +class AwsS3 implements Adapter, + MetadataSupporter, + ListKeysAware +{ + protected $service; + protected $bucket; + protected $options; + protected $bucketExists; + protected $metadata = array(); + + public function __construct(S3Client $service, $bucket, array $options = array()) + { + $this->service = $service; + $this->bucket = $bucket; + $this->options = array_replace(array('create' => false), $options); + } + + /** + * Gets the publicly accessible URL of an Amazon S3 object + * + * @param string $key Object key + * @param array $options Associative array of options used to buld the URL + * - expires: The time at which the URL should expire + * represented as a UNIX timestamp + * - Any options available in the Amazon S3 GetObject + * operation may be specified. + * @return string + */ + public function getUrl($key, array $options = array()) + { + return $this->service->getObjectUrl( + $this->bucket, + $key, + isset($options['expires']) ? $options['expires'] : null, + $options + ); + } + + /** + * {@inheritDoc} + */ + public function setMetadata($key, $metadata) + { + $this->metadata[$key] = $metadata; + } + + /** + * {@inheritDoc} + */ + public function getMetadata($key) + { + return isset($this->metadata[$key]) ? $this->metadata[$key] : array(); + } + + /** + * {@inheritDoc} + */ + public function read($key) + { + $this->ensureBucketExists($key); + $options = $this->getOptions($key); + + try { + return (string) $this->service->getObject($options)->get('Body'); + } catch (\Exception $e) { + return false; + } + } + + /** + * {@inheritDoc} + */ + public function rename($sourceKey, $targetKey) + { + $this->ensureBucketExists($targetKey); + $options = $this->getOptions($targetKey, array('CopySource' => $sourceKey)); + + try { + $this->service->copyObject($options); + return true; + } catch (\Exception $e) { + return false; + } + } + + /** + * {@inheritDoc} + */ + public function write($key, $content) + { + $this->ensureBucketExists($key); + $options = $this->getOptions($key, array('Body' => $content)); + + try { + $this->service->putObject($options); + return strlen($content); + } catch (\Exception $e) { + return false; + } + } + + /** + * {@inheritDoc} + */ + public function exists($key) + { + return $this->service->doesObjectExist($this->bucket, $key); + } + + /** + * {@inheritDoc} + */ + public function mtime($key) + { + try { + $result = $this->service->headObject($this->getOptions($key)); + return strtotime($result['LastModified']); + } catch (\Exception $e) { + return false; + } + } + + /** + * {@inheritDoc} + */ + public function keys() + { + return $this->listKeys(); + } + + /** + * {@inheritdoc} + */ + public function listKeys($prefix = '') + { + $options = array('Bucket' => $this->bucket); + if ((string) $prefix != '') { + $options['Prefix'] = $prefix; + } + + $keys = array(); + $iter = $this->service->getIterator('ListObjects', $options); + foreach ($iter as $file) { + $keys[] = $file['Key']; + } + + return $keys; + } + + /** + * {@inheritDoc} + */ + public function delete($key) + { + try { + $this->service->deleteObject($this->getOptions($key)); + return true; + } catch (\Exception $e) { + return false; + } + } + + /** + * {@inheritDoc} + */ + public function isDirectory($key) + { + $result = $this->service->listObjects(array( + 'Bucket' => $this->bucket, + 'Prefix' => rtrim($key, '/') . '/', + 'MaxKeys' => 1 + )); + + return count($result['Contents']) > 0; + } + + /** + * Ensures the specified bucket exists. If the bucket does not exists + * and the create option is set to true, it will try to create the + * bucket. The bucket is created using the same region as the supplied + * client object. + * + * @throws \RuntimeException if the bucket does not exists or could not be + * created + */ + private function ensureBucketExists() + { + if ($this->bucketExists) { + return true; + } + + if ($this->bucketExists = $this->service->doesBucketExist($this->bucket)) { + return true; + } + + if (!$this->options['create']) { + throw new \RuntimeException(sprintf( + 'The configured bucket "%s" does not exist.', + $this->bucket + )); + } + + $options = array('Bucket' => $this->bucket); + if ($this->service->getRegion() != 'us-east-1') { + $options['LocationConstraint'] = $this->service->getRegion(); + } + + $this->service->createBucket($options); + $this->bucketExists = true; + + return true; + } + + private function getOptions($key, array $options = array()) + { + $options['Bucket'] = $this->bucket; + $options['Key'] = $key; + + return $options + $this->getMetadata($key); + } +} diff --git a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php new file mode 100644 index 000000000..51bc711b5 --- /dev/null +++ b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php @@ -0,0 +1,91 @@ + 'foo', + 'secret' => 'bar' + )); + } + + public function testCreatesBucketIfMissing() + { + $mock = new MockPlugin(array( + new Response(404), // Head bucket response + new Response(200), // Create bucket response + new Response(200, array(), 'foo') // Get object response + )); + $client = $this->getClient(); + $client->addSubscriber($mock); + $adapter = new AmazonS3($client, 'bucket', array('create' => true)); + $this->assertEquals('foo', $adapter->read('foo')); + + $requests = $mock->getReceivedRequests(); + $this->assertEquals('HEAD', $requests[0]->getMethod()); + $this->assertEquals('PUT', $requests[1]->getMethod()); + $this->assertEquals('GET', $requests[2]->getMethod()); + $this->assertEquals('bucket.s3.amazonaws.com', $requests[0]->getHost()); + } + + /** + * @expectedException \RuntimeException + */ + public function testThrowsExceptionIfBucketMissingAndNotCreating() + { + $mock = new MockPlugin(array(new Response(404))); + $client = $this->getClient(); + $client->addSubscriber($mock); + $adapter = new AmazonS3($client, 'bucket'); + $adapter->read('foo'); + } + + public function testDoesNotSupportDirectories() + { + $adapter = new AmazonS3($this->getClient(), 'bucket'); + $this->assertFalse($adapter->isDirectory('foo')); + } + + public function testWritesObjects() + { + $mock = new MockPlugin(array( + new Response(200), // HEAD bucket response + new Response(201) // PUT object response + )); + $client = $this->getClient(); + $client->addSubscriber($mock); + $adapter = new AmazonS3($client, 'bucket'); + $this->assertEquals(7, $adapter->write('foo', 'testing')); + $requests = $mock->getReceivedRequests(); + $this->assertEquals('bucket.s3.amazonaws.com', $requests[1]->getHost()); + $this->assertEquals('PUT', $requests[1]->getMethod()); + } + + public function testChecksForObjectExistence() + { + $mock = new MockPlugin(array(new Response(200))); + $client = $this->getClient(); + $client->addSubscriber($mock); + $adapter = new AmazonS3($client, 'bucket'); + $this->assertTrue($adapter->exists('foo')); + $requests = $mock->getReceivedRequests(); + $this->assertEquals('bucket.s3.amazonaws.com', $requests[0]->getHost()); + $this->assertEquals('HEAD', $requests[0]->getMethod()); + $this->assertEquals('/foo', $requests[0]->getResource()); + } + + public function testGetsObjectUrls() + { + $client = $this->getClient(); + $adapter = new AmazonS3($client, 'bucket'); + $this->assertEquals('https://bucket.s3.amazonaws.com/foo', $adapter->getUrl('foo')); + } +} From 48cee891fd25357b78ed4505e90933ed1936931f Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Tue, 6 Aug 2013 14:44:09 -0700 Subject: [PATCH 028/426] Adding the start of a spec for the new adapter --- spec/Gaufrette/Adapter/AwsS3Spec.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 spec/Gaufrette/Adapter/AwsS3Spec.php diff --git a/spec/Gaufrette/Adapter/AwsS3Spec.php b/spec/Gaufrette/Adapter/AwsS3Spec.php new file mode 100644 index 000000000..8190433fc --- /dev/null +++ b/spec/Gaufrette/Adapter/AwsS3Spec.php @@ -0,0 +1,14 @@ +shouldHaveType('src\Gaufrette\Adapter\AwsS3.php'); + } +} From ff6da5461cfd2031c82c85519a6d5643cb97968e Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Wed, 7 Aug 2013 16:03:04 -0700 Subject: [PATCH 029/426] More spec tests for the AwsS3 adapter --- spec/Gaufrette/Adapter/AwsS3Spec.php | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/spec/Gaufrette/Adapter/AwsS3Spec.php b/spec/Gaufrette/Adapter/AwsS3Spec.php index 8190433fc..5a6775bd6 100644 --- a/spec/Gaufrette/Adapter/AwsS3Spec.php +++ b/spec/Gaufrette/Adapter/AwsS3Spec.php @@ -1,14 +1,32 @@ beConstructedWith($service, 'bucketName'); + } + function it_is_initializable() { - $this->shouldHaveType('src\Gaufrette\Adapter\AwsS3.php'); + $this->shouldHaveType('Gaufrette\Adapter\AwsS3'); + } + + function it_is_adapter() + { + $this->shouldHaveType('Gaufrette\Adapter'); + } + + function it_supports_metadata() + { + $this->shouldHaveType('Gaufrette\Adapter\MetadataSupporter'); } } From 2996603d9018163fe901f7db7999cc9c9df3ac95 Mon Sep 17 00:00:00 2001 From: Michael Dowling Date: Thu, 8 Aug 2013 14:03:36 -0700 Subject: [PATCH 030/426] Removing unnecessary alias --- src/Gaufrette/Adapter/AwsS3.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index e45765247..ccc922350 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -3,7 +3,7 @@ namespace Gaufrette\Adapter; use Gaufrette\Adapter; -use Aws\S3\S3Client as S3Client; +use Aws\S3\S3Client; /** * Amazon S3 adapter using the AWS SDK for PHP v2.x From 252842f39c4fb86123243afed9287992bdcbb7f2 Mon Sep 17 00:00:00 2001 From: Luciano Mammino Date: Fri, 9 Aug 2013 19:05:09 +0200 Subject: [PATCH 031/426] Added AzureBlobStorage adapter --- composer.json | 8 +- src/Gaufrette/Adapter/AzureBlobStorage.php | 342 ++++++++++++++++++ .../Adapter/AzureBlobStorageTest.php | 7 + .../Functional/Adapter/FunctionalTestCase.php | 11 + .../adapters/AzureBlobStorage.php.dist | 7 + 5 files changed, 374 insertions(+), 1 deletion(-) create mode 100644 src/Gaufrette/Adapter/AzureBlobStorage.php create mode 100644 tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php create mode 100644 tests/Gaufrette/Functional/adapters/AzureBlobStorage.php.dist diff --git a/composer.json b/composer.json index c583589bd..ee0b2b79a 100644 --- a/composer.json +++ b/composer.json @@ -35,6 +35,10 @@ "files": ["cloudfiles.php"] } } + }, + { + "type": "pear", + "url": "http://pear.php.net" } ], "require": { @@ -50,7 +54,8 @@ "doctrine/dbal": ">=2.3", "dropbox-php/dropbox-php": "*", "herzult/php-ssh": "*", - "phpunit/phpunit": "3.7.*" + "phpunit/phpunit": "3.7.*", + "microsoft/windowsazure": "*" }, "suggest": { "knplabs/knp-gaufrette-bundle": "to use with Symfony2", @@ -60,6 +65,7 @@ "amazonwebservices/aws-sdk-for-php": "to use the legacy Amazon S3 adapters", "phpseclib/phpseclib": "to use the SFTP", "doctrine/dbal": "to use the Doctrine DBAL adapter", + "microsoft/windowsazure": "to use Microsoft Azure Blob Storage adapter", "ext-zip": "to use the Zip adapter", "ext-apc": "to use the APC adapter", "ext-curl": "*", diff --git a/src/Gaufrette/Adapter/AzureBlobStorage.php b/src/Gaufrette/Adapter/AzureBlobStorage.php new file mode 100644 index 000000000..53f76b11b --- /dev/null +++ b/src/Gaufrette/Adapter/AzureBlobStorage.php @@ -0,0 +1,342 @@ + + */ +class AzureBlobStorage implements Adapter, + MetadataSupporter +{ + /** + * Error constants + */ + const ERROR_CONTAINER_ALREADY_EXISTS = 'ContainerAlreadyExists'; + const ERROR_CONTAINER_NOT_FOUND = 'ContainerNotFound'; + + /** + * @var string $connectionString + */ + protected $connectionString; + + /** + * @var string $containerName + */ + protected $containerName; + + /** + * @var bool $detectContentType + */ + protected $detectContentType; + + /** + * @var bool $calculateChecksum + */ + protected $calculateChecksum; + + /** + * @var \WindowsAzure\Blob\Internal\IBlob $blobProxy + */ + protected $blobProxy; + + /** + * Constructor + * + * @param string $connectionString + * @param string $containerName + * @param bool $create + * @param bool $detectContentType + * @param bool $calculateChecksum + */ + public function __construct($connectionString, $containerName, $create = false, $detectContentType = true, + $calculateChecksum = true) + { + $this->connectionString = $connectionString; + $this->containerName = $containerName; + $this->detectContentType = $detectContentType; + $this->calculateChecksum = $calculateChecksum; + if($create) + $this->createContainer($containerName); + } + + /** + * Creates a new container + * + * @param string $containerName + * @param \WindowsAzure\Blob\Models\CreateContainerOptions $options + * @throws \RuntimeException if cannot create the container + */ + public function createContainer($containerName, CreateContainerOptions $options = null) + { + $this->init(); + + try { + $this->blobProxy->createContainer($containerName, $options); + } catch (ServiceException $e) { + $errorCode = $this->getErrorCodeFromServiceException($e); + + if ($errorCode != self::ERROR_CONTAINER_ALREADY_EXISTS) { + throw new \RuntimeException(sprintf( + 'Failed to create the configured container "%s": %s (%s).', + $containerName, + $e->getErrorText(), + $errorCode + ), $e->getCode()); + } + } + } + + /** + * Deletes a container + * + * @param string $containerName + * @param DeleteContainerOptions $options + * @throws \RuntimeException if cannot delete the container + */ + public function deleteContainer($containerName, DeleteContainerOptions $options = null) + { + $this->init(); + + try { + $this->blobProxy->deleteContainer($containerName, $options); + } catch (ServiceException $e) { + $errorCode = $this->getErrorCodeFromServiceException($e); + + if ($errorCode != self::ERROR_CONTAINER_NOT_FOUND) { + throw new \RuntimeException(sprintf( + 'Failed to delete the configured container "%s": %s (%s).', + $containerName, + $e->getErrorText(), + $errorCode + ), $e->getCode()); + } + } + } + + /** + * {@inheritDoc} + */ + public function read($key) + { + $this->init(); + + try { + $blob = $this->blobProxy->getBlob($this->containerName, $key); + + return stream_get_contents($blob->getContentStream()); + } catch (ServiceException $e) { + return false; + } + } + + /** + * {@inheritDoc} + */ + public function write($key, $content) + { + $this->init(); + + try { + $options = new CreateBlobOptions(); + + if ($this->detectContentType) { + $fileInfo = new \finfo(FILEINFO_MIME_TYPE); + $contentType = $fileInfo->buffer($content); + $options->setContentType($contentType); + } + + if ($this->calculateChecksum) { + $options->setContentMD5(Util\Checksum::fromContent($content)); + } + + $this->blobProxy->createBlockBlob($this->containerName, $key, $content, $options); + + return Util\Size::fromContent($content); + } catch (ServiceException $e) { + return false; + } + } + + /** + * {@inheritDoc} + */ + public function exists($key) + { + $this->init(); + + try { + $this->blobProxy->getBlob($this->containerName, $key); + + return true; + } catch (ServiceException $e) { + return false; + } + } + + /** + * {@inheritDoc} + */ + public function keys() + { + $this->init(); + + try { + $blobList = $this->blobProxy->listBlobs($this->containerName); + $blobs = $blobList->getBlobs(); + $keys = array(); + + foreach ($blobs as $blob) { + $keys[] = $blob->getName(); + } + + return $keys; + } catch (ServiceException $e) { + $errorCode = $this->getErrorCodeFromServiceException($e); + + throw new \RuntimeException(sprintf( + 'Failed to list keys for the container "%s": %s (%s).', + $this->containerName, + $e->getErrorText(), + $errorCode + ), $e->getCode()); + } + } + + /** + * {@inheritDoc} + */ + public function mtime($key) + { + $this->init(); + + try { + $properties = $this->blobProxy->getBlobProperties($this->containerName, $key); + + return $properties->getProperties()->getLastModified()->getTimestamp(); + } catch (ServiceException $e) { + return false; + } + } + + /** + * {@inheritDoc} + */ + public function delete($key) + { + $this->init(); + + try { + $this->blobProxy->deleteBlob($this->containerName, $key); + + return true; + } catch (ServiceException $e) { + return false; + } + } + + /** + * {@inheritDoc} + */ + public function rename($sourceKey, $targetKey) + { + $this->init(); + + try { + $this->blobProxy->copyBlob($this->containerName, $targetKey, $this->containerName, $sourceKey); + $this->blobProxy->deleteBlob($this->containerName, $sourceKey); + + return true; + } catch (ServiceException $e) { + return false; + } + } + + /** + * {@inheritDoc} + */ + public function isDirectory($key) + { + $this->init(); + + if ($this->exists($key.'/')) { + return true; + } + + return false; + } + + /** + * {@inheritDoc} + */ + public function setMetadata($key, $content) + { + try { + $this->blobProxy->setBlobMetadata($this->containerName, $key, $content); + } catch (ServiceException $e) { + $errorCode = $this->getErrorCodeFromServiceException($e); + + throw new \RuntimeException(sprintf( + 'Failed to set metadata for blob "%s" in container "%s": %s (%s).', + $key, + $this->containerName, + $e->getErrorText(), + $errorCode + ), $e->getCode()); + } + } + + /** + * {@inheritDoc} + */ + public function getMetadata($key) + { + try { + $properties = $this->blobProxy->getBlobProperties($this->containerName, $key); + + return $properties->getMetadata(); + } catch (ServiceException $e) { + $errorCode = $this->getErrorCodeFromServiceException($e); + + throw new \RuntimeException(sprintf( + 'Failed to get metadata for blob "%s" in container "%s": %s (%s).', + $key, + $this->containerName, + $e->getErrorText(), + $errorCode + ), $e->getCode()); + } + } + + /** + * Lazy initialization, automatically called when some method is called after construction + */ + protected function init() + { + if($this->blobProxy == null) + $this->blobProxy = ServicesBuilder::getInstance()->createBlobService($this->connectionString); + } + + /** + * Extracts the error code from a service exception + * + * @param ServiceException $exception + * @return string + */ + protected function getErrorCodeFromServiceException(ServiceException $exception) + { + $xml = simplexml_load_string($exception->getErrorReason()); + + return (string) $xml->Code; + } +} diff --git a/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php b/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php new file mode 100644 index 000000000..c6afa3276 --- /dev/null +++ b/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php @@ -0,0 +1,7 @@ +assertEquals('Some content', $this->filesystem->read('foo')); $this->assertEquals('Some content1', $this->filesystem->read('test/subdir/foo')); + $this->filesystem->delete('foo'); + $this->filesystem->delete('test/subdir/foo'); } /** @@ -79,6 +81,7 @@ public function shouldUpdateFileContent() $this->filesystem->write('foo', 'Some content updated', true); $this->assertEquals('Some content updated', $this->filesystem->read('foo')); + $this->filesystem->delete('foo'); } /** @@ -94,6 +97,8 @@ public function shouldCheckIfFileExists() $this->assertTrue($this->filesystem->has('foo')); $this->assertFalse($this->filesystem->has('test/somefile')); $this->assertFalse($this->filesystem->has('test/somefile')); + + $this->filesystem->delete('foo'); } /** @@ -105,6 +110,8 @@ public function shouldGetMtime() $this->filesystem->write('foo', 'Some content'); $this->assertGreaterThan(0, $this->filesystem->mtime('foo')); + + $this->filesystem->delete('foo'); } /** @@ -172,6 +179,10 @@ public function shouldFetchKeys() foreach (array('foo', 'bar', 'baz') as $key) { $this->assertContains($key, $actualKeys); } + + $this->filesystem->delete('foo'); + $this->filesystem->delete('bar'); + $this->filesystem->delete('baz'); } /** diff --git a/tests/Gaufrette/Functional/adapters/AzureBlobStorage.php.dist b/tests/Gaufrette/Functional/adapters/AzureBlobStorage.php.dist new file mode 100644 index 000000000..a13c12d27 --- /dev/null +++ b/tests/Gaufrette/Functional/adapters/AzureBlobStorage.php.dist @@ -0,0 +1,7 @@ + Date: Sat, 10 Aug 2013 00:45:12 +0200 Subject: [PATCH 032/426] - Decoupled Blob Factory - PSR fixes - Removed checksum auto calculation (causes weird problems in some circumstances) --- src/Gaufrette/Adapter/AzureBlobStorage.php | 52 +++++++++---------- .../AzureBlobStorage/BlobProxyFactory.php | 36 +++++++++++++ .../BlobProxyFactoryInterface.php | 18 +++++++ .../adapters/AzureBlobStorage.php.dist | 3 +- 4 files changed, 81 insertions(+), 28 deletions(-) create mode 100644 src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php create mode 100644 src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactoryInterface.php diff --git a/src/Gaufrette/Adapter/AzureBlobStorage.php b/src/Gaufrette/Adapter/AzureBlobStorage.php index 53f76b11b..d29c5f354 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage.php @@ -4,17 +4,18 @@ use Gaufrette\Adapter; use Gaufrette\Util; +use Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface; use WindowsAzure\Blob\Models\CreateBlobOptions; use WindowsAzure\Blob\Models\CreateContainerOptions; use WindowsAzure\Blob\Models\DeleteContainerOptions; -use WindowsAzure\Common\ServicesBuilder; use WindowsAzure\Common\ServiceException; /** * Microsoft Azure Blob Storage adapter * * @author Luciano Mammino + * @author Paweł Czyżewski */ class AzureBlobStorage implements Adapter, MetadataSupporter @@ -26,25 +27,20 @@ class AzureBlobStorage implements Adapter, const ERROR_CONTAINER_NOT_FOUND = 'ContainerNotFound'; /** - * @var string $connectionString + * @var AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory */ - protected $connectionString; + protected $blobProxyFactory; /** - * @var string $containerName + * @var string $containerName */ protected $containerName; /** - * @var bool $detectContentType + * @var bool $detectContentType */ protected $detectContentType; - /** - * @var bool $calculateChecksum - */ - protected $calculateChecksum; - /** * @var \WindowsAzure\Blob\Internal\IBlob $blobProxy */ @@ -53,21 +49,19 @@ class AzureBlobStorage implements Adapter, /** * Constructor * - * @param string $connectionString - * @param string $containerName - * @param bool $create - * @param bool $detectContentType - * @param bool $calculateChecksum + * @param AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory + * @param string $containerName + * @param bool $create + * @param bool $detectContentType */ - public function __construct($connectionString, $containerName, $create = false, $detectContentType = true, - $calculateChecksum = true) + public function __construct(BlobProxyFactoryInterface $blobProxyFactory, $containerName, $create = false, $detectContentType = true) { - $this->connectionString = $connectionString; + $this->blobProxyFactory = $blobProxyFactory; $this->containerName = $containerName; $this->detectContentType = $detectContentType; - $this->calculateChecksum = $calculateChecksum; - if($create) + if ($create) { $this->createContainer($containerName); + } } /** @@ -156,10 +150,6 @@ public function write($key, $content) $options->setContentType($contentType); } - if ($this->calculateChecksum) { - $options->setContentMD5(Util\Checksum::fromContent($content)); - } - $this->blobProxy->createBlockBlob($this->containerName, $key, $content, $options); return Util\Size::fromContent($content); @@ -281,6 +271,8 @@ public function isDirectory($key) */ public function setMetadata($key, $content) { + $this->init(); + try { $this->blobProxy->setBlobMetadata($this->containerName, $key, $content); } catch (ServiceException $e) { @@ -301,6 +293,8 @@ public function setMetadata($key, $content) */ public function getMetadata($key) { + $this->init(); + try { $properties = $this->blobProxy->getBlobProperties($this->containerName, $key); @@ -323,8 +317,9 @@ public function getMetadata($key) */ protected function init() { - if($this->blobProxy == null) - $this->blobProxy = ServicesBuilder::getInstance()->createBlobService($this->connectionString); + if ($this->blobProxy == null) { + $this->blobProxy = $this->blobProxyFactory->create(); + } } /** @@ -336,7 +331,10 @@ protected function init() protected function getErrorCodeFromServiceException(ServiceException $exception) { $xml = simplexml_load_string($exception->getErrorReason()); + if(isset($xml->Code)) + + return (string) $xml->Code; - return (string) $xml->Code; + return $exception->getErrorReason(); } } diff --git a/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php b/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php new file mode 100644 index 000000000..be6e0a7d7 --- /dev/null +++ b/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php @@ -0,0 +1,36 @@ + + */ +class BlobProxyFactory implements BlobProxyFactoryInterface +{ + /** + * @var string $connectionString + */ + protected $connectionString; + + /** + * Constructor + * + * @param string $connectionString + */ + public function __construct($connectionString) + { + $this->connectionString = $connectionString; + } + + /** + * {@inheritDoc} + */ + public function create() + { + return ServicesBuilder::getInstance()->createBlobService($this->connectionString); + } +} diff --git a/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactoryInterface.php b/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactoryInterface.php new file mode 100644 index 000000000..8424fe742 --- /dev/null +++ b/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactoryInterface.php @@ -0,0 +1,18 @@ + + */ +interface BlobProxyFactoryInterface +{ + /** + * Creates a new instance of the Blob proxy + * + * @return \WindowsAzure\Blob\Internal\IBlob + */ + public function create(); +} diff --git a/tests/Gaufrette/Functional/adapters/AzureBlobStorage.php.dist b/tests/Gaufrette/Functional/adapters/AzureBlobStorage.php.dist index a13c12d27..415c34477 100644 --- a/tests/Gaufrette/Functional/adapters/AzureBlobStorage.php.dist +++ b/tests/Gaufrette/Functional/adapters/AzureBlobStorage.php.dist @@ -4,4 +4,5 @@ $connectionString = 'BlobEndpoint=http://XXXXXXXXXXX.blob.core.windows.net/;Ac $containerName = 'gaufrette-test'; $create = true; -return new Gaufrette\Adapter\AzureBlobStorage($connectionString, $containerName, $create); \ No newline at end of file +$factory = new Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactory($connectionString); +return new Gaufrette\Adapter\AzureBlobStorage($factory, $containerName, $create); \ No newline at end of file From 3e215d1d9b4a907e3fab9c722abe21e695eae8b8 Mon Sep 17 00:00:00 2001 From: Luciano Mammino Date: Sat, 10 Aug 2013 19:02:58 +0200 Subject: [PATCH 033/426] - Added specs - Some minor PSR fixes - Now fails on major methods when container does not exists (as in amazonS3 adapter) --- spec/Gaufrette/Adapter/AzureBlobStorage.php | 494 ++++++++++++++++++ .../AzureBlobStorage/BlobProxyFactory.php | 22 + src/Gaufrette/Adapter/AzureBlobStorage.php | 38 +- 3 files changed, 552 insertions(+), 2 deletions(-) create mode 100644 spec/Gaufrette/Adapter/AzureBlobStorage.php create mode 100644 spec/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php diff --git a/spec/Gaufrette/Adapter/AzureBlobStorage.php b/spec/Gaufrette/Adapter/AzureBlobStorage.php new file mode 100644 index 000000000..9ff9065dc --- /dev/null +++ b/spec/Gaufrette/Adapter/AzureBlobStorage.php @@ -0,0 +1,494 @@ +beConstructedWith($blobProxyFactory, 'containerName'); + } + + public function it_should_be_initializable() + { + $this->shouldHaveType('Gaufrette\Adapter\AzureBlobStorage'); + $this->shouldHaveType('Gaufrette\Adapter'); + $this->shouldHaveType('Gaufrette\Adapter\MetadataSupporter'); + } + + /** + * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory + * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy + * @param \WindowsAzure\Blob\Models\GetBlobResult $getBlobResult + */ + public function it_should_read_file($blobProxyFactory, $blobProxy, $getBlobResult) + { + $getBlobResult + ->getContentStream() + ->shouldBeCalled() + //azure blob content is handled as stream so we need to fake it + ->willReturn(fopen('data://text/plain,some content','r')); + + $blobProxy + ->getBlob('containerName', 'filename') + ->shouldBeCalled() + ->willReturn($getBlobResult); + + $blobProxyFactory + ->create() + ->shouldBeCalled() + ->willReturn($blobProxy); + + $this->read('filename')->shouldReturn('some content'); + } + + /** + * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory + * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy + */ + public function it_should_return_false_when_cannot_read($blobProxyFactory, $blobProxy) + { + $blobProxy + ->getBlob('containerName', 'filename') + ->shouldBeCalled() + ->willThrow(new ServiceException(500)); + + $blobProxyFactory + ->create() + ->shouldBeCalled() + ->willReturn($blobProxy); + + $this->read('filename')->shouldReturn(false); + } + + /** + * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory + * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy + */ + public function it_should_not_mask_exception_when_read($blobProxyFactory, $blobProxy) + { + $blobProxy + ->getBlob('containerName', 'filename') + ->shouldBeCalled() + ->willThrow(new \RuntimeException('read')); + + $blobProxyFactory + ->create() + ->shouldBeCalled() + ->willReturn($blobProxy); + + $this->shouldThrow(new \RuntimeException('read'))->duringRead('filename'); + } + + /** + * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory + * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy + */ + public function it_should_rename_file($blobProxyFactory, $blobProxy) + { + $blobProxy + ->copyBlob('containerName', 'filename2', 'containerName', 'filename1') + ->shouldBeCalled(); + + $blobProxy + ->deleteBlob('containerName', 'filename1') + ->shouldBeCalled(); + + $blobProxyFactory + ->create() + ->shouldBeCalled() + ->willReturn($blobProxy); + + $this->rename('filename1', 'filename2')->shouldReturn(true); + } + + /** + * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory + * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy + */ + public function it_should_return_false_when_cannot_rename($blobProxyFactory, $blobProxy) + { + $blobProxy + ->copyBlob('containerName', 'filename2', 'containerName', 'filename1') + ->shouldBeCalled() + ->willThrow(new ServiceException(500)); + + $blobProxyFactory + ->create() + ->shouldBeCalled() + ->willReturn($blobProxy); + + $this->rename('filename1', 'filename2')->shouldReturn(false); + } + + /** + * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory + * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy + */ + public function it_should_not_mask_exception_when_rename($blobProxyFactory, $blobProxy) + { + $blobProxy + ->copyBlob('containerName', 'filename2', 'containerName', 'filename1') + ->shouldBeCalled() + ->willThrow(new \RuntimeException('rename')); + + $blobProxyFactory + ->create() + ->shouldBeCalled() + ->willReturn($blobProxy); + + $this->shouldThrow(new \RuntimeException('rename'))->duringRename('filename1', 'filename2'); + } + + /** + * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory + * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy + */ + public function it_should_write_file($blobProxyFactory, $blobProxy) + { + $blobProxy + ->createBlockBlob('containerName', 'filename', 'some content', + \Mockery::type('\WindowsAzure\Blob\Models\CreateBlobOptions')) + ->shouldBeCalled(); + + $blobProxyFactory + ->create() + ->shouldBeCalled() + ->willReturn($blobProxy); + + $this->write('filename', 'some content')->shouldReturn(12); + } + + /** + * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory + * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy + */ + public function it_should_return_false_when_cannot_write($blobProxyFactory, $blobProxy) + { + $blobProxy + ->createBlockBlob('containerName', 'filename', 'some content', + \Mockery::type('\WindowsAzure\Blob\Models\CreateBlobOptions')) + ->willThrow(new ServiceException(500)); + + $blobProxyFactory + ->create() + ->shouldBeCalled() + ->willReturn($blobProxy); + + $this->write('filename', 'some content')->shouldReturn(false); + } + + /** + * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory + * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy + */ + public function it_should_not_mask_exception_when_write($blobProxyFactory, $blobProxy) + { + $blobProxy + ->createBlockBlob('containerName', 'filename', 'some content', + \Mockery::type('\WindowsAzure\Blob\Models\CreateBlobOptions')) + ->willThrow(new \RuntimeException('write')); + + $blobProxyFactory + ->create() + ->shouldBeCalled() + ->willReturn($blobProxy); + + $this->shouldThrow(new \RuntimeException('write'))->duringWrite('filename', 'some content'); + } + + /** + * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory + * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy + * @param \WindowsAzure\Blob\Models\GetBlobResult $getBlobResult + */ + public function it_should_check_if_file_exists($blobProxyFactory, $blobProxy, $getBlobResult) + { + $blobProxyFactory + ->create() + ->shouldBeCalled() + ->willReturn($blobProxy); + + $blobProxy + ->getBlob('containerName', 'filename') + ->shouldBeCalled() + ->willThrow(new ServiceException(404)); + + $this->exists('filename')->shouldReturn(false); + + $blobProxy + ->getBlob('containerName', 'filename2') + ->shouldBeCalled() + ->willReturn($getBlobResult); + + $this->exists('filename2')->shouldReturn(true); + } + + /** + * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory + * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy + */ + public function it_should_not_mask_exception_when_check_if_file_exists($blobProxyFactory, $blobProxy) + { + $blobProxyFactory + ->create() + ->shouldBeCalled() + ->willReturn($blobProxy); + + $blobProxy + ->getBlob('containerName', 'filename') + ->shouldBeCalled() + ->willThrow(new \RuntimeException('exists')); + + $this->shouldThrow(new \RuntimeException('exists'))->duringExists('filename'); + } + + /** + * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory + * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy + * @param \WindowsAzure\Blob\Models\GetBlobPropertiesResult $getBlobPropertiesResult + * @param \WindowsAzure\Blob\Models\BlobProperties $blobProperties + */ + public function it_should_get_file_mtime($blobProxyFactory, $blobProxy, $getBlobPropertiesResult, $blobProperties) + { + $blobProxyFactory + ->create() + ->shouldBeCalled() + ->willReturn($blobProxy); + + $blobProxy + ->getBlobProperties('containerName', 'filename') + ->shouldBeCalled() + ->willReturn($getBlobPropertiesResult); + + $getBlobPropertiesResult + ->getProperties() + ->shouldBeCalled() + ->willReturn($blobProperties); + + $blobProperties + ->getLastModified() + ->shouldBeCalled() + ->willReturn(new \DateTime('1987-12-28 20:00:00')); + + $this->mtime('filename')->shouldReturn(strtotime('1987-12-28 20:00:00')); + } + + /** + * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory + * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy + */ + public function it_should_return_false_when_cannot_mtime($blobProxyFactory, $blobProxy) + { + $blobProxyFactory + ->create() + ->shouldBeCalled() + ->willReturn($blobProxy); + + $blobProxy + ->getBlobProperties('containerName', 'filename') + ->shouldBeCalled() + ->willThrow(new ServiceException(500)); + + $this->mtime('filename')->shouldReturn(false); + } + + /** + * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory + * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy + */ + public function it_should_not_mask_exception_when_get_mtime($blobProxyFactory, $blobProxy) + { + $blobProxyFactory + ->create() + ->shouldBeCalled() + ->willReturn($blobProxy); + + $blobProxy + ->getBlobProperties('containerName', 'filename') + ->shouldBeCalled() + ->willThrow(new \RuntimeException('mtime')); + + $this->shouldThrow(new \RuntimeException('mtime'))->duringMtime('filename'); + } + + /** + * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory + * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy + */ + public function it_should_delete_file($blobProxyFactory, $blobProxy) + { + $blobProxyFactory + ->create() + ->shouldBeCalled() + ->willReturn($blobProxy); + + $blobProxy + ->deleteBlob('containerName', 'filename') + ->shouldBeCalled(); + + $this->delete('filename')->shouldReturn(true); + } + + /** + * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory + * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy + */ + public function it_should_return_false_when_cannot_delete_file($blobProxyFactory, $blobProxy) + { + $blobProxyFactory + ->create() + ->shouldBeCalled() + ->willReturn($blobProxy); + + $blobProxy + ->deleteBlob('containerName', 'filename') + ->shouldBeCalled() + ->willThrow(new ServiceException(500)); + + $this->delete('filename')->shouldReturn(false); + } + + /** + * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory + * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy + */ + public function it_should_not_mask_exception_when_delete($blobProxyFactory, $blobProxy) + { + $blobProxyFactory + ->create() + ->shouldBeCalled() + ->willReturn($blobProxy); + + $blobProxy + ->deleteBlob('containerName', 'filename') + ->shouldBeCalled() + ->willThrow(new \RuntimeException('delete')); + + $this->shouldThrow(new \RuntimeException('delete'))->duringDelete('filename'); + } + + /** + * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory + * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy + * @param \WindowsAzure\Blob\Models\ListBlobsResult $listBlobResult + */ + public function it_should_get_keys($blobProxyFactory, $blobProxy, $listBlobResult) + { + $fileNames = array('aaa', 'aaa/filename', 'filename1', 'filename2'); + $blobs = array(); + foreach ($fileNames as $fileName) { + $blob = new Blob(); + $blob->setName($fileName); + $blobs[] = $blob; + } + + $blobProxyFactory + ->create() + ->shouldBeCalled() + ->willReturn($blobProxy); + + $blobProxy + ->listBlobs('containerName') + ->shouldBeCalled() + ->willReturn($listBlobResult); + + $listBlobResult + ->getBlobs() + ->shouldBeCalled() + ->willReturn($blobs); + + $this->keys()->shouldReturn(array('aaa', 'aaa/filename', 'filename1', 'filename2')); + } + + /** + * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory + * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy + */ + public function it_should_not_mask_exception_when_get_keys($blobProxyFactory, $blobProxy) + { + $blobProxyFactory + ->create() + ->shouldBeCalled() + ->willReturn($blobProxy); + + $blobProxy + ->listBlobs('containerName') + ->shouldBeCalled() + ->willThrow(new \RuntimeException('keys')); + + $this->shouldThrow(new \RuntimeException('keys'))->duringKeys(); + } + + /** + * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory + * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy + */ + public function it_should_handle_dirs($blobProxyFactory, $blobProxy) + { + $blobProxyFactory + ->create() + ->shouldBeCalled() + ->willReturn($blobProxy); + + $blobProxy + ->getBlob('containerName', 'filename') + ->shouldNotBeCalled(); + $blobProxy + ->getBlob('containerName', 'filename/') + ->shouldBeCalled() + ->willThrow(new ServiceException(404)); + $blobProxy + ->getBlob('containerName', 'dirname/') + ->shouldBeCalled(); + + $this->isDirectory('filename')->shouldReturn(false); + $this->isDirectory('dirname')->shouldReturn(true); + } + + /** + * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory + * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy + */ + public function it_should_create_container($blobProxyFactory, $blobProxy) + { + $blobProxyFactory + ->create() + ->shouldBeCalled() + ->willReturn($blobProxy); + + $blobProxy + ->createContainer('containerName', null) + ->shouldBeCalled(); + + $this->createContainer('containerName'); + } + + /** + * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory + * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy + */ + public function it_should_fail_when_cannot_create_container($blobProxyFactory, $blobProxy) + { + $blobProxyFactory + ->create() + ->shouldBeCalled() + ->willReturn($blobProxy); + + $blobProxy + ->createContainer('containerName', null) + ->shouldBeCalled() + ->willThrow(new ServiceException(500)); + + $this->shouldThrow(new \RuntimeException('Failed to create the configured container "containerName": 0 ().', null))->duringCreateContainer('containerName'); + } + +} diff --git a/spec/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php b/spec/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php new file mode 100644 index 000000000..c5d8178f5 --- /dev/null +++ b/spec/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php @@ -0,0 +1,22 @@ +beConstructedWith($connectionString); + } + + public function it_should_be_initializable() + { + $this->shouldHaveType('Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactory'); + $this->shouldHaveType('Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface'); + } +} diff --git a/src/Gaufrette/Adapter/AzureBlobStorage.php b/src/Gaufrette/Adapter/AzureBlobStorage.php index d29c5f354..96dc55e4d 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage.php @@ -86,7 +86,7 @@ public function createContainer($containerName, CreateContainerOptions $options $containerName, $e->getErrorText(), $errorCode - ), $e->getCode()); + )); } } } @@ -130,6 +130,8 @@ public function read($key) return stream_get_contents($blob->getContentStream()); } catch (ServiceException $e) { + $this->failIfContainerNotFound($e, sprintf('read key "%s"', $key)); + return false; } } @@ -154,6 +156,8 @@ public function write($key, $content) return Util\Size::fromContent($content); } catch (ServiceException $e) { + $this->failIfContainerNotFound($e, sprintf('write content for key "%s"', $key)); + return false; } } @@ -170,6 +174,8 @@ public function exists($key) return true; } catch (ServiceException $e) { + $this->failIfContainerNotFound($e, sprintf('check if key "%s" exists', $key)); + return false; } } @@ -192,6 +198,7 @@ public function keys() return $keys; } catch (ServiceException $e) { + $this->failIfContainerNotFound($e, 'retrieve keys'); $errorCode = $this->getErrorCodeFromServiceException($e); throw new \RuntimeException(sprintf( @@ -215,6 +222,8 @@ public function mtime($key) return $properties->getProperties()->getLastModified()->getTimestamp(); } catch (ServiceException $e) { + $this->failIfContainerNotFound($e, sprintf('read mtime for key "%s"', $key)); + return false; } } @@ -231,6 +240,8 @@ public function delete($key) return true; } catch (ServiceException $e) { + $this->failIfContainerNotFound($e, sprintf('delete key "%s"', $key)); + return false; } } @@ -248,6 +259,8 @@ public function rename($sourceKey, $targetKey) return true; } catch (ServiceException $e) { + $this->failIfContainerNotFound($e, sprintf('rename key "%s"', $sourceKey)); + return false; } } @@ -322,6 +335,26 @@ protected function init() } } + /** + * Throws a runtime exception if a give ServiceException derived from a "container not found" error + * + * @param ServiceException $exception + * @param string $action + * @throws \RuntimeException + */ + protected function failIfContainerNotFound(ServiceException $exception, $action) + { + $errorCode = $this->getErrorCodeFromServiceException($exception); + + if ($errorCode == self::ERROR_CONTAINER_NOT_FOUND) { + throw new \RuntimeException(sprintf( + 'Failed to %s: container "%s" not found.', + $action, + $this->containerName + ), $exception->getCode()); + } + } + /** * Extracts the error code from a service exception * @@ -331,9 +364,10 @@ protected function init() protected function getErrorCodeFromServiceException(ServiceException $exception) { $xml = simplexml_load_string($exception->getErrorReason()); - if(isset($xml->Code)) + if (isset($xml->Code)) { return (string) $xml->Code; + } return $exception->getErrorReason(); } From fcef9863dbabd4fafd871b2d71d8db3982d8e9f6 Mon Sep 17 00:00:00 2001 From: Luciano Mammino Date: Mon, 12 Aug 2013 15:27:41 +0200 Subject: [PATCH 034/426] - Fixed `microsoft/windowsazure` version in `composer.json` - Improved `keys()` method (thanks @Baachi) --- composer.json | 2 +- src/Gaufrette/Adapter/AzureBlobStorage.php | 13 ++++++------- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/composer.json b/composer.json index ee0b2b79a..c4694dd10 100644 --- a/composer.json +++ b/composer.json @@ -55,7 +55,7 @@ "dropbox-php/dropbox-php": "*", "herzult/php-ssh": "*", "phpunit/phpunit": "3.7.*", - "microsoft/windowsazure": "*" + "microsoft/windowsazure": "dev-master" }, "suggest": { "knplabs/knp-gaufrette-bundle": "to use with Symfony2", diff --git a/src/Gaufrette/Adapter/AzureBlobStorage.php b/src/Gaufrette/Adapter/AzureBlobStorage.php index 96dc55e4d..356c93938 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage.php @@ -189,14 +189,13 @@ public function keys() try { $blobList = $this->blobProxy->listBlobs($this->containerName); - $blobs = $blobList->getBlobs(); - $keys = array(); - foreach ($blobs as $blob) { - $keys[] = $blob->getName(); - } - - return $keys; + return array_map( + function($blob) { + return $blob->getName(); + }, + $blobList->getBlobs() + ); } catch (ServiceException $e) { $this->failIfContainerNotFound($e, 'retrieve keys'); $errorCode = $this->getErrorCodeFromServiceException($e); From 7aa141f53e78e2fe8cd1fbbe6dbb34ca55ca8faa Mon Sep 17 00:00:00 2001 From: Luciano Mammino Date: Tue, 13 Aug 2013 09:56:56 +0200 Subject: [PATCH 035/426] - Added documentation and sample code on README file --- README.markdown | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/README.markdown b/README.markdown index cec928bc3..130bbab07 100644 --- a/README.markdown +++ b/README.markdown @@ -154,6 +154,32 @@ $connection = new OpenCloud\Rackspace( ``` +Using AzureBlobStorage +---------------------- +Azure Blob Storage is the storage service provided by Microsoft Windows Azure cloud environment. To use this adapter +you need to install the [Azure SDK for php](http://www.windowsazure.com/en-us/develop/php/common-tasks/download-php-sdk/) +into your project. + +To instantiate the `AzureBlobStorage` adapter you need a `BlobProxyFactoryInterface` instance (you can use the default +`BlobProxyFactory` class) and a connection string. The connection string should follow this prototype: + + BlobEndpoint=https://XXXXXXXXXX.blob.core.windows.net/;AccountName=XXXXXXXX;AccountKey=XXXXXXXXXXXXXXXXXXXX + +You should be able to find your **endpoint**, **account name** and **account key** in your +[Windows Azure management console](https://manage.windowsazure.com). + +Thanks to the blob proxy factory, the adapter lazy loads the connection to the endpoint, so it will not create any +connection until it's really needed (eg. when a read or write operation is issued). + +Follows a simple example on how to build the adapter: + +```php +$connectionString = '...'; +$factory = new Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactory($connectionString); +$adapter = new Gaufrette\Adapter\AzureBlobStorage($factory, 'my-container'); +$filesystem = new Gaufrette\Filesystem($adapter); +``` + Using FTP adapters --------------- From 9f8d621f339d60b2d517f80629ce0c6a2a3e1719 Mon Sep 17 00:00:00 2001 From: Luciano Mammino Date: Sun, 18 Aug 2013 23:52:23 +0200 Subject: [PATCH 036/426] - Improved `exists` performance --- src/Gaufrette/Adapter/AzureBlobStorage.php | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Gaufrette/Adapter/AzureBlobStorage.php b/src/Gaufrette/Adapter/AzureBlobStorage.php index 356c93938..051315643 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage.php @@ -9,6 +9,7 @@ use WindowsAzure\Blob\Models\CreateBlobOptions; use WindowsAzure\Blob\Models\CreateContainerOptions; use WindowsAzure\Blob\Models\DeleteContainerOptions; +use WindowsAzure\Blob\Models\ListBlobsOptions; use WindowsAzure\Common\ServiceException; /** @@ -169,15 +170,18 @@ public function exists($key) { $this->init(); - try { - $this->blobProxy->getBlob($this->containerName, $key); + $listBlobsOptions = new ListBlobsOptions(); + $listBlobsOptions->setPrefix($key); - return true; - } catch (ServiceException $e) { - $this->failIfContainerNotFound($e, sprintf('check if key "%s" exists', $key)); + $blobsList = $this->blobProxy->listBlobs($this->containerName, $listBlobsOptions); - return false; + foreach ($blobsList->getBlobs() as $blob) { + if ($key === $blob->getName()) { + return true; + } } + + return false; } /** From 01e184380dd7a56cbad1e547b3f648f17ddcc67b Mon Sep 17 00:00:00 2001 From: Luciano Mammino Date: Tue, 20 Aug 2013 01:10:26 +0200 Subject: [PATCH 037/426] - Improved error management for exists method - changed isDirectory to always return false --- src/Gaufrette/Adapter/AzureBlobStorage.php | 28 ++++++++++++++-------- 1 file changed, 18 insertions(+), 10 deletions(-) diff --git a/src/Gaufrette/Adapter/AzureBlobStorage.php b/src/Gaufrette/Adapter/AzureBlobStorage.php index 051315643..612a98cc5 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage.php @@ -173,12 +173,25 @@ public function exists($key) $listBlobsOptions = new ListBlobsOptions(); $listBlobsOptions->setPrefix($key); - $blobsList = $this->blobProxy->listBlobs($this->containerName, $listBlobsOptions); + try { + $blobsList = $this->blobProxy->listBlobs($this->containerName, $listBlobsOptions); - foreach ($blobsList->getBlobs() as $blob) { - if ($key === $blob->getName()) { - return true; + foreach ($blobsList->getBlobs() as $blob) { + if ($key === $blob->getName()) { + return true; + } } + } catch (ServiceException $e) { + $this->failIfContainerNotFound($e, 'check if key exists'); + $errorCode = $this->getErrorCodeFromServiceException($e); + + throw new \RuntimeException(sprintf( + 'Failed to check if key "%s" exists in container "%s": %s (%s).', + $key, + $this->containerName, + $e->getErrorText(), + $errorCode + ), $e->getCode()); } return false; @@ -273,12 +286,7 @@ public function rename($sourceKey, $targetKey) */ public function isDirectory($key) { - $this->init(); - - if ($this->exists($key.'/')) { - return true; - } - + // Windows Azure Blob Storage does not support directories return false; } From 0639f0cf7e19923f4db6d25ed07430a620b6da25 Mon Sep 17 00:00:00 2001 From: l3l0 Date: Tue, 20 Aug 2013 08:47:19 +0200 Subject: [PATCH 038/426] Fix AwsS3 unit test. --- .../Functional/Adapter/AwsS3Test.php | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php index 51bc711b5..e90bfc63c 100644 --- a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php +++ b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php @@ -2,12 +2,15 @@ namespace Gaufrette\Functional\Adapter; -use Gaufrette\Adapter\AmazonS3; +use Gaufrette\Adapter\AwsS3; use Aws\S3\S3Client; use Guzzle\Plugin\Mock\MockPlugin; use Guzzle\Http\Message\Response; -class AmazonS3Test extends \PHPUnit_Framework_TestCase +/** + * @todo move to phpspec + */ +class AwsS3Test extends \PHPUnit_Framework_TestCase { protected function getClient() { @@ -26,7 +29,7 @@ public function testCreatesBucketIfMissing() )); $client = $this->getClient(); $client->addSubscriber($mock); - $adapter = new AmazonS3($client, 'bucket', array('create' => true)); + $adapter = new AwsS3($client, 'bucket', array('create' => true)); $this->assertEquals('foo', $adapter->read('foo')); $requests = $mock->getReceivedRequests(); @@ -44,16 +47,10 @@ public function testThrowsExceptionIfBucketMissingAndNotCreating() $mock = new MockPlugin(array(new Response(404))); $client = $this->getClient(); $client->addSubscriber($mock); - $adapter = new AmazonS3($client, 'bucket'); + $adapter = new AwsS3($client, 'bucket'); $adapter->read('foo'); } - public function testDoesNotSupportDirectories() - { - $adapter = new AmazonS3($this->getClient(), 'bucket'); - $this->assertFalse($adapter->isDirectory('foo')); - } - public function testWritesObjects() { $mock = new MockPlugin(array( @@ -62,7 +59,7 @@ public function testWritesObjects() )); $client = $this->getClient(); $client->addSubscriber($mock); - $adapter = new AmazonS3($client, 'bucket'); + $adapter = new AwsS3($client, 'bucket'); $this->assertEquals(7, $adapter->write('foo', 'testing')); $requests = $mock->getReceivedRequests(); $this->assertEquals('bucket.s3.amazonaws.com', $requests[1]->getHost()); @@ -74,7 +71,7 @@ public function testChecksForObjectExistence() $mock = new MockPlugin(array(new Response(200))); $client = $this->getClient(); $client->addSubscriber($mock); - $adapter = new AmazonS3($client, 'bucket'); + $adapter = new AwsS3($client, 'bucket'); $this->assertTrue($adapter->exists('foo')); $requests = $mock->getReceivedRequests(); $this->assertEquals('bucket.s3.amazonaws.com', $requests[0]->getHost()); @@ -85,7 +82,7 @@ public function testChecksForObjectExistence() public function testGetsObjectUrls() { $client = $this->getClient(); - $adapter = new AmazonS3($client, 'bucket'); + $adapter = new AwsS3($client, 'bucket'); $this->assertEquals('https://bucket.s3.amazonaws.com/foo', $adapter->getUrl('foo')); } } From a12c934ace484b35952d880052692f6772de46d2 Mon Sep 17 00:00:00 2001 From: l3l0 Date: Tue, 20 Aug 2013 08:51:36 +0200 Subject: [PATCH 039/426] Add composer lock with azure deps. --- composer.lock | 296 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 294 insertions(+), 2 deletions(-) diff --git a/composer.lock b/composer.lock index 8ace9a55d..c04641440 100644 --- a/composer.lock +++ b/composer.lock @@ -3,7 +3,7 @@ "This file locks the dependencies of your project to a known state", "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" ], - "hash": "d04605350d94fc82797b0ed9bf72dd7d", + "hash": "489a24fff82077ec0b653de82b7406d9", "packages": [ ], @@ -801,6 +801,297 @@ ], "time": "2013-05-24 20:02:36" }, + { + "name": "microsoft/windowsazure", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/WindowsAzure/azure-sdk-for-php.git", + "reference": "d8daa05ad1852e0c70a85d15a6db88f7e3b23405" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WindowsAzure/azure-sdk-for-php/zipball/d8daa05ad1852e0c70a85d15a6db88f7e3b23405", + "reference": "d8daa05ad1852e0c70a85d15a6db88f7e3b23405", + "shasum": "" + }, + "require": { + "pear-pear/http_request2": "*", + "pear-pear/mail_mime": "*", + "pear-pear/mail_mimedecode": "*" + }, + "type": "library", + "autoload": { + "psr-0": { + "WindowsAzure\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Azure PHP SDK", + "email": "azurephpsdk@microsoft.com" + } + ], + "description": "This project provides a set of PHP client libraries that make it easy to access Windows Azure tables, blobs, queues, service runtime and service management APIs.", + "keywords": [ + "azure", + "php", + "sdk" + ], + "time": "2012-12-17 23:41:42" + }, + { + "name": "pear-pear.php.net/Archive_Tar", + "version": "1.3.11", + "dist": { + "type": "file", + "url": "http://pear.php.net/get/Archive_Tar-1.3.11.tgz", + "reference": null, + "shasum": null + }, + "require": { + "php": ">=4.3.0.0" + }, + "replace": { + "pear-pear/archive_tar": "== 1.3.11.0" + }, + "type": "pear-library", + "autoload": { + "classmap": [ + "" + ] + }, + "include-path": [ + "/" + ], + "description": "This class provides handling of tar files in PHP.\nIt supports creating, listing, extracting and adding to tar files.\nGzip support is available if PHP has the zlib extension built-in or\nloaded. Bz2 compression is also supported with the bz2 extension loaded." + }, + { + "name": "pear-pear.php.net/Console_Getopt", + "version": "1.3.1", + "dist": { + "type": "file", + "url": "http://pear.php.net/get/Console_Getopt-1.3.1.tgz", + "reference": null, + "shasum": null + }, + "require": { + "php": ">=4.3.0.0" + }, + "replace": { + "pear-pear/console_getopt": "== 1.3.1.0" + }, + "type": "pear-library", + "autoload": { + "classmap": [ + "" + ] + }, + "include-path": [ + "/" + ], + "description": "This is a PHP implementation of "getopt" supporting both\nshort and long options." + }, + { + "name": "pear-pear.php.net/HTTP_Request2", + "version": "2.1.1", + "dist": { + "type": "file", + "url": "http://pear.php.net/get/HTTP_Request2-2.1.1.tgz", + "reference": null, + "shasum": null + }, + "require": { + "pear-pear.php.net/net_url2": ">=2.0.0.0", + "pear-pear.php.net/pear": ">=1.9.2.0", + "php": ">=5.2.0.0" + }, + "replace": { + "pear-pear/http_request2": "== 2.1.1.0" + }, + "type": "pear-library", + "autoload": { + "classmap": [ + "" + ] + }, + "include-path": [ + "/" + ], + "description": "PHP5 rewrite of HTTP_Request package (with parts of HTTP_Client). Provides\ncleaner API and pluggable Adapters:\n * Socket adapter, based on old HTTP_Request code,\n * Curl adapter, wraps around PHP's cURL extension,\n * Mock adapter, to use for testing packages dependent on HTTP_Request2.\nSupports POST requests with data and file uploads, basic and digest\nauthentication, cookies, managing cookies across requests, proxies, gzip and\ndeflate encodings, redirects, monitoring the request progress with Observers..." + }, + { + "name": "pear-pear.php.net/Mail_Mime", + "version": "1.8.8", + "dist": { + "type": "file", + "url": "http://pear.php.net/get/Mail_Mime-1.8.8.tgz", + "reference": null, + "shasum": null + }, + "require": { + "php": ">=4.3.0.0" + }, + "replace": { + "pear-pear/mail_mime": "== 1.8.8.0" + }, + "type": "pear-library", + "autoload": { + "classmap": [ + "" + ] + }, + "include-path": [ + "/" + ], + "description": "Mail_Mime provides classes to deal with the creation and manipulation of MIME messages.\nIt allows people to create e-mail messages consisting of:\n* Text Parts\n* HTML Parts\n* Inline HTML Images\n* Attachments\n* Attached messages\n\nIt supports big messages, base64 and quoted-printable encodings and\nnon-ASCII characters in filenames, subjects, recipients, etc. encoded\nusing RFC2047 and/or RFC2231." + }, + { + "name": "pear-pear.php.net/Mail_mimeDecode", + "version": "1.5.5", + "dist": { + "type": "file", + "url": "http://pear.php.net/get/Mail_mimeDecode-1.5.5.tgz", + "reference": null, + "shasum": null + }, + "require": { + "pear-pear.php.net/mail_mime": ">1.4.0.0", + "php": ">=4.3.0.0" + }, + "replace": { + "pear-pear/mail_mimedecode": "== 1.5.5.0" + }, + "type": "pear-library", + "autoload": { + "classmap": [ + "" + ] + }, + "include-path": [ + "/" + ], + "description": "Provides a class to deal with the decoding and interpreting of mime messages.\n This package used to be part of the Mail_Mime package, but has been split off." + }, + { + "name": "pear-pear.php.net/Net_URL2", + "version": "2.0.0", + "dist": { + "type": "file", + "url": "http://pear.php.net/get/Net_URL2-2.0.0.tgz", + "reference": null, + "shasum": null + }, + "require": { + "php": ">=5.0.0.0" + }, + "replace": { + "pear-pear/net_url2": "== 2.0.0.0" + }, + "type": "pear-library", + "autoload": { + "classmap": [ + "" + ] + }, + "include-path": [ + "/" + ], + "description": "Provides parsing of URLs into their constituent parts (scheme, host, path etc.), URL generation, and resolving of relative URLs." + }, + { + "name": "pear-pear.php.net/PEAR", + "version": "1.9.4", + "dist": { + "type": "file", + "url": "http://pear.php.net/get/PEAR-1.9.4.tgz", + "reference": null, + "shasum": null + }, + "require": { + "ext-pcre": "*", + "ext-xml": "*", + "pear-pear.php.net/archive_tar": ">=1.3.7.0", + "pear-pear.php.net/console_getopt": ">=1.2.0.0", + "pear-pear.php.net/structures_graph": ">=1.0.2.0", + "pear-pear.php.net/xml_util": ">=1.2.0.0", + "php": ">=4.4.0.0,!=5.0.0.0,!=5.1.0.0,!=5.1.1.0,!=5.1.2.0,!=5.1.3.0,!=5.1.4.0,!=5.1.5.0" + }, + "conflict": { + "pear-pear.php.net/pear_frontend_gtk": "<0.4.0.0", + "pear-pear.php.net/pear_frontend_web": "<=0.4.0.0" + }, + "replace": { + "pear-pear/pear": "== 1.9.4.0" + }, + "type": "pear-library", + "autoload": { + "classmap": [ + "" + ] + }, + "include-path": [ + "/" + ], + "description": "The PEAR package contains:\n * the PEAR installer, for creating, distributing\n and installing packages\n * the PEAR_Exception PHP5 error handling mechanism\n * the PEAR_ErrorStack advanced error handling mechanism\n * the PEAR_Error error handling mechanism\n * the OS_Guess class for retrieving info about the OS\n where PHP is running on\n * the System class for quick handling of common operations\n with files and directories\n * the PEAR base class\n Features in a nutshell:\n * full support for channels\n * pre-download dependency validation\n * new package.xml 2.0 format allows tremendous flexibility while maintaining BC\n * support for optional dependency groups and limited support for sub-packaging\n * robust dependency support\n * full dependency validation on uninstall\n * remote install for hosts with only ftp access - no more problems with\n restricted host installation\n * full support for mirroring\n * support for bundling several packages into a single tarball\n * support for static dependencies on a url-based package\n * support for custom file roles and installation tasks" + }, + { + "name": "pear-pear.php.net/Structures_Graph", + "version": "1.0.4", + "dist": { + "type": "file", + "url": "http://pear.php.net/get/Structures_Graph-1.0.4.tgz", + "reference": null, + "shasum": null + }, + "require": { + "php": ">=4.2.0.0" + }, + "replace": { + "pear-pear/structures_graph": "== 1.0.4.0" + }, + "type": "pear-library", + "autoload": { + "classmap": [ + "" + ] + }, + "include-path": [ + "/" + ], + "description": "Structures_Graph is a package for creating and manipulating graph datastructures. It allows building of directed\nand undirected graphs, with data and metadata stored in nodes. The library provides functions for graph traversing\nas well as for characteristic extraction from the graph topology." + }, + { + "name": "pear-pear.php.net/XML_Util", + "version": "1.2.1", + "dist": { + "type": "file", + "url": "http://pear.php.net/get/XML_Util-1.2.1.tgz", + "reference": null, + "shasum": null + }, + "require": { + "ext-pcre": "*", + "php": ">=4.3.0.0" + }, + "replace": { + "pear-pear/xml_util": "== 1.2.1.0" + }, + "type": "pear-library", + "autoload": { + "classmap": [ + "" + ] + }, + "include-path": [ + "/" + ], + "description": "Selection of methods that are often needed when working with XML documents. Functionality includes creating of attribute lists from arrays, creation of tags, validation of XML names and more." + }, { "name": "phpseclib/phpseclib", "version": "dev-master", @@ -1677,7 +1968,8 @@ "minimum-stability": "dev", "stability-flags": { "rackspace/php-opencloud": 20, - "phpseclib/phpseclib": 20 + "phpseclib/phpseclib": 20, + "microsoft/windowsazure": 20 }, "platform": { "php": ">=5.3.2" From 0b051236d577250e3bbb3593b5cc483413c70ed5 Mon Sep 17 00:00:00 2001 From: Toni Uebernickel Date: Wed, 21 Aug 2013 14:53:40 +0200 Subject: [PATCH 040/426] remove parameters from ensureBucketExists calls --- src/Gaufrette/Adapter/AwsS3.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index ccc922350..88f7e75f3 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -70,7 +70,7 @@ public function getMetadata($key) */ public function read($key) { - $this->ensureBucketExists($key); + $this->ensureBucketExists(); $options = $this->getOptions($key); try { @@ -85,7 +85,7 @@ public function read($key) */ public function rename($sourceKey, $targetKey) { - $this->ensureBucketExists($targetKey); + $this->ensureBucketExists(); $options = $this->getOptions($targetKey, array('CopySource' => $sourceKey)); try { @@ -101,7 +101,7 @@ public function rename($sourceKey, $targetKey) */ public function write($key, $content) { - $this->ensureBucketExists($key); + $this->ensureBucketExists(); $options = $this->getOptions($key, array('Body' => $content)); try { From 0c52526fff726bbf9890510c095cfaf1dcff5e63 Mon Sep 17 00:00:00 2001 From: Toni Uebernickel Date: Wed, 21 Aug 2013 15:45:00 +0200 Subject: [PATCH 041/426] add support for directory prefix --- src/Gaufrette/Adapter/AwsS3.php | 25 +++++++++++++------ .../Functional/Adapter/AwsS3Test.php | 20 +++++++++++++++ 2 files changed, 38 insertions(+), 7 deletions(-) diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index 88f7e75f3..3a8232bb2 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -25,7 +25,7 @@ public function __construct(S3Client $service, $bucket, array $options = array() { $this->service = $service; $this->bucket = $bucket; - $this->options = array_replace(array('create' => false), $options); + $this->options = array_replace(array('create' => false, 'directory' => ''), $options); } /** @@ -43,7 +43,7 @@ public function getUrl($key, array $options = array()) { return $this->service->getObjectUrl( $this->bucket, - $key, + $this->computePath($key), isset($options['expires']) ? $options['expires'] : null, $options ); @@ -86,7 +86,7 @@ public function read($key) public function rename($sourceKey, $targetKey) { $this->ensureBucketExists(); - $options = $this->getOptions($targetKey, array('CopySource' => $sourceKey)); + $options = $this->getOptions($targetKey, array('CopySource' => $this->computePath($sourceKey))); try { $this->service->copyObject($options); @@ -117,7 +117,7 @@ public function write($key, $content) */ public function exists($key) { - return $this->service->doesObjectExist($this->bucket, $key); + return $this->service->doesObjectExist($this->bucket, $this->computePath($key)); } /** @@ -148,7 +148,9 @@ public function listKeys($prefix = '') { $options = array('Bucket' => $this->bucket); if ((string) $prefix != '') { - $options['Prefix'] = $prefix; + $options['Prefix'] = $this->computePath($prefix); + } elseif (!empty($this->options['directory'])) { + $options['Prefix'] = $this->options['directory']; } $keys = array(); @@ -180,7 +182,7 @@ public function isDirectory($key) { $result = $this->service->listObjects(array( 'Bucket' => $this->bucket, - 'Prefix' => rtrim($key, '/') . '/', + 'Prefix' => rtrim($this->computePath($key), '/') . '/', 'MaxKeys' => 1 )); @@ -227,8 +229,17 @@ private function ensureBucketExists() private function getOptions($key, array $options = array()) { $options['Bucket'] = $this->bucket; - $options['Key'] = $key; + $options['Key'] = $this->computePath($key); return $options + $this->getMetadata($key); } + + private function computePath($key) + { + if (empty($this->options['directory'])) { + return $key; + } + + return sprintf('%s/%s', $this->options['directory'], $key); + } } diff --git a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php index e90bfc63c..5e23be512 100644 --- a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php +++ b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php @@ -85,4 +85,24 @@ public function testGetsObjectUrls() $adapter = new AwsS3($client, 'bucket'); $this->assertEquals('https://bucket.s3.amazonaws.com/foo', $adapter->getUrl('foo')); } + + public function testChecksForObjectExistenceWithDirectory() + { + $mock = new MockPlugin(array(new Response(200))); + $client = $this->getClient(); + $client->addSubscriber($mock); + $adapter = new AwsS3($client, 'bucket', array('directory' => 'bar')); + $this->assertTrue($adapter->exists('foo')); + $requests = $mock->getReceivedRequests(); + $this->assertEquals('bucket.s3.amazonaws.com', $requests[0]->getHost()); + $this->assertEquals('HEAD', $requests[0]->getMethod()); + $this->assertEquals('/bar/foo', $requests[0]->getResource()); + } + + public function testGetsObjectUrlsWithDirectory() + { + $client = $this->getClient(); + $adapter = new AwsS3($client, 'bucket', array('directory' => 'bar')); + $this->assertEquals('https://bucket.s3.amazonaws.com/bar/foo', $adapter->getUrl('foo')); + } } From 35d50694e3e086a717935b868f361ab71a06f616 Mon Sep 17 00:00:00 2001 From: Luciano Mammino Date: Fri, 23 Aug 2013 16:58:05 +0200 Subject: [PATCH 042/426] Fixed Broken parsing of some errors in AzureBlobStorage adapter Sometimes Azure Blob Storage returns HTML 4.0 messages as error message so they can't be correctly parsed with `simplexml_load_string`. This pull request fixes the problem. --- src/Gaufrette/Adapter/AzureBlobStorage.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Gaufrette/Adapter/AzureBlobStorage.php b/src/Gaufrette/Adapter/AzureBlobStorage.php index 612a98cc5..0d49e2025 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage.php @@ -374,9 +374,9 @@ protected function failIfContainerNotFound(ServiceException $exception, $action) */ protected function getErrorCodeFromServiceException(ServiceException $exception) { - $xml = simplexml_load_string($exception->getErrorReason()); + $xml = @simplexml_load_string($exception->getErrorReason()); - if (isset($xml->Code)) { + if ($xml && isset($xml->Code)) { return (string) $xml->Code; } From 43a4721e9b9c0525f1a5f02ee834b32ff6abd761 Mon Sep 17 00:00:00 2001 From: Yohann Valentin Date: Thu, 29 Aug 2013 17:53:15 +0200 Subject: [PATCH 043/426] createDirectory return false on sftp.php class Each time that a new folder is created on the sftp server, mkdir function return false. Nevertheless the folder is created on the sftp server. The lib php-ssh propose the mkdir functionnality to use directly ssh2_sftp_mkdir. With this, the function createDirectory return the good value (true) if the folder has been created properly. --- src/Gaufrette/Adapter/Sftp.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/Sftp.php b/src/Gaufrette/Adapter/Sftp.php index 73cb76d50..d2f8cb0f9 100644 --- a/src/Gaufrette/Adapter/Sftp.php +++ b/src/Gaufrette/Adapter/Sftp.php @@ -218,6 +218,6 @@ protected function ensureDirectoryExists($directory, $create = false) */ protected function createDirectory($directory) { - return mkdir($this->sftp->getUrl($directory), 0777, true); + return $this->sftp->mkdir($directory, 0777, true); } } From 5084853f18fab2ed27e5237ec9a9a105855f07ba Mon Sep 17 00:00:00 2001 From: Chirs Warner Date: Wed, 2 Oct 2013 14:48:15 -0400 Subject: [PATCH 044/426] Created LazyOpenCloud Connection. Created Connection Factories to Allow LazyOpenCloud Connections to work. Created Unit tests for OpenCloud and LazyOpenCloud Added Documentation to README.md for LazyOpenCloud. Set the dev revision for rackspace/opencloud to version 1.6.* Fixed Serveral bugs within the OpenCloud adapater class that would have throw fatal php errors about calling a method on a none object. --- README.markdown | 22 + composer.json | 2 +- composer.lock | 190 +++--- .../Adapter/LazyOpenStackCloudFiles.php | 111 ++++ src/Gaufrette/Adapter/OpenCloud.php | 38 +- .../BaseOpenStackAuthenciationFactory.php | 32 + .../ConnectionFactoryInterface.php | 16 + .../OpenStackAuthenticationFactory.php | 30 + ...ckspaceAuthenticationConnectionFactory.php | 37 ++ .../Gaufrette/Unit/BaseOpenCloudTestCase.php | 582 ++++++++++++++++++ .../Unit/LazyOpenStackCloudFilesTest.php | 38 ++ tests/Gaufrette/Unit/OpenCloudTest.php | 20 + 12 files changed, 1017 insertions(+), 101 deletions(-) create mode 100644 src/Gaufrette/Adapter/LazyOpenStackCloudFiles.php create mode 100644 src/Gaufrette/Adapter/OpenStackCloudFiles/BaseOpenStackAuthenciationFactory.php create mode 100644 src/Gaufrette/Adapter/OpenStackCloudFiles/ConnectionFactoryInterface.php create mode 100644 src/Gaufrette/Adapter/OpenStackCloudFiles/OpenStackAuthenticationFactory.php create mode 100644 src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationConnectionFactory.php create mode 100644 tests/Gaufrette/Unit/BaseOpenCloudTestCase.php create mode 100644 tests/Gaufrette/Unit/LazyOpenStackCloudFilesTest.php create mode 100644 tests/Gaufrette/Unit/OpenCloudTest.php diff --git a/README.markdown b/README.markdown index 130bbab07..18060f85a 100644 --- a/README.markdown +++ b/README.markdown @@ -154,6 +154,28 @@ $connection = new OpenCloud\Rackspace( ``` +### LazyOpenCloud + +...php +$factory = new Gaufrette\Adapter\OpenStackAuthenticationFactory( + $url, // connection URL from cloud Vendor + $apikey, // password + $username, //username + $region, // region + $tenant) // tenant +$adapter = new Gaufrette\Adapter\LazyOpenStackCloudFiles($factory, 'container-name'); +... + +### LazyOpenCloud on Rackspace +...php +$factory = new Gaufrette\Adapter\RackspaceAuthenticationFactory( + $url, // connection URL from cloud Vendor + $apikey, // password + $username, //username + $region, // region + $tenant) // tenant + + Using AzureBlobStorage ---------------------- Azure Blob Storage is the storage service provided by Microsoft Windows Azure cloud environment. To use this adapter diff --git a/composer.json b/composer.json index c4694dd10..a9fca2246 100644 --- a/composer.json +++ b/composer.json @@ -47,7 +47,7 @@ "require-dev": { "aws/aws-sdk-php": "~2", "amazonwebservices/aws-sdk-for-php": "1.5.*", - "rackspace/php-opencloud" : "dev-master", + "rackspace/php-opencloud" : "1.6.*", "phpspec/phpspec": "2.0.*", "phpseclib/phpseclib": "dev-master", "rackspace/php-cloudfiles": "*", diff --git a/composer.lock b/composer.lock index c04641440..347978188 100644 --- a/composer.lock +++ b/composer.lock @@ -3,7 +3,7 @@ "This file locks the dependencies of your project to a known state", "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" ], - "hash": "489a24fff82077ec0b653de82b7406d9", + "hash": "7b64d4c027c38f15e08ce5dcbed5bfbf", "packages": [ ], @@ -14,12 +14,12 @@ "source": { "type": "git", "url": "https://github.com/amazonwebservices/aws-sdk-for-php.git", - "reference": "1.5.17.1" + "reference": "42d86270e9458acffff721a661f62d818ca7282f" }, "dist": { "type": "zip", - "url": "https://github.com/amazonwebservices/aws-sdk-for-php/archive/1.5.17.1.zip", - "reference": "1.5.17.1", + "url": "https://api.github.com/repos/amazonwebservices/aws-sdk-for-php/zipball/42d86270e9458acffff721a661f62d818ca7282f", + "reference": "42d86270e9458acffff721a661f62d818ca7282f", "shasum": "" }, "require": { @@ -64,12 +64,12 @@ "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "cd986623f0c5631337a30159d0d1c8953602c933" + "reference": "c5da9510480f719361e881a84bfc7ed06716b159" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/cd986623f0c5631337a30159d0d1c8953602c933", - "reference": "cd986623f0c5631337a30159d0d1c8953602c933", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c5da9510480f719361e881a84bfc7ed06716b159", + "reference": "c5da9510480f719361e881a84bfc7ed06716b159", "shasum": "" }, "require": { @@ -122,7 +122,7 @@ "s3", "sdk" ], - "time": "2013-07-22 21:22:56" + "time": "2013-10-01 00:08:29" }, { "name": "doctrine/annotations", @@ -130,12 +130,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "v1.1.2" + "reference": "40db0c96985aab2822edbc4848b3bd2429e02670" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/v1.1.2", - "reference": "v1.1.2", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/40db0c96985aab2822edbc4848b3bd2429e02670", + "reference": "40db0c96985aab2822edbc4848b3bd2429e02670", "shasum": "" }, "require": { @@ -197,16 +197,16 @@ }, { "name": "doctrine/cache", - "version": "dev-master", + "version": "v1.2.0", "source": { "type": "git", "url": "https://github.com/doctrine/cache.git", - "reference": "dc3438804c6303f1b5892f184a79aa8e3ce5d81d" + "reference": "d0e4447707a064a5814b18cb0dcc2f24185f6179" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/dc3438804c6303f1b5892f184a79aa8e3ce5d81d", - "reference": "dc3438804c6303f1b5892f184a79aa8e3ce5d81d", + "url": "https://api.github.com/repos/doctrine/cache/zipball/d0e4447707a064a5814b18cb0dcc2f24185f6179", + "reference": "d0e4447707a064a5814b18cb0dcc2f24185f6179", "shasum": "" }, "require": { @@ -262,7 +262,7 @@ "cache", "caching" ], - "time": "2013-07-04 15:48:06" + "time": "2013-09-26 19:23:25" }, { "name": "doctrine/collections", @@ -270,12 +270,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "623caf42551b416460df786154d89bc50e3d1798" + "reference": "bcb53776a096a0c64579cc8d8ec0db62f1109fbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/623caf42551b416460df786154d89bc50e3d1798", - "reference": "623caf42551b416460df786154d89bc50e3d1798", + "url": "https://api.github.com/repos/doctrine/collections/zipball/bcb53776a096a0c64579cc8d8ec0db62f1109fbc", + "reference": "bcb53776a096a0c64579cc8d8ec0db62f1109fbc", "shasum": "" }, "require": { @@ -329,20 +329,20 @@ "collections", "iterator" ], - "time": "2013-07-23 08:27:26" + "time": "2013-08-29 16:56:45" }, { "name": "doctrine/common", - "version": "dev-master", + "version": "2.4.x-dev", "source": { "type": "git", "url": "https://github.com/doctrine/common.git", - "reference": "6a39bb947b20d12a16820f2709509e41e28af005" + "reference": "c94d6ff79e25418b1225e187c782bf4742f23a8b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/common/zipball/6a39bb947b20d12a16820f2709509e41e28af005", - "reference": "6a39bb947b20d12a16820f2709509e41e28af005", + "url": "https://api.github.com/repos/doctrine/common/zipball/c94d6ff79e25418b1225e187c782bf4742f23a8b", + "reference": "c94d6ff79e25418b1225e187c782bf4742f23a8b", "shasum": "" }, "require": { @@ -403,7 +403,7 @@ "persistence", "spl" ], - "time": "2013-07-02 11:26:20" + "time": "2013-09-07 10:20:35" }, { "name": "doctrine/dbal", @@ -411,16 +411,16 @@ "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "2.4.0-RC2" + "reference": "a67755c2726360ee697bae6105d29dcffb9f000c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/2.4.0-RC2", - "reference": "2.4.0-RC2", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/a67755c2726360ee697bae6105d29dcffb9f000c", + "reference": "a67755c2726360ee697bae6105d29dcffb9f000c", "shasum": "" }, "require": { - "doctrine/common": "2.4.*@beta", + "doctrine/common": "2.4.*", "php": ">=5.3.2" }, "require-dev": { @@ -433,7 +433,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.4.x-dev" + "dev-master": "2.5.x-dev" } }, "autoload": { @@ -473,7 +473,7 @@ "persistence", "queryobject" ], - "time": "2013-06-25 17:49:29" + "time": "2013-10-02 10:10:08" }, { "name": "doctrine/inflector", @@ -529,7 +529,7 @@ { "name": "Johannes Schmitt", "email": "schmittjoh@gmail.com", - "homepage": "http://jmsyst.com", + "homepage": "https://github.com/schmittjoh", "role": "Developer of wrapped JMSSerializerBundle" } ], @@ -635,7 +635,7 @@ { "name": "Evert Pot", "email": "evert@rooftopsolutions.nl", - "homepage": "http://evertpot.com/", + "homepage": "http://www.rooftopsolutions.nl/", "role": "Developer" }, { @@ -666,17 +666,17 @@ "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "9faeeaf4baa2c9e1c1c4848eea62818eeaa54f02" + "reference": "e024996037f14d72ce963074edd21600827a55cc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/9faeeaf4baa2c9e1c1c4848eea62818eeaa54f02", - "reference": "9faeeaf4baa2c9e1c1c4848eea62818eeaa54f02", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/e024996037f14d72ce963074edd21600827a55cc", + "reference": "e024996037f14d72ce963074edd21600827a55cc", "shasum": "" }, "require": { "ext-curl": "*", - "php": ">=5.3.2", + "php": ">=5.3.3", "symfony/event-dispatcher": ">=2.1" }, "replace": { @@ -750,7 +750,7 @@ "rest", "web service" ], - "time": "2013-07-18 20:50:44" + "time": "2013-10-01 17:27:14" }, { "name": "herzult/php-ssh", @@ -1098,12 +1098,12 @@ "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "bf6da217c19514506060d98797a53dc1a78c807e" + "reference": "77447a3fc7869773007fa3beeb8d6f8de842ec2a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/bf6da217c19514506060d98797a53dc1a78c807e", - "reference": "bf6da217c19514506060d98797a53dc1a78c807e", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/77447a3fc7869773007fa3beeb8d6f8de842ec2a", + "reference": "77447a3fc7869773007fa3beeb8d6f8de842ec2a", "shasum": "" }, "require": { @@ -1164,7 +1164,7 @@ "x.509", "x509" ], - "time": "2013-07-23 04:53:45" + "time": "2013-09-20 17:14:01" }, { "name": "phpspec/php-diff", @@ -1172,12 +1172,12 @@ "source": { "type": "git", "url": "https://github.com/phpspec/php-diff.git", - "reference": "v1.0.1" + "reference": "8d82ac415225fac373a4073ba14b1fe286aa2312" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/php-diff/zipball/v1.0.1", - "reference": "v1.0.1", + "url": "https://api.github.com/repos/phpspec/php-diff/zipball/8d82ac415225fac373a4073ba14b1fe286aa2312", + "reference": "8d82ac415225fac373a4073ba14b1fe286aa2312", "shasum": "" }, "type": "library", @@ -1205,12 +1205,12 @@ "source": { "type": "git", "url": "https://github.com/phpspec/phpspec.git", - "reference": "00cbb43e1db5e015330f9f754a239459b99af1de" + "reference": "79f91276fc8c853596b8180103d93762072e457a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/phpspec/zipball/00cbb43e1db5e015330f9f754a239459b99af1de", - "reference": "00cbb43e1db5e015330f9f754a239459b99af1de", + "url": "https://api.github.com/repos/phpspec/phpspec/zipball/79f91276fc8c853596b8180103d93762072e457a", + "reference": "79f91276fc8c853596b8180103d93762072e457a", "shasum": "" }, "require": { @@ -1265,7 +1265,7 @@ "testing", "tests" ], - "time": "2013-07-15 16:25:26" + "time": "2013-09-02 20:20:07" }, { "name": "phpspec/prophecy", @@ -1273,12 +1273,12 @@ "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "27f73e6ed956c401f9464b9215d5000ce82c9d42" + "reference": "82d11835df7eccf904ea0c84fbb7304080346bc1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/27f73e6ed956c401f9464b9215d5000ce82c9d42", - "reference": "27f73e6ed956c401f9464b9215d5000ce82c9d42", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/82d11835df7eccf904ea0c84fbb7304080346bc1", + "reference": "82d11835df7eccf904ea0c84fbb7304080346bc1", "shasum": "" }, "require-dev": { @@ -1320,7 +1320,7 @@ "spy", "stub" ], - "time": "2013-07-04 22:33:32" + "time": "2013-09-30 08:21:23" }, { "name": "phpunit/php-code-coverage", @@ -1328,12 +1328,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "d274ffb5a9899ab71c769924071fb8aa09852fb6" + "reference": "466e7cd2554b4e264c9e3f31216d25ac0e5f3d94" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/d274ffb5a9899ab71c769924071fb8aa09852fb6", - "reference": "d274ffb5a9899ab71c769924071fb8aa09852fb6", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/466e7cd2554b4e264c9e3f31216d25ac0e5f3d94", + "reference": "466e7cd2554b4e264c9e3f31216d25ac0e5f3d94", "shasum": "" }, "require": { @@ -1381,7 +1381,7 @@ "testing", "xunit" ], - "time": "2013-07-13 13:31:16" + "time": "2013-09-10 08:14:32" }, { "name": "phpunit/php-file-iterator", @@ -1474,16 +1474,16 @@ }, { "name": "phpunit/php-timer", - "version": "1.0.x-dev", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "ecf7920b27003a9412b07dad79dbb5ad1249e6c3" + "reference": "19689d4354b295ee3d8c54b4f42c3efb69cbc17c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/ecf7920b27003a9412b07dad79dbb5ad1249e6c3", - "reference": "ecf7920b27003a9412b07dad79dbb5ad1249e6c3", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/19689d4354b295ee3d8c54b4f42c3efb69cbc17c", + "reference": "19689d4354b295ee3d8c54b4f42c3efb69cbc17c", "shasum": "" }, "require": { @@ -1514,7 +1514,7 @@ "keywords": [ "timer" ], - "time": "2013-01-30 06:08:51" + "time": "2013-08-02 07:42:54" }, { "name": "phpunit/php-token-stream", @@ -1522,12 +1522,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "c25dd88e1592e66dee2553c99ef244203d5a1b98" + "reference": "5220af2a7929aa35cf663d97c89ad3d50cf5fa3e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/c25dd88e1592e66dee2553c99ef244203d5a1b98", - "reference": "c25dd88e1592e66dee2553c99ef244203d5a1b98", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/5220af2a7929aa35cf663d97c89ad3d50cf5fa3e", + "reference": "5220af2a7929aa35cf663d97c89ad3d50cf5fa3e", "shasum": "" }, "require": { @@ -1535,6 +1535,11 @@ "php": ">=5.3.3" }, "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.2-dev" + } + }, "autoload": { "classmap": [ "PHP/" @@ -1559,7 +1564,7 @@ "keywords": [ "tokenizer" ], - "time": "2013-01-07 10:56:35" + "time": "2013-09-13 04:58:23" }, { "name": "phpunit/phpunit", @@ -1567,12 +1572,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "3.7.22" + "reference": "4b024e753e3421837afbcca962c8724c58b39376" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3.7.22", - "reference": "3.7.22", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4b024e753e3421837afbcca962c8724c58b39376", + "reference": "4b024e753e3421837afbcca962c8724c58b39376", "shasum": "" }, "require": { @@ -1584,7 +1589,7 @@ "phpunit/php-code-coverage": "~1.2.1", "phpunit/php-file-iterator": ">=1.3.1", "phpunit/php-text-template": ">=1.1.1", - "phpunit/php-timer": "~1.0.2", + "phpunit/php-timer": ">=1.0.4", "phpunit/phpunit-mock-objects": "~1.2.0", "symfony/yaml": "~2.0" }, @@ -1633,7 +1638,7 @@ "testing", "xunit" ], - "time": "2013-07-06 06:29:15" + "time": "2013-09-16 03:09:52" }, { "name": "phpunit/phpunit-mock-objects", @@ -1717,16 +1722,16 @@ }, { "name": "rackspace/php-opencloud", - "version": "dev-master", + "version": "V1.6.0", "source": { "type": "git", "url": "https://github.com/rackspace/php-opencloud.git", - "reference": "80b4929d600a6638b4f31d5a8f6a455d2ac5f645" + "reference": "7be280fde422651d0966c70b07f6477b37dd4270" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rackspace/php-opencloud/zipball/80b4929d600a6638b4f31d5a8f6a455d2ac5f645", - "reference": "80b4929d600a6638b4f31d5a8f6a455d2ac5f645", + "url": "https://api.github.com/repos/rackspace/php-opencloud/zipball/7be280fde422651d0966c70b07f6477b37dd4270", + "reference": "7be280fde422651d0966c70b07f6477b37dd4270", "shasum": "" }, "require": { @@ -1758,7 +1763,7 @@ "cloudfiles", "opencloud" ], - "time": "2013-07-23 18:28:55" + "time": "2013-09-03 15:49:02" }, { "name": "symfony/console", @@ -1767,12 +1772,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/Console.git", - "reference": "470446b34c029c7cd1933a55c9bdd887b9f5ec0d" + "reference": "ed9b8a905b21e3d4bab8b6e0e016a076aefcc5bc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Console/zipball/470446b34c029c7cd1933a55c9bdd887b9f5ec0d", - "reference": "470446b34c029c7cd1933a55c9bdd887b9f5ec0d", + "url": "https://api.github.com/repos/symfony/Console/zipball/ed9b8a905b21e3d4bab8b6e0e016a076aefcc5bc", + "reference": "ed9b8a905b21e3d4bab8b6e0e016a076aefcc5bc", "shasum": "" }, "require": { @@ -1811,7 +1816,7 @@ ], "description": "Symfony Console Component", "homepage": "http://symfony.com", - "time": "2013-07-21 20:19:01" + "time": "2013-10-02 07:34:37" }, { "name": "symfony/event-dispatcher", @@ -1820,12 +1825,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/EventDispatcher.git", - "reference": "b8bca82c9347de810aa00898684adbf9b601eb5d" + "reference": "9b4fe5757870682eb2251e283228a66d938265a8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/b8bca82c9347de810aa00898684adbf9b601eb5d", - "reference": "b8bca82c9347de810aa00898684adbf9b601eb5d", + "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/9b4fe5757870682eb2251e283228a66d938265a8", + "reference": "9b4fe5757870682eb2251e283228a66d938265a8", "shasum": "" }, "require": { @@ -1865,7 +1870,7 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "http://symfony.com", - "time": "2013-07-21 20:19:01" + "time": "2013-09-19 09:47:34" }, { "name": "symfony/finder", @@ -1874,12 +1879,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/Finder.git", - "reference": "6b6564a27b23daa5fa3dc12db7a4b0eeabf13f1f" + "reference": "e2ce3164ab58b4d54612e630571f158035ee8603" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Finder/zipball/6b6564a27b23daa5fa3dc12db7a4b0eeabf13f1f", - "reference": "6b6564a27b23daa5fa3dc12db7a4b0eeabf13f1f", + "url": "https://api.github.com/repos/symfony/Finder/zipball/e2ce3164ab58b4d54612e630571f158035ee8603", + "reference": "e2ce3164ab58b4d54612e630571f158035ee8603", "shasum": "" }, "require": { @@ -1912,7 +1917,7 @@ ], "description": "Symfony Finder Component", "homepage": "http://symfony.com", - "time": "2013-07-21 20:19:01" + "time": "2013-09-19 09:47:34" }, { "name": "symfony/yaml", @@ -1921,12 +1926,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/Yaml.git", - "reference": "4d80b4f4c447e8ad7cab8b3afb0b51aad0431f7f" + "reference": "ef4878bcca171ebe7466be0505b85a710818d788" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Yaml/zipball/4d80b4f4c447e8ad7cab8b3afb0b51aad0431f7f", - "reference": "4d80b4f4c447e8ad7cab8b3afb0b51aad0431f7f", + "url": "https://api.github.com/repos/symfony/Yaml/zipball/ef4878bcca171ebe7466be0505b85a710818d788", + "reference": "ef4878bcca171ebe7466be0505b85a710818d788", "shasum": "" }, "require": { @@ -1959,7 +1964,7 @@ ], "description": "Symfony Yaml Component", "homepage": "http://symfony.com", - "time": "2013-07-21 20:19:01" + "time": "2013-09-22 18:04:51" } ], "aliases": [ @@ -1967,7 +1972,6 @@ ], "minimum-stability": "dev", "stability-flags": { - "rackspace/php-opencloud": 20, "phpseclib/phpseclib": 20, "microsoft/windowsazure": 20 }, diff --git a/src/Gaufrette/Adapter/LazyOpenStackCloudFiles.php b/src/Gaufrette/Adapter/LazyOpenStackCloudFiles.php new file mode 100644 index 000000000..02f3c7a1e --- /dev/null +++ b/src/Gaufrette/Adapter/LazyOpenStackCloudFiles.php @@ -0,0 +1,111 @@ +connectionFactory = $connectionFactory; + $this->containerName = $containerName; + $this->createContainer = $createContainer; + $this->detectContentType = $detectContentType; + + } + + public function read($key) + { + $this->connect(); + return parent::read($key); // TODO: Change the autogenerated stub + } + + public function write($key, $content) + { + $this->connect(); + return parent::write($key, $content); // TODO: Change the autogenerated stub + } + + public function exists($key) + { + $this->connect(); + return parent::exists($key); // TODO: Change the autogenerated stub + } + + public function keys() + { + $this->connect(); + return parent::keys(); // TODO: Change the autogenerated stub + } + + public function delete($key) + { + $this->connect(); + return parent::delete($key); // TODO: Change the autogenerated stub + } + + public function rename($sourceKey, $targetKey) + { + $this->connect(); + parent::rename($sourceKey, $targetKey); // TODO: Change the autogenerated stub + } + + public function isDirectory($key) + { + $this->connect(); + return parent::isDirectory($key); // TODO: Change the autogenerated stub + } + + public function checksum($key) + { + $this->connect(); + return parent::checksum($key); // TODO: Change the autogenerated stub + } + + public function mtime($key) + { + $this->connect(); + return parent::mtime($key); // TODO: Change the autogenerated stub + } + + + protected function connect() + { + if(!$this->connected){ + /** @var OpenStack $connection */ + $connection = $this->connectionFactory->create(); + + $objectStore = $connection->objectStore(); + + $this->objectStore = $objectStore; + } + } + +} diff --git a/src/Gaufrette/Adapter/OpenCloud.php b/src/Gaufrette/Adapter/OpenCloud.php index 343c06631..ecdc64ddf 100644 --- a/src/Gaufrette/Adapter/OpenCloud.php +++ b/src/Gaufrette/Adapter/OpenCloud.php @@ -3,7 +3,8 @@ namespace Gaufrette\Adapter; use Gaufrette\Adapter; -use OpenCloud\ObjectStore\Container; +use OpenCloud\Common\Exceptions\DeleteError; +use OpenCloud\ObjectStore\Resource\Container; use OpenCloud\ObjectStore\Service; use OpenCloud\Common\Exceptions\CreateUpdateError; use OpenCloud\Common\Exceptions\ObjFetchError; @@ -71,7 +72,16 @@ private function initialize() public function read($key) { $this->initialize(); - return $this->tryGetObject($key)->SaveToString(); + + // This method can return boolean or the object + // if there is a fetch error, tryGetObject returns false + // If it returns false, php throws a fatal error because boolean is a non-object. + $object = $this->tryGetObject($key); + if($object) + { + return $object->SaveToString(); + } + return $object; } /** @@ -148,7 +158,11 @@ public function keys() public function mtime($key) { $this->initialize(); - $lastModified = $this->tryGetObject($key)->last_modified; + + $object = $this->tryGetObject($key); + + $lastModified = $object?$object->last_modified:$object; + return $lastModified; } @@ -163,8 +177,14 @@ public function delete($key) { $this->initialize(); try{ - $this->tryGetObject($key)->Delete(); - }catch (ObjectStore\DeleteError $deleteError){ + $object = $this->tryGetObject($key); + if($object){ + $object->Delete(); + } else { + return false; + } + }catch (DeleteError $deleteError){ + return false; } return true; @@ -207,12 +227,16 @@ public function isDirectory($key) public function checksum($key) { $this->initialize(); - return $this->tryGetObject($key)->getETag(); + $object = $this->tryGetObject($key); + if($object) + return $object->getETag(); + + return false; } /** * @param $key - * @return \OpenCloud\ObjectStore\DataObject + * @return \OpenCloud\ObjectStore\Resource\DataObject */ protected function tryGetObject($key) { diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/BaseOpenStackAuthenciationFactory.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/BaseOpenStackAuthenciationFactory.php new file mode 100644 index 000000000..cddc43df5 --- /dev/null +++ b/src/Gaufrette/Adapter/OpenStackCloudFiles/BaseOpenStackAuthenciationFactory.php @@ -0,0 +1,32 @@ +apikey = $apikey; + $this->region = $region; + $this->tenant = $tenant; + $this->url = $url; + $this->username = $username; + } + +} diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/ConnectionFactoryInterface.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/ConnectionFactoryInterface.php new file mode 100644 index 000000000..90bd807bc --- /dev/null +++ b/src/Gaufrette/Adapter/OpenStackCloudFiles/ConnectionFactoryInterface.php @@ -0,0 +1,16 @@ +authenciationService) + { + $this->authenciationService = new OpenStack($this->url, array($this->username, $this->apikey)); + $this->authenciationService->authenticate(); + $this->authenciationService->setDefaults('cloudFiles', null, $this->region); + } elseif($this->authenciationService->hasExpired()) { + $this->authenciationService->authenticate(); + $this->authenciationService->setDefaults('cloudFiles', null, $this->region); + } + return $this->authenciationService; + + } + +} diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationConnectionFactory.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationConnectionFactory.php new file mode 100644 index 000000000..609f661d7 --- /dev/null +++ b/src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationConnectionFactory.php @@ -0,0 +1,37 @@ +authenciationService) + { + $this->authenciationService = new Rackspace($this->url, array($this->username, $this->apikey)); + $this->authenciationService->authenticate(); + + + } elseif($this->authenciationService->expired()) { + $this->authenciationService->authenticate(); + } + + return $this->authenciationService; + } + +} diff --git a/tests/Gaufrette/Unit/BaseOpenCloudTestCase.php b/tests/Gaufrette/Unit/BaseOpenCloudTestCase.php new file mode 100644 index 000000000..50374651d --- /dev/null +++ b/tests/Gaufrette/Unit/BaseOpenCloudTestCase.php @@ -0,0 +1,582 @@ +getMock( + 'OpenCloud\ObjectStore\Service', + array('Container', 'Create'), + array(), '', false, false, false, false); + + $container = $this->getMock( + 'OpenCloud\ObjectStore\Resource\Container', + array('DataObject'), + array(), '', false, false, false, false); + + $dataObject = $this->getMock( + 'OpenCloud\OpenStore\Resource\DataObject', + array('SaveToString'), + array(), '', false, false, false, false + ); + + $dataObject->expects($this->once()) + ->method('SaveToString') + ->will($this->returnValue('test')); + + $container->expects($this->once()) + ->method('DataObject') + ->with($this->equalTo('test')) + ->will($this->returnValue($dataObject)); + + $objectStore->expects($this->once()) + ->method('Container') + ->will($this->returnValue($container)); + + $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); + $result = $sut->read('test'); + + $this->assertSame('test', $result); + } + + public function testRead_KeyDoesNotExists_ReturnsFalse() + { + $objectStore = $this->getMock( + 'OpenCloud\ObjectStore\Service', + array('Container', 'Create'), + array(), '', false, false, false, false); + + $container = $this->getMock( + 'OpenCloud\ObjectStore\Resource\Container', + array('DataObject'), + array(), '', false, false, false, false); + + $container->expects($this->once()) + ->method('DataObject') + ->with($this->equalTo('test')) + ->will($this->throwException(new ObjFetchError())); + + $objectStore->expects($this->once()) + ->method('Container') + ->will($this->returnValue($container)); + + $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); + $result = $sut->read('test'); + + $this->assertFalse($result); + + } + + public function testWrite_keyDoesNotExist_ReturnsXbytes(){ + + $objectStore = $this->getMock( + 'OpenCloud\ObjectStore\Service', + array('Container', 'Create'), + array(), '', false, false, false, false); + + $container = $this->getMock( + 'OpenCloud\ObjectStore\Resource\Container', + array('DataObject'), + array(), '', false, false, false, false); + + $dataObject = $this->getMock( + 'OpenCloud\OpenStore\Resource\DataObject', + array('SetData', 'Create'), + array(), '', false, false, false, false + ); + + $content = "The quick brown fox jumps over a lazy dog"; + $data = array('name'=>'test', 'content_type' => 'text/plain'); + $dataBytes = sizeof($content); + + $dataObject->bytes = $dataBytes; + + $dataObject->expects($this->once()) + ->method('SetData') + ->with($this->equalTo($content)); + + $dataObject->expects($this->once()) + ->method('Create') + ->with($this->equalTo($data)); + + $container->expects($this->exactly(2)) + ->method('DataObject') + ->will($this->onConsecutiveCalls(false, $dataObject)); + + $objectStore->expects($this->once()) + ->method('Container') + ->will($this->returnValue($container)); + + $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); + + $bytes = $sut->write('test', $content); + + $this->assertEquals($dataBytes, $bytes); + + + } + + public function testWrite_keyExists_ReturnsXbytes(){ + $objectStore = $this->getMock( + 'OpenCloud\ObjectStore\Service', + array('Container', 'Create'), + array(), '', false, false, false, false); + + $container = $this->getMock( + 'OpenCloud\ObjectStore\Resource\Container', + array('DataObject'), + array(), '', false, false, false, false); + + $dataObject = $this->getMock( + 'OpenCloud\OpenStore\Resource\DataObject', + array('SetData', 'Create'), + array(), '', false, false, false, false + ); + + $content = "The quick brown fox jumps over a lazy dog"; + $data = array('name'=>'test', 'content_type' => 'text/plain'); + $dataBytes = sizeof($content); + + $dataObject->bytes = $dataBytes; + + $container->expects($this->once()) + ->method('DataObject') + ->will($this->onConsecutiveCalls($dataObject)); + + $objectStore->expects($this->once()) + ->method('Container') + ->will($this->returnValue($container)); + + $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); + + $bytes = $sut->write('test', $content); + + $this->assertEquals($dataBytes, $bytes); + } + + public function testWrite_updateFailed_ReturnsFalse(){ + $objectStore = $this->getMock( + 'OpenCloud\ObjectStore\Service', + array('Container', 'Create'), + array(), '', false, false, false, false); + + $container = $this->getMock( + 'OpenCloud\ObjectStore\Resource\Container', + array('DataObject'), + array(), '', false, false, false, false); + + $dataObject = $this->getMock( + 'OpenCloud\OpenStore\Resource\DataObject', + array('SetData', 'Create'), + array(), '', false, false, false, false + ); + + $content = "The quick brown fox jumps over a lazy dog"; + $data = array('name'=>'test', 'content_type' => 'text/plain'); + $dataBytes = sizeof($content); + + $dataObject->bytes = $dataBytes; + + $dataObject->expects($this->once()) + ->method('SetData') + ->with($this->equalTo($content)); + + $dataObject->expects($this->once()) + ->method('Create') + ->with($this->equalTo($data)) + ->will($this->throwException(new CreateUpdateError())); + + $container->expects($this->exactly(2)) + ->method('DataObject') + ->will($this->onConsecutiveCalls(false, $dataObject)); + + $objectStore->expects($this->once()) + ->method('Container') + ->will($this->returnValue($container)); + + $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); + + $bytes = $sut->write('test', $content); + + $this->assertFalse($bytes); + + } + + public function testExists_keyExists_ReturnTrue(){ + $objectStore = $this->getMock( + 'OpenCloud\ObjectStore\Service', + array('Container', 'Create'), + array(), '', false, false, false, false); + + $container = $this->getMock( + 'OpenCloud\ObjectStore\Resource\Container', + array('DataObject'), + array(), '', false, false, false, false); + + $dataObject = $this->getMock( + 'OpenCloud\OpenStore\Resource\DataObject', + array('SaveToString'), + array(), '', false, false, false, false + ); + + $container->expects($this->once()) + ->method('DataObject') + ->with($this->equalTo('test')) + ->will($this->returnValue($dataObject)); + + $objectStore->expects($this->once()) + ->method('Container') + ->will($this->returnValue($container)); + + $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); + + $this->assertTrue($sut->exists('test')); + + } + + public function testExists_keyDoesNotExist_ReturnFalse(){ + $objectStore = $this->getMock( + 'OpenCloud\ObjectStore\Service', + array('Container', 'Create'), + array(), '', false, false, false, false); + + $container = $this->getMock( + 'OpenCloud\ObjectStore\Resource\Container', + array('DataObject'), + array(), '', false, false, false, false); + + $container->expects($this->once()) + ->method('DataObject') + ->with($this->equalTo('test')) + ->will($this->throwException(new ObjFetchError())); + + $objectStore->expects($this->once()) + ->method('Container') + ->will($this->returnValue($container)); + + $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); + + $this->assertFalse($sut->exists('test')); + } + + public function testKeys_hasKeys_ReturnsSortedArray(){ + + $objectStore = $this->getMock( + 'OpenCloud\ObjectStore\Service', + array('Container', 'Create'), + array(), '', false, false, false, false); + + $container = $this->getMock( + 'OpenCloud\ObjectStore\Resource\Container', + array('ObjectList'), + array(), '', false, false, false, false); + + $objectList = $this->getMock( + 'OpenCloud\Common\Collection', + array('next'), + array(), '', false, false, false, false + ); + + $expectedValues = array('test1', 'test2', 'test3'); + $index = 0; + $objectList->expects($this->any()) + ->method('next') + ->will($this->returnCallback(function() use($expectedValues, &$index) { + if(isset($expectedValues[$index])){ + $class = new \stdClass(); + $class->name = $expectedValues[$index]; + $index++; + return $class; + } + return false; + })); + + $container->expects($this->once()) + ->method('ObjectList') + ->will($this->returnValue($objectList)) + ; + + $objectStore->expects($this->once()) + ->method('Container') + ->will($this->returnValue($container)); + + $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); + + $this->assertEquals($expectedValues, $sut->keys()); + } + + public function testKeys_noKeys_returnsEmptyArray(){ + + $objectStore = $this->getMock( + 'OpenCloud\ObjectStore\Service', + array('Container', 'Create'), + array(), '', false, false, false, false); + + $container = $this->getMock( + 'OpenCloud\ObjectStore\Resource\Container', + array('ObjectList'), + array(), '', false, false, false, false); + + $objectList = $this->getMock( + 'OpenCloud\Common\Collection', + array('next'), + array(), '', false, false, false, false + ); + + $expectedValues = array(); + $objectList->expects($this->Once()) + ->method('next') + ->will($this->returnValue(false)); + + $container->expects($this->once()) + ->method('ObjectList') + ->will($this->returnValue($objectList)) + ; + + $objectStore->expects($this->once()) + ->method('Container') + ->will($this->returnValue($container)); + + $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); + + $this->assertEquals($expectedValues, $sut->keys()); + } + + public function testMtime_keyExists_ReturnCurrentTime(){ + $objectStore = $this->getMock( + 'OpenCloud\ObjectStore\Service', + array('Container', 'Create'), + array(), '', false, false, false, false); + + $container = $this->getMock( + 'OpenCloud\ObjectStore\Resource\Container', + array('DataObject'), + array(), '', false, false, false, false); + + $dataObject = $this->getMock( + 'OpenCloud\OpenStore\Resource\DataObject', + array(), + array(), '', false, false, false, false + ); + + $modDate = new \DateTime(); + + $dataObject->last_modified = $modDate; + + $container->expects($this->once()) + ->method('DataObject') + ->with($this->equalTo('test')) + ->will($this->returnValue($dataObject)); + + $objectStore->expects($this->once()) + ->method('Container') + ->will($this->returnValue($container)); + + $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); + + $this->assertEquals($modDate, $sut->mtime('test')); + } + + public function testMtime_keyDoesNotExist_ReturnFalse() + { + $objectStore = $this->getMock( + 'OpenCloud\ObjectStore\Service', + array('Container', 'Create'), + array(), '', false, false, false, false); + + $container = $this->getMock( + 'OpenCloud\ObjectStore\Resource\Container', + array('DataObject'), + array(), '', false, false, false, false); + + $container->expects($this->once()) + ->method('DataObject') + ->with($this->equalTo('test')) + ->will($this->throwException(new ObjFetchError())); + + $objectStore->expects($this->once()) + ->method('Container') + ->will($this->returnValue($container)); + + $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); + + $this->assertFalse($sut->mtime('test')); + } + + public function testDelete_keyExistsDeleteSuccess_ReturnTrue(){ + $objectStore = $this->getMock( + 'OpenCloud\ObjectStore\Service', + array('Container', 'Create'), + array(), '', false, false, false, false); + + $container = $this->getMock( + 'OpenCloud\ObjectStore\Resource\Container', + array('DataObject'), + array(), '', false, false, false, false); + + $dataObject = $this->getMock( + 'OpenCloud\OpenStore\Resource\DataObject', + array('Delete'), + array(), '', false, false, false, false + ); + + $dataObject->expects($this->once()) + ->method('Delete'); + + $container->expects($this->once()) + ->method('DataObject') + ->with($this->equalTo('test')) + ->will($this->returnValue($dataObject)); + + $objectStore->expects($this->once()) + ->method('Container') + ->will($this->returnValue($container)); + + + $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); + + $this->assertTrue($sut->delete('test')); + + } + + public function testDelete_keyExistsDeleteFailure_ReturnFalse(){ + $objectStore = $this->getMock( + 'OpenCloud\ObjectStore\Service', + array('Container', 'Create'), + array(), '', false, false, false, false); + + $container = $this->getMock( + 'OpenCloud\ObjectStore\Resource\Container', + array('DataObject'), + array(), '', false, false, false, false); + + $dataObject = $this->getMock( + 'OpenCloud\OpenStore\Resource\DataObject', + array('Delete'), + array(), '', false, false, false, false + ); + + $dataObject->expects($this->once()) + ->method('Delete') + ->will($this->throwException(new DeleteError())); + + $container->expects($this->once()) + ->method('DataObject') + ->with($this->equalTo('test')) + ->will($this->returnValue($dataObject)); + + $objectStore->expects($this->once()) + ->method('Container') + ->will($this->returnValue($container)); + + $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); + + $this->assertFalse($sut->delete('test')); + + } + + public function testDelete_keyDoesNotExist_ReturnFalse(){ + $objectStore = $this->getMock( + 'OpenCloud\ObjectStore\Service', + array('Container', 'Create'), + array(), '', false, false, false, false); + + $container = $this->getMock( + 'OpenCloud\ObjectStore\Resource\Container', + array('DataObject'), + array(), '', false, false, false, false); + + $container->expects($this->once()) + ->method('DataObject') + ->with($this->equalTo('test')) + ->will($this->throwException(new ObjFetchError())); + + $objectStore->expects($this->once()) + ->method('Container') + ->will($this->returnValue($container)); + + $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); + + $this->assertFalse($sut->delete('test')); + } + + public function testCheckSum_keyExists_ReturnsString(){ + $objectStore = $this->getMock( + 'OpenCloud\ObjectStore\Service', + array('Container', 'Create'), + array(), '', false, false, false, false); + + $container = $this->getMock( + 'OpenCloud\ObjectStore\Resource\Container', + array('DataObject'), + array(), '', false, false, false, false); + + $dataObject = $this->getMock( + 'OpenCloud\OpenStore\Resource\DataObject', + array('getETag'), + array(), '', false, false, false, false + ); + + $testString = 'testString'; + + $dataObject->expects($this->once()) + ->method('getETag') + ->will($this->returnValue($testString)); + + $container->expects($this->once()) + ->method('DataObject') + ->with($this->equalTo('test')) + ->will($this->returnValue($dataObject)); + + $objectStore->expects($this->once()) + ->method('Container') + ->will($this->returnValue($container)); + + + $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); + + $this->assertSame($testString, $sut->checksum('test')); + + } + + public function testCheckSum_keyDoesNotExist_ReturnFalse(){ + $objectStore = $this->getMock( + 'OpenCloud\ObjectStore\Service', + array('Container', 'Create'), + array(), '', false, false, false, false); + + $container = $this->getMock( + 'OpenCloud\ObjectStore\Resource\Container', + array('DataObject'), + array(), '', false, false, false, false); + + $container->expects($this->once()) + ->method('DataObject') + ->with($this->equalTo('test')) + ->will($this->throwException(new ObjFetchError())); + + $objectStore->expects($this->once()) + ->method('Container') + ->will($this->returnValue($container)); + + $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); + + $this->assertFalse($sut->checksum('test')); + + } + +} diff --git a/tests/Gaufrette/Unit/LazyOpenStackCloudFilesTest.php b/tests/Gaufrette/Unit/LazyOpenStackCloudFilesTest.php new file mode 100644 index 000000000..0efebb6ec --- /dev/null +++ b/tests/Gaufrette/Unit/LazyOpenStackCloudFilesTest.php @@ -0,0 +1,38 @@ +getMock('Gaufrette\Adapter\OpenStackCloudFiles\OpenStackAuthenticationFactory', + array('create'), array(), '', false, false, false, false ); + + $connection = $this->getMock('OpenCloud\OpenStack', + array('objectStore'),array(), '', false, false, false, false + ); + + $connection->expects($this->once()) + ->method('objectStore') + ->will($this->returnValue($objectStore)); + + $factory->expects($this->once()) + ->method('create') + ->will($this->returnValue($connection)); + + $sut = new LazyOpenStackCloudFiles($factory, $createContainer, $detectContentType); + + return $sut; + } + +} diff --git a/tests/Gaufrette/Unit/OpenCloudTest.php b/tests/Gaufrette/Unit/OpenCloudTest.php new file mode 100644 index 000000000..e296d8462 --- /dev/null +++ b/tests/Gaufrette/Unit/OpenCloudTest.php @@ -0,0 +1,20 @@ + Date: Wed, 2 Oct 2013 14:56:36 -0400 Subject: [PATCH 045/426] Fixed Punctuation in mardown file. --- README.markdown | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.markdown b/README.markdown index 18060f85a..94eaeb778 100644 --- a/README.markdown +++ b/README.markdown @@ -156,7 +156,7 @@ $connection = new OpenCloud\Rackspace( ### LazyOpenCloud -...php +```php $factory = new Gaufrette\Adapter\OpenStackAuthenticationFactory( $url, // connection URL from cloud Vendor $apikey, // password @@ -164,17 +164,17 @@ $factory = new Gaufrette\Adapter\OpenStackAuthenticationFactory( $region, // region $tenant) // tenant $adapter = new Gaufrette\Adapter\LazyOpenStackCloudFiles($factory, 'container-name'); -... +``` ### LazyOpenCloud on Rackspace -...php +```php $factory = new Gaufrette\Adapter\RackspaceAuthenticationFactory( $url, // connection URL from cloud Vendor $apikey, // password $username, //username $region, // region $tenant) // tenant - +``` Using AzureBlobStorage ---------------------- From 6ad062f72094fb9ac7226b738ee36cccc3fa4538 Mon Sep 17 00:00:00 2001 From: Chirs Warner Date: Thu, 3 Oct 2013 09:22:58 -0400 Subject: [PATCH 046/426] Fixed Notes as given by I310 --- composer.lock | 16 ++--- .../Adapter/LazyOpenStackCloudFiles.php | 67 ++++++++++++------- src/Gaufrette/Adapter/OpenCloud.php | 25 +++---- .../BaseOpenStackAuthenciationFactory.php | 51 ++++++++++---- .../ConnectionFactoryInterface.php | 14 ++-- .../OpenStackAuthenticationFactory.php | 19 +++--- ...ckspaceAuthenticationConnectionFactory.php | 12 +++- 7 files changed, 129 insertions(+), 75 deletions(-) diff --git a/composer.lock b/composer.lock index 347978188..277d8dfcd 100644 --- a/composer.lock +++ b/composer.lock @@ -64,12 +64,12 @@ "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "c5da9510480f719361e881a84bfc7ed06716b159" + "reference": "5ac62533b924b92eb7756df8c5f95a434eca7c9b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/c5da9510480f719361e881a84bfc7ed06716b159", - "reference": "c5da9510480f719361e881a84bfc7ed06716b159", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/5ac62533b924b92eb7756df8c5f95a434eca7c9b", + "reference": "5ac62533b924b92eb7756df8c5f95a434eca7c9b", "shasum": "" }, "require": { @@ -122,7 +122,7 @@ "s3", "sdk" ], - "time": "2013-10-01 00:08:29" + "time": "2013-10-02 20:19:48" }, { "name": "doctrine/annotations", @@ -666,12 +666,12 @@ "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "e024996037f14d72ce963074edd21600827a55cc" + "reference": "b170b028c6bb5799640e46c8803015b0f9a45ed9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/e024996037f14d72ce963074edd21600827a55cc", - "reference": "e024996037f14d72ce963074edd21600827a55cc", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b170b028c6bb5799640e46c8803015b0f9a45ed9", + "reference": "b170b028c6bb5799640e46c8803015b0f9a45ed9", "shasum": "" }, "require": { @@ -750,7 +750,7 @@ "rest", "web service" ], - "time": "2013-10-01 17:27:14" + "time": "2013-10-02 20:47:00" }, { "name": "herzult/php-ssh", diff --git a/src/Gaufrette/Adapter/LazyOpenStackCloudFiles.php b/src/Gaufrette/Adapter/LazyOpenStackCloudFiles.php index 02f3c7a1e..9a8667fa3 100644 --- a/src/Gaufrette/Adapter/LazyOpenStackCloudFiles.php +++ b/src/Gaufrette/Adapter/LazyOpenStackCloudFiles.php @@ -1,19 +1,15 @@ + */ class LazyOpenStackCloudFiles extends OpenCloud { /** @@ -28,7 +24,7 @@ class LazyOpenStackCloudFiles extends OpenCloud { /** * @param ConnectionFactoryInterface $connectionFactory - * @param $containerName + * @param string $containerName * @param bool $createContainer * @param bool $detectContentType */ @@ -38,73 +34,96 @@ public function __construct($connectionFactory, $containerName, $createContainer $this->containerName = $containerName; $this->createContainer = $createContainer; $this->detectContentType = $detectContentType; - } + /** + * @inheritdoc + */ public function read($key) { $this->connect(); - return parent::read($key); // TODO: Change the autogenerated stub + return parent::read($key); } + /** + * @@inheritdoc + */ public function write($key, $content) { $this->connect(); - return parent::write($key, $content); // TODO: Change the autogenerated stub + return parent::write($key, $content); } + /** + * @inheritdoc + */ public function exists($key) { $this->connect(); - return parent::exists($key); // TODO: Change the autogenerated stub + return parent::exists($key); } + /** + * @inheritdoc + */ public function keys() { $this->connect(); - return parent::keys(); // TODO: Change the autogenerated stub + return parent::keys(); } + /** + * @inheritdoc + */ public function delete($key) { $this->connect(); - return parent::delete($key); // TODO: Change the autogenerated stub + return parent::delete($key); } + /** + * @inheritdoc + */ public function rename($sourceKey, $targetKey) { $this->connect(); - parent::rename($sourceKey, $targetKey); // TODO: Change the autogenerated stub + parent::rename($sourceKey, $targetKey); } + /** + * @inheritdoc + */ public function isDirectory($key) { $this->connect(); - return parent::isDirectory($key); // TODO: Change the autogenerated stub + return parent::isDirectory($key); } + /** + * @inheritdoc + */ public function checksum($key) { $this->connect(); - return parent::checksum($key); // TODO: Change the autogenerated stub + return parent::checksum($key); } + /** + * @inheritdoc + */ public function mtime($key) { $this->connect(); - return parent::mtime($key); // TODO: Change the autogenerated stub + return parent::mtime($key); } - protected function connect() { if(!$this->connected){ /** @var OpenStack $connection */ $connection = $this->connectionFactory->create(); - $objectStore = $connection->objectStore(); - - $this->objectStore = $objectStore; + $this->objectStore = $connection->objectStore(); } } diff --git a/src/Gaufrette/Adapter/OpenCloud.php b/src/Gaufrette/Adapter/OpenCloud.php index ecdc64ddf..747775f06 100644 --- a/src/Gaufrette/Adapter/OpenCloud.php +++ b/src/Gaufrette/Adapter/OpenCloud.php @@ -77,7 +77,7 @@ public function read($key) // if there is a fetch error, tryGetObject returns false // If it returns false, php throws a fatal error because boolean is a non-object. $object = $this->tryGetObject($key); - if($object) + if ($object) { return $object->SaveToString(); } @@ -98,7 +98,7 @@ public function write($key, $content) $object = $this->tryGetObject($key); try{ - if($object === false){ + if ($object === false){ $object = $this->container->DataObject(); $object->SetData($content); @@ -113,7 +113,7 @@ public function write($key, $content) $object->Create($data); } return $object->bytes; - }catch(CreateUpdateError $updateError){ + } catch(CreateUpdateError $updateError) { return false; } } @@ -141,7 +141,7 @@ public function keys() $this->initialize(); $objectList = $this->container->ObjectList(); $keys = array(); - while($object = $objectList->Next()) { + while ( $object = $objectList->Next() ) { $keys[] = $object->name; } sort($keys); @@ -161,9 +161,11 @@ public function mtime($key) $object = $this->tryGetObject($key); - $lastModified = $object?$object->last_modified:$object; + if ($object) { + return $object->last_modified; + } - return $lastModified; + return false; } /** @@ -178,12 +180,11 @@ public function delete($key) $this->initialize(); try{ $object = $this->tryGetObject($key); - if($object){ - $object->Delete(); - } else { + if (!$object) { return false; } - }catch (DeleteError $deleteError){ + $object->Delete(); + } catch (DeleteError $deleteError) { return false; } @@ -228,7 +229,7 @@ public function checksum($key) { $this->initialize(); $object = $this->tryGetObject($key); - if($object) + if ($object) return $object->getETag(); return false; @@ -242,7 +243,7 @@ protected function tryGetObject($key) { try{ return $this->container->DataObject($key); - }catch (ObjFetchError $objFetchError){ + }catch (ObjFetchError $objFetchError) { return false; } } diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/BaseOpenStackAuthenciationFactory.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/BaseOpenStackAuthenciationFactory.php index cddc43df5..abaff6556 100644 --- a/src/Gaufrette/Adapter/OpenStackCloudFiles/BaseOpenStackAuthenciationFactory.php +++ b/src/Gaufrette/Adapter/OpenStackCloudFiles/BaseOpenStackAuthenciationFactory.php @@ -1,24 +1,49 @@ + */ abstract class BaseOpenStackAuthenticationFactory implements ConnectionFactoryInterface { - protected $authenciationService = null; - protected $url, $username, $tenant, $apikey, $region; /** - * @param $url - * @param $apikey - * @param $username - * @param $region - * @param null $tenant + * @var null|OpenStack|Rackspace + */ + protected $authenciationService = null; + /** + * @var string + */ + protected $url; + /** + * @var string + */ + protected $username; + /** + * @var null|string + */ + protected $tenant; + /** + * @var string + */ + protected $apikey; + /** + * @var string + */ + protected $region; + + /** + * @param string $url + * @param string $apikey + * @param string $username + * @param string $region + * @param null|string $tenant */ function __construct($url, $apikey, $username, $region, $tenant = null) { diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/ConnectionFactoryInterface.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/ConnectionFactoryInterface.php index 90bd807bc..013cb2497 100644 --- a/src/Gaufrette/Adapter/OpenStackCloudFiles/ConnectionFactoryInterface.php +++ b/src/Gaufrette/Adapter/OpenStackCloudFiles/ConnectionFactoryInterface.php @@ -1,15 +1,15 @@ + */ +interface ConnectionFactoryInterface +{ public function create(); diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/OpenStackAuthenticationFactory.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/OpenStackAuthenticationFactory.php index 7fd213910..54a6c9c75 100644 --- a/src/Gaufrette/Adapter/OpenStackCloudFiles/OpenStackAuthenticationFactory.php +++ b/src/Gaufrette/Adapter/OpenStackCloudFiles/OpenStackAuthenticationFactory.php @@ -1,20 +1,23 @@ + */ +class OpenStackAuthenticationFactory extends BaseOpenStackAuthenticationFactory implements ConnectionFactoryInterface +{ + /** + * @return OpenStack + */ public function create() { - if(null === $this->authenciationService) + if (null === $this->authenciationService) { $this->authenciationService = new OpenStack($this->url, array($this->username, $this->apikey)); $this->authenciationService->authenticate(); diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationConnectionFactory.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationConnectionFactory.php index 609f661d7..c245271e9 100644 --- a/src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationConnectionFactory.php +++ b/src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationConnectionFactory.php @@ -14,20 +14,26 @@ use OpenCloud\Common\Base; use OpenCloud\Rackspace; -class RackspaceAuthenticationConnectionFactory extends BaseOpenStackAuthenticationFactory implements ConnectionFactoryInterface { +/** + * Class RackspaceAuthenticationConnectionFactory + * @package Gaufrette\Adapter\OpenStackCloudFiles + * @author Chris Warner + */ +class RackspaceAuthenticationConnectionFactory extends BaseOpenStackAuthenticationFactory implements ConnectionFactoryInterface +{ /** * @return Rackspace */ public function create() { - if(null === $this->authenciationService) + if (null === $this->authenciationService) { $this->authenciationService = new Rackspace($this->url, array($this->username, $this->apikey)); $this->authenciationService->authenticate(); - } elseif($this->authenciationService->expired()) { + } elseif ($this->authenciationService->expired()) { $this->authenciationService->authenticate(); } From a6c775aabf9fe4248314cd3dad72f72e25f106c8 Mon Sep 17 00:00:00 2001 From: Chirs Warner Date: Thu, 3 Oct 2013 14:45:55 -0400 Subject: [PATCH 047/426] Removed the Unit tests for the OpenCloud and LazyOpenStack adapters, and created specs to cover those classes. --- .../Adapter/LazyOpenStackCloudFilesSpec.php | 286 +++++++++ spec/Gaufrette/Adapter/OpenCloudSpec.php | 231 +++++++ ...=> BaseOpenStackAuthenticationFactory.php} | 0 ...php => RackspaceAuthenticationFactory.php} | 2 +- .../Gaufrette/Unit/BaseOpenCloudTestCase.php | 582 ------------------ .../Unit/LazyOpenStackCloudFilesTest.php | 38 -- tests/Gaufrette/Unit/OpenCloudTest.php | 20 - 7 files changed, 518 insertions(+), 641 deletions(-) create mode 100644 spec/Gaufrette/Adapter/LazyOpenStackCloudFilesSpec.php create mode 100644 spec/Gaufrette/Adapter/OpenCloudSpec.php rename src/Gaufrette/Adapter/OpenStackCloudFiles/{BaseOpenStackAuthenciationFactory.php => BaseOpenStackAuthenticationFactory.php} (100%) rename src/Gaufrette/Adapter/OpenStackCloudFiles/{RackspaceAuthenticationConnectionFactory.php => RackspaceAuthenticationFactory.php} (87%) delete mode 100644 tests/Gaufrette/Unit/BaseOpenCloudTestCase.php delete mode 100644 tests/Gaufrette/Unit/LazyOpenStackCloudFilesTest.php delete mode 100644 tests/Gaufrette/Unit/OpenCloudTest.php diff --git a/spec/Gaufrette/Adapter/LazyOpenStackCloudFilesSpec.php b/spec/Gaufrette/Adapter/LazyOpenStackCloudFilesSpec.php new file mode 100644 index 000000000..dd0d0d6bc --- /dev/null +++ b/spec/Gaufrette/Adapter/LazyOpenStackCloudFilesSpec.php @@ -0,0 +1,286 @@ + + */ +class LazyOpenStackCloudFilesSpec extends ObjectBehavior { + + /** + * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory + */ + public function let($connectionFactory) + { + $containerName = 'test'; + $createContainer = false; + $detectContentType = false; + + $this->beConstructedWith($connectionFactory, $containerName, $createContainer, $detectContentType); + } + + public function it_is_adapter() + { + $this->shouldHaveType('Gaufrette\Adapter'); + } + + /** + * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory + * @param OpenCloud\OpenStack $connection + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\DataObject $object + */ + public function it_reads_file($connectionFactory, $connection, $objectStore, $container, $object) + { + $object->saveToString()->willReturn("Hello World"); + $container->dataObject("test")->willReturn($object); + $objectStore->container("test")->willReturn($container); + $connection->objectStore()->willReturn($objectStore)->shouldBeCalledTimes(1); + $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); + $this->read('test')->shouldReturn('Hello World'); + } + + /** + * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory + * @param OpenCloud\OpenStack $connection + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + */ + public function it_reads_file_on_error_returns_false($connectionFactory, $connection, $objectStore, $container) + { + $connection->objectStore()->willReturn($objectStore)->shouldBeCalledTimes(1); + $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); + $container->dataObject("test")->willThrow(new ObjFetchError()); + $objectStore->container("test")->willReturn($container); + + $this->read('test')->shouldReturn(false); + } + + /** + * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory + * @param OpenCloud\OpenStack $connection + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\DataObject $object + */ + public function it_writes_file_key_doesnot_exist($connectionFactory, $connection, $objectStore, $container, $object) + { + $testData = "Hello World!"; + $testDataSize = sizeof($testData); + $object->create(array('name'=>'test'))->willReturn(null); + $object->setData($testData)->willReturn(null); + $object->bytes = $testDataSize; + $container->dataObject('test')->willReturn(false); + $container->dataObject()->willReturn($object); + $objectStore->container("test")->willReturn($container); + $connection->objectStore()->willReturn($objectStore)->shouldBeCalledTimes(1); + $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); + + $this->write('test', $testData)->shouldReturn($testDataSize); + + } + + /** + * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory + * @param OpenCloud\OpenStack $connection + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\DataObject $object + */ + public function it_writes_file_key_exists($connectionFactory, $connection, $objectStore, $container, $object) + { + $testData = "Hello World!"; + $testDataSize = sizeof($testData); + $object->bytes = $testDataSize; + $container->dataObject('test')->willReturn($object); + $objectStore->container("test")->willReturn($container); + $connection->objectStore()->willReturn($objectStore)->shouldBeCalledTimes(1); + $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); + + $this->write('test', $testData)->shouldReturn($testDataSize); + + } + + /** + * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory + * @param OpenCloud\OpenStack $connection + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\DataObject $object + */ + public function it_writes_file_write_fails($connectionFactory, $connection, $objectStore, $container, $object) + { + $testData = "Hello World!"; + $object->create(array('name'=>'test'))->willThrow(new CreateUpdateError()); + $object->setData($testData)->willReturn(null); + $container->dataObject('test')->willReturn(false); + $container->dataObject()->willReturn($object); + $objectStore->container("test")->willReturn($container); + $connection->objectStore()->willReturn($objectStore)->shouldBeCalledTimes(1); + $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); + + $this->write('test', $testData)->shouldReturn(false); + + } + + /** + * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory + * @param OpenCloud\OpenStack $connection + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\DataObject $object + */ + public function it_key_exists_true($connectionFactory, $connection, $objectStore, $container, $object) + { + $container->dataObject('test')->willReturn($object); + $objectStore->container("test")->willReturn($container); + $connection->objectStore()->willReturn($objectStore)->shouldBeCalledTimes(1); + $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); + + $this->exists('test')->shouldReturn(true); + + } + + /** + * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory + * @param OpenCloud\OpenStack $connection + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\DataObject $object + */ + public function it_key_does_not_exist_false($connectionFactory, $connection, $objectStore, $container, $object) + { + $container->dataObject('test')->willReturn($object)->willThrow(new ObjFetchError()); + $objectStore->container("test")->willReturn($container); + $connection->objectStore()->willReturn($objectStore)->shouldBeCalledTimes(1); + $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); + + $this->exists('test')->shouldReturn(false); + + } + + /** + * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory + * @param OpenCloud\OpenStack $connection + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\DataObject $object + */ + public function it_deletes_file($connectionFactory, $connection, $objectStore, $container, $object) + { + $object->delete()->willReturn(null); + $container->dataObject("test")->willReturn($object); + $objectStore->container("test")->willReturn($container); + $connection->objectStore()->willReturn($objectStore)->shouldBeCalledTimes(1); + $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); + + $this->delete('test')->shouldReturn(true); + } + + /** + * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory + * @param OpenCloud\OpenStack $connection + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\DataObject $object + */ + public function it_deletes_file_fails($connectionFactory, $connection, $objectStore, $container, $object) + { + $object->delete()->willThrow(new DeleteError()); + $container->dataObject("test")->willReturn($object); + $objectStore->container("test")->willReturn($container); + $connection->objectStore()->willReturn($objectStore)->shouldBeCalledTimes(1); + $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); + + $this->delete('test')->shouldReturn(false); + } + /** + * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory + * @param OpenCloud\OpenStack $connection + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + */ + public function it_deletes_file_does_not_exist_returns_false($connectionFactory, $connection, $objectStore, $container) + { + $container->dataObject("test")->willThrow(new ObjFetchError()); + $objectStore->container("test")->willReturn($container); + $connection->objectStore()->willReturn($objectStore)->shouldBeCalledTimes(1); + $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); + + $this->delete('test')->shouldReturn(false); + } + + /** + * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory + * @param OpenCloud\OpenStack $connection + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\DataObject $object + */ + public function it_checksum_returns_string($connectionFactory, $connection, $objectStore, $container, $object) + { + $object->getETag()->willReturn("test String"); + $container->dataObject("test")->willReturn($object); + $objectStore->container("test")->willReturn($container); + $connection->objectStore()->willReturn($objectStore)->shouldBeCalledTimes(1); + $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); + + $this->checksum('test')->shouldReturn("test String"); + + } + /** + * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory + * @param OpenCloud\OpenStack $connection + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + */ + public function it_checksum_file_does_not_exist_returns_false($connectionFactory, $connection, $objectStore, $container) + { + $container->dataObject("test")->willThrow(new ObjFetchError()); + $objectStore->container("test")->willReturn($container); + $connection->objectStore()->willReturn($objectStore)->shouldBeCalledTimes(1); + $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); + + $this->checksum('test')->shouldReturn(false); + } + /** + * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory + * @param OpenCloud\OpenStack $connection + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\Common\Collection $objectList + */ + public function it_keys_returns_sorted_array($connectionFactory, $connection, $objectStore, $container, $objectList) + { + $inputArray = array('key5', 'key2', 'key1'); + $outputArray = $inputArray; + sort($outputArray); + $index = 0; + + $objectList->next()->will(function() use($inputArray, &$index){ + if ($index < count($inputArray)) { + $objectItem = new \stdClass(); + $objectItem->name = $inputArray[$index]; + $index++; + return $objectItem; + } + })->shouldBeCalledTimes(count($inputArray)+1); + + $container->objectList()->willReturn($objectList); + $objectStore->container("test")->willReturn($container); + $connection->objectStore()->willReturn($objectStore)->shouldBeCalledTimes(1); + $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); + + $this->keys()->shouldReturn($outputArray); + } + +} diff --git a/spec/Gaufrette/Adapter/OpenCloudSpec.php b/spec/Gaufrette/Adapter/OpenCloudSpec.php new file mode 100644 index 000000000..dc5738e5f --- /dev/null +++ b/spec/Gaufrette/Adapter/OpenCloudSpec.php @@ -0,0 +1,231 @@ + + */ +class OpenCloudSpec extends ObjectBehavior { + /** + * @param OpenCloud\ObjectStore\Service $objectStore + */ + public function let($objectStore) + { + $this->beConstructedWith($objectStore, 'test', false, false); + } + + public function it_is_adapter() + { + $this->shouldHaveType('Gaufrette\Adapter'); + } + + /** + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\DataObject $object + */ + public function it_reads_file($objectStore, $container, $object) + { + $object->saveToString()->willReturn("Hello World"); + $container->dataObject("test")->willReturn($object); + $objectStore->container("test")->willReturn($container); + + $this->read('test')->shouldReturn('Hello World'); + } + + /** + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + */ + public function it_reads_file_on_error_returns_false($objectStore, $container) + { + $container->dataObject("test")->willThrow(new ObjFetchError()); + $objectStore->container("test")->willReturn($container); + + $this->read('test')->shouldReturn(false); + } + + /** + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\DataObject $object + */ + public function it_writes_file_key_doesnot_exist($objectStore, $container, $object) + { + $testData = "Hello World!"; + $testDataSize = sizeof($testData); + $object->create(array('name'=>'test'))->willReturn(null); + $object->setData($testData)->willReturn(null); + $object->bytes = $testDataSize; + $container->dataObject('test')->willReturn(false); + $container->dataObject()->willReturn($object); + $objectStore->container("test")->willReturn($container); + + $this->write('test', $testData)->shouldReturn($testDataSize); + + } + + /** + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\DataObject $object + */ + public function it_writes_file_key_exists($objectStore, $container, $object) + { + $testData = "Hello World!"; + $testDataSize = sizeof($testData); + $object->bytes = $testDataSize; + $container->dataObject('test')->willReturn($object); + $objectStore->container("test")->willReturn($container); + + $this->write('test', $testData)->shouldReturn($testDataSize); + + } + + /** + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\DataObject $object + */ + public function it_writes_file_write_fails($objectStore, $container, $object) + { + $testData = "Hello World!"; +// $testDataSize = sizeof($testData); + $object->create(array('name'=>'test'))->willThrow(new CreateUpdateError()); + $object->setData($testData)->willReturn(null); + $container->dataObject('test')->willReturn(false); + $container->dataObject()->willReturn($object); + $objectStore->container("test")->willReturn($container); + + $this->write('test', $testData)->shouldReturn(false); + + } + + /** + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\DataObject $object + */ + public function it_key_exists_true($objectStore, $container, $object) + { + $container->dataObject('test')->willReturn($object); + $objectStore->container("test")->willReturn($container); + + $this->exists('test')->shouldReturn(true); + + } + + /** + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\DataObject $object + */ + public function it_key_does_not_exist_false($objectStore, $container, $object) + { + $container->dataObject('test')->willReturn($object)->willThrow(new ObjFetchError()); + $objectStore->container("test")->willReturn($container); + + $this->exists('test')->shouldReturn(false); + + } + + /** + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\DataObject $object + */ + public function it_deletes_file($objectStore, $container, $object) + { + $object->delete()->willReturn(null); + $container->dataObject("test")->willReturn($object); + $objectStore->container("test")->willReturn($container); + + $this->delete('test')->shouldReturn(true); + } + + /** + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\DataObject $object + */ + public function it_deletes_file_fails($objectStore, $container, $object) + { + $object->delete()->willThrow(new DeleteError()); + $container->dataObject("test")->willReturn($object); + $objectStore->container("test")->willReturn($container); + + $this->delete('test')->shouldReturn(false); + } + /** + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + */ + public function it_deletes_file_does_not_exist_returns_false($objectStore, $container) + { + $container->dataObject("test")->willThrow(new ObjFetchError()); + $objectStore->container("test")->willReturn($container); + + $this->delete('test')->shouldReturn(false); + } + + /** + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\DataObject $object + */ + public function it_checksum_returns_string($objectStore, $container, $object) + { + $object->getETag()->willReturn("test String"); + $container->dataObject("test")->willReturn($object); + $objectStore->container("test")->willReturn($container); + + $this->checksum('test')->shouldReturn("test String"); + + } + /** + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + */ + public function it_checksum_file_does_not_exist_returns_false($objectStore, $container) + { + $container->dataObject("test")->willThrow(new ObjFetchError()); + $objectStore->container("test")->willReturn($container); + + $this->checksum('test')->shouldReturn(false); + } + + /** + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\Common\Collection $objectList + */ + public function it_keys_returns_sorted_array($objectStore, $container, $objectList) + { + $inputArray = array('key5', 'key2', 'key1'); + $outputArray = $inputArray; + sort($outputArray); + $index = 0; + + $objectList->next()->will(function() use($inputArray, &$index){ + if ($index < count($inputArray)) { + $objectItem = new \stdClass(); + $objectItem->name = $inputArray[$index]; + $index++; + return $objectItem; + } + })->shouldBeCalledTimes(count($inputArray)+1); + + $container->objectList()->willReturn($objectList); + $objectStore->container("test")->willReturn($container); + + $this->keys()->shouldReturn($outputArray); + } + +} diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/BaseOpenStackAuthenciationFactory.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/BaseOpenStackAuthenticationFactory.php similarity index 100% rename from src/Gaufrette/Adapter/OpenStackCloudFiles/BaseOpenStackAuthenciationFactory.php rename to src/Gaufrette/Adapter/OpenStackCloudFiles/BaseOpenStackAuthenticationFactory.php diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationConnectionFactory.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationFactory.php similarity index 87% rename from src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationConnectionFactory.php rename to src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationFactory.php index c245271e9..7b52971fe 100644 --- a/src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationConnectionFactory.php +++ b/src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationFactory.php @@ -19,7 +19,7 @@ * @package Gaufrette\Adapter\OpenStackCloudFiles * @author Chris Warner */ -class RackspaceAuthenticationConnectionFactory extends BaseOpenStackAuthenticationFactory implements ConnectionFactoryInterface +class RackspaceAuthenticationFactory extends BaseOpenStackAuthenticationFactory implements ConnectionFactoryInterface { /** diff --git a/tests/Gaufrette/Unit/BaseOpenCloudTestCase.php b/tests/Gaufrette/Unit/BaseOpenCloudTestCase.php deleted file mode 100644 index 50374651d..000000000 --- a/tests/Gaufrette/Unit/BaseOpenCloudTestCase.php +++ /dev/null @@ -1,582 +0,0 @@ -getMock( - 'OpenCloud\ObjectStore\Service', - array('Container', 'Create'), - array(), '', false, false, false, false); - - $container = $this->getMock( - 'OpenCloud\ObjectStore\Resource\Container', - array('DataObject'), - array(), '', false, false, false, false); - - $dataObject = $this->getMock( - 'OpenCloud\OpenStore\Resource\DataObject', - array('SaveToString'), - array(), '', false, false, false, false - ); - - $dataObject->expects($this->once()) - ->method('SaveToString') - ->will($this->returnValue('test')); - - $container->expects($this->once()) - ->method('DataObject') - ->with($this->equalTo('test')) - ->will($this->returnValue($dataObject)); - - $objectStore->expects($this->once()) - ->method('Container') - ->will($this->returnValue($container)); - - $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); - $result = $sut->read('test'); - - $this->assertSame('test', $result); - } - - public function testRead_KeyDoesNotExists_ReturnsFalse() - { - $objectStore = $this->getMock( - 'OpenCloud\ObjectStore\Service', - array('Container', 'Create'), - array(), '', false, false, false, false); - - $container = $this->getMock( - 'OpenCloud\ObjectStore\Resource\Container', - array('DataObject'), - array(), '', false, false, false, false); - - $container->expects($this->once()) - ->method('DataObject') - ->with($this->equalTo('test')) - ->will($this->throwException(new ObjFetchError())); - - $objectStore->expects($this->once()) - ->method('Container') - ->will($this->returnValue($container)); - - $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); - $result = $sut->read('test'); - - $this->assertFalse($result); - - } - - public function testWrite_keyDoesNotExist_ReturnsXbytes(){ - - $objectStore = $this->getMock( - 'OpenCloud\ObjectStore\Service', - array('Container', 'Create'), - array(), '', false, false, false, false); - - $container = $this->getMock( - 'OpenCloud\ObjectStore\Resource\Container', - array('DataObject'), - array(), '', false, false, false, false); - - $dataObject = $this->getMock( - 'OpenCloud\OpenStore\Resource\DataObject', - array('SetData', 'Create'), - array(), '', false, false, false, false - ); - - $content = "The quick brown fox jumps over a lazy dog"; - $data = array('name'=>'test', 'content_type' => 'text/plain'); - $dataBytes = sizeof($content); - - $dataObject->bytes = $dataBytes; - - $dataObject->expects($this->once()) - ->method('SetData') - ->with($this->equalTo($content)); - - $dataObject->expects($this->once()) - ->method('Create') - ->with($this->equalTo($data)); - - $container->expects($this->exactly(2)) - ->method('DataObject') - ->will($this->onConsecutiveCalls(false, $dataObject)); - - $objectStore->expects($this->once()) - ->method('Container') - ->will($this->returnValue($container)); - - $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); - - $bytes = $sut->write('test', $content); - - $this->assertEquals($dataBytes, $bytes); - - - } - - public function testWrite_keyExists_ReturnsXbytes(){ - $objectStore = $this->getMock( - 'OpenCloud\ObjectStore\Service', - array('Container', 'Create'), - array(), '', false, false, false, false); - - $container = $this->getMock( - 'OpenCloud\ObjectStore\Resource\Container', - array('DataObject'), - array(), '', false, false, false, false); - - $dataObject = $this->getMock( - 'OpenCloud\OpenStore\Resource\DataObject', - array('SetData', 'Create'), - array(), '', false, false, false, false - ); - - $content = "The quick brown fox jumps over a lazy dog"; - $data = array('name'=>'test', 'content_type' => 'text/plain'); - $dataBytes = sizeof($content); - - $dataObject->bytes = $dataBytes; - - $container->expects($this->once()) - ->method('DataObject') - ->will($this->onConsecutiveCalls($dataObject)); - - $objectStore->expects($this->once()) - ->method('Container') - ->will($this->returnValue($container)); - - $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); - - $bytes = $sut->write('test', $content); - - $this->assertEquals($dataBytes, $bytes); - } - - public function testWrite_updateFailed_ReturnsFalse(){ - $objectStore = $this->getMock( - 'OpenCloud\ObjectStore\Service', - array('Container', 'Create'), - array(), '', false, false, false, false); - - $container = $this->getMock( - 'OpenCloud\ObjectStore\Resource\Container', - array('DataObject'), - array(), '', false, false, false, false); - - $dataObject = $this->getMock( - 'OpenCloud\OpenStore\Resource\DataObject', - array('SetData', 'Create'), - array(), '', false, false, false, false - ); - - $content = "The quick brown fox jumps over a lazy dog"; - $data = array('name'=>'test', 'content_type' => 'text/plain'); - $dataBytes = sizeof($content); - - $dataObject->bytes = $dataBytes; - - $dataObject->expects($this->once()) - ->method('SetData') - ->with($this->equalTo($content)); - - $dataObject->expects($this->once()) - ->method('Create') - ->with($this->equalTo($data)) - ->will($this->throwException(new CreateUpdateError())); - - $container->expects($this->exactly(2)) - ->method('DataObject') - ->will($this->onConsecutiveCalls(false, $dataObject)); - - $objectStore->expects($this->once()) - ->method('Container') - ->will($this->returnValue($container)); - - $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); - - $bytes = $sut->write('test', $content); - - $this->assertFalse($bytes); - - } - - public function testExists_keyExists_ReturnTrue(){ - $objectStore = $this->getMock( - 'OpenCloud\ObjectStore\Service', - array('Container', 'Create'), - array(), '', false, false, false, false); - - $container = $this->getMock( - 'OpenCloud\ObjectStore\Resource\Container', - array('DataObject'), - array(), '', false, false, false, false); - - $dataObject = $this->getMock( - 'OpenCloud\OpenStore\Resource\DataObject', - array('SaveToString'), - array(), '', false, false, false, false - ); - - $container->expects($this->once()) - ->method('DataObject') - ->with($this->equalTo('test')) - ->will($this->returnValue($dataObject)); - - $objectStore->expects($this->once()) - ->method('Container') - ->will($this->returnValue($container)); - - $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); - - $this->assertTrue($sut->exists('test')); - - } - - public function testExists_keyDoesNotExist_ReturnFalse(){ - $objectStore = $this->getMock( - 'OpenCloud\ObjectStore\Service', - array('Container', 'Create'), - array(), '', false, false, false, false); - - $container = $this->getMock( - 'OpenCloud\ObjectStore\Resource\Container', - array('DataObject'), - array(), '', false, false, false, false); - - $container->expects($this->once()) - ->method('DataObject') - ->with($this->equalTo('test')) - ->will($this->throwException(new ObjFetchError())); - - $objectStore->expects($this->once()) - ->method('Container') - ->will($this->returnValue($container)); - - $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); - - $this->assertFalse($sut->exists('test')); - } - - public function testKeys_hasKeys_ReturnsSortedArray(){ - - $objectStore = $this->getMock( - 'OpenCloud\ObjectStore\Service', - array('Container', 'Create'), - array(), '', false, false, false, false); - - $container = $this->getMock( - 'OpenCloud\ObjectStore\Resource\Container', - array('ObjectList'), - array(), '', false, false, false, false); - - $objectList = $this->getMock( - 'OpenCloud\Common\Collection', - array('next'), - array(), '', false, false, false, false - ); - - $expectedValues = array('test1', 'test2', 'test3'); - $index = 0; - $objectList->expects($this->any()) - ->method('next') - ->will($this->returnCallback(function() use($expectedValues, &$index) { - if(isset($expectedValues[$index])){ - $class = new \stdClass(); - $class->name = $expectedValues[$index]; - $index++; - return $class; - } - return false; - })); - - $container->expects($this->once()) - ->method('ObjectList') - ->will($this->returnValue($objectList)) - ; - - $objectStore->expects($this->once()) - ->method('Container') - ->will($this->returnValue($container)); - - $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); - - $this->assertEquals($expectedValues, $sut->keys()); - } - - public function testKeys_noKeys_returnsEmptyArray(){ - - $objectStore = $this->getMock( - 'OpenCloud\ObjectStore\Service', - array('Container', 'Create'), - array(), '', false, false, false, false); - - $container = $this->getMock( - 'OpenCloud\ObjectStore\Resource\Container', - array('ObjectList'), - array(), '', false, false, false, false); - - $objectList = $this->getMock( - 'OpenCloud\Common\Collection', - array('next'), - array(), '', false, false, false, false - ); - - $expectedValues = array(); - $objectList->expects($this->Once()) - ->method('next') - ->will($this->returnValue(false)); - - $container->expects($this->once()) - ->method('ObjectList') - ->will($this->returnValue($objectList)) - ; - - $objectStore->expects($this->once()) - ->method('Container') - ->will($this->returnValue($container)); - - $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); - - $this->assertEquals($expectedValues, $sut->keys()); - } - - public function testMtime_keyExists_ReturnCurrentTime(){ - $objectStore = $this->getMock( - 'OpenCloud\ObjectStore\Service', - array('Container', 'Create'), - array(), '', false, false, false, false); - - $container = $this->getMock( - 'OpenCloud\ObjectStore\Resource\Container', - array('DataObject'), - array(), '', false, false, false, false); - - $dataObject = $this->getMock( - 'OpenCloud\OpenStore\Resource\DataObject', - array(), - array(), '', false, false, false, false - ); - - $modDate = new \DateTime(); - - $dataObject->last_modified = $modDate; - - $container->expects($this->once()) - ->method('DataObject') - ->with($this->equalTo('test')) - ->will($this->returnValue($dataObject)); - - $objectStore->expects($this->once()) - ->method('Container') - ->will($this->returnValue($container)); - - $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); - - $this->assertEquals($modDate, $sut->mtime('test')); - } - - public function testMtime_keyDoesNotExist_ReturnFalse() - { - $objectStore = $this->getMock( - 'OpenCloud\ObjectStore\Service', - array('Container', 'Create'), - array(), '', false, false, false, false); - - $container = $this->getMock( - 'OpenCloud\ObjectStore\Resource\Container', - array('DataObject'), - array(), '', false, false, false, false); - - $container->expects($this->once()) - ->method('DataObject') - ->with($this->equalTo('test')) - ->will($this->throwException(new ObjFetchError())); - - $objectStore->expects($this->once()) - ->method('Container') - ->will($this->returnValue($container)); - - $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); - - $this->assertFalse($sut->mtime('test')); - } - - public function testDelete_keyExistsDeleteSuccess_ReturnTrue(){ - $objectStore = $this->getMock( - 'OpenCloud\ObjectStore\Service', - array('Container', 'Create'), - array(), '', false, false, false, false); - - $container = $this->getMock( - 'OpenCloud\ObjectStore\Resource\Container', - array('DataObject'), - array(), '', false, false, false, false); - - $dataObject = $this->getMock( - 'OpenCloud\OpenStore\Resource\DataObject', - array('Delete'), - array(), '', false, false, false, false - ); - - $dataObject->expects($this->once()) - ->method('Delete'); - - $container->expects($this->once()) - ->method('DataObject') - ->with($this->equalTo('test')) - ->will($this->returnValue($dataObject)); - - $objectStore->expects($this->once()) - ->method('Container') - ->will($this->returnValue($container)); - - - $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); - - $this->assertTrue($sut->delete('test')); - - } - - public function testDelete_keyExistsDeleteFailure_ReturnFalse(){ - $objectStore = $this->getMock( - 'OpenCloud\ObjectStore\Service', - array('Container', 'Create'), - array(), '', false, false, false, false); - - $container = $this->getMock( - 'OpenCloud\ObjectStore\Resource\Container', - array('DataObject'), - array(), '', false, false, false, false); - - $dataObject = $this->getMock( - 'OpenCloud\OpenStore\Resource\DataObject', - array('Delete'), - array(), '', false, false, false, false - ); - - $dataObject->expects($this->once()) - ->method('Delete') - ->will($this->throwException(new DeleteError())); - - $container->expects($this->once()) - ->method('DataObject') - ->with($this->equalTo('test')) - ->will($this->returnValue($dataObject)); - - $objectStore->expects($this->once()) - ->method('Container') - ->will($this->returnValue($container)); - - $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); - - $this->assertFalse($sut->delete('test')); - - } - - public function testDelete_keyDoesNotExist_ReturnFalse(){ - $objectStore = $this->getMock( - 'OpenCloud\ObjectStore\Service', - array('Container', 'Create'), - array(), '', false, false, false, false); - - $container = $this->getMock( - 'OpenCloud\ObjectStore\Resource\Container', - array('DataObject'), - array(), '', false, false, false, false); - - $container->expects($this->once()) - ->method('DataObject') - ->with($this->equalTo('test')) - ->will($this->throwException(new ObjFetchError())); - - $objectStore->expects($this->once()) - ->method('Container') - ->will($this->returnValue($container)); - - $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); - - $this->assertFalse($sut->delete('test')); - } - - public function testCheckSum_keyExists_ReturnsString(){ - $objectStore = $this->getMock( - 'OpenCloud\ObjectStore\Service', - array('Container', 'Create'), - array(), '', false, false, false, false); - - $container = $this->getMock( - 'OpenCloud\ObjectStore\Resource\Container', - array('DataObject'), - array(), '', false, false, false, false); - - $dataObject = $this->getMock( - 'OpenCloud\OpenStore\Resource\DataObject', - array('getETag'), - array(), '', false, false, false, false - ); - - $testString = 'testString'; - - $dataObject->expects($this->once()) - ->method('getETag') - ->will($this->returnValue($testString)); - - $container->expects($this->once()) - ->method('DataObject') - ->with($this->equalTo('test')) - ->will($this->returnValue($dataObject)); - - $objectStore->expects($this->once()) - ->method('Container') - ->will($this->returnValue($container)); - - - $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); - - $this->assertSame($testString, $sut->checksum('test')); - - } - - public function testCheckSum_keyDoesNotExist_ReturnFalse(){ - $objectStore = $this->getMock( - 'OpenCloud\ObjectStore\Service', - array('Container', 'Create'), - array(), '', false, false, false, false); - - $container = $this->getMock( - 'OpenCloud\ObjectStore\Resource\Container', - array('DataObject'), - array(), '', false, false, false, false); - - $container->expects($this->once()) - ->method('DataObject') - ->with($this->equalTo('test')) - ->will($this->throwException(new ObjFetchError())); - - $objectStore->expects($this->once()) - ->method('Container') - ->will($this->returnValue($container)); - - $sut = $this->buildOpenCloudClass($objectStore, 'testContainer', false, true); - - $this->assertFalse($sut->checksum('test')); - - } - -} diff --git a/tests/Gaufrette/Unit/LazyOpenStackCloudFilesTest.php b/tests/Gaufrette/Unit/LazyOpenStackCloudFilesTest.php deleted file mode 100644 index 0efebb6ec..000000000 --- a/tests/Gaufrette/Unit/LazyOpenStackCloudFilesTest.php +++ /dev/null @@ -1,38 +0,0 @@ -getMock('Gaufrette\Adapter\OpenStackCloudFiles\OpenStackAuthenticationFactory', - array('create'), array(), '', false, false, false, false ); - - $connection = $this->getMock('OpenCloud\OpenStack', - array('objectStore'),array(), '', false, false, false, false - ); - - $connection->expects($this->once()) - ->method('objectStore') - ->will($this->returnValue($objectStore)); - - $factory->expects($this->once()) - ->method('create') - ->will($this->returnValue($connection)); - - $sut = new LazyOpenStackCloudFiles($factory, $createContainer, $detectContentType); - - return $sut; - } - -} diff --git a/tests/Gaufrette/Unit/OpenCloudTest.php b/tests/Gaufrette/Unit/OpenCloudTest.php deleted file mode 100644 index e296d8462..000000000 --- a/tests/Gaufrette/Unit/OpenCloudTest.php +++ /dev/null @@ -1,20 +0,0 @@ - Date: Thu, 3 Oct 2013 21:55:11 -0400 Subject: [PATCH 048/426] Fixed Spec with notes from Stof. Also, removed the comment from RackspaceAuthenticateionFactory. --- spec/Gaufrette/Adapter/OpenCloudSpec.php | 23 +++++++++---------- .../RackspaceAuthenticationFactory.php | 8 ------- 2 files changed, 11 insertions(+), 20 deletions(-) diff --git a/spec/Gaufrette/Adapter/OpenCloudSpec.php b/spec/Gaufrette/Adapter/OpenCloudSpec.php index dc5738e5f..cf438cc38 100644 --- a/spec/Gaufrette/Adapter/OpenCloudSpec.php +++ b/spec/Gaufrette/Adapter/OpenCloudSpec.php @@ -57,7 +57,7 @@ public function it_reads_file_on_error_returns_false($objectStore, $container) * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - public function it_writes_file_key_doesnot_exist($objectStore, $container, $object) + public function it_writes_file_if_key_does_not_exist_returns_size($objectStore, $container, $object) { $testData = "Hello World!"; $testDataSize = sizeof($testData); @@ -77,7 +77,7 @@ public function it_writes_file_key_doesnot_exist($objectStore, $container, $obje * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - public function it_writes_file_key_exists($objectStore, $container, $object) + public function it_writes_file_if_key_exists_returns_size($objectStore, $container, $object) { $testData = "Hello World!"; $testDataSize = sizeof($testData); @@ -94,10 +94,9 @@ public function it_writes_file_key_exists($objectStore, $container, $object) * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - public function it_writes_file_write_fails($objectStore, $container, $object) + public function it_writes_file_and_write_fails_returns_false($objectStore, $container, $object) { $testData = "Hello World!"; -// $testDataSize = sizeof($testData); $object->create(array('name'=>'test'))->willThrow(new CreateUpdateError()); $object->setData($testData)->willReturn(null); $container->dataObject('test')->willReturn(false); @@ -113,7 +112,7 @@ public function it_writes_file_write_fails($objectStore, $container, $object) * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - public function it_key_exists_true($objectStore, $container, $object) + public function if_key_exists_return_true($objectStore, $container, $object) { $container->dataObject('test')->willReturn($object); $objectStore->container("test")->willReturn($container); @@ -127,7 +126,7 @@ public function it_key_exists_true($objectStore, $container, $object) * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - public function it_key_does_not_exist_false($objectStore, $container, $object) + public function if_key_does_not_exist_return_false($objectStore, $container, $object) { $container->dataObject('test')->willReturn($object)->willThrow(new ObjFetchError()); $objectStore->container("test")->willReturn($container); @@ -141,7 +140,7 @@ public function it_key_does_not_exist_false($objectStore, $container, $object) * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - public function it_deletes_file($objectStore, $container, $object) + public function it_deletes_file_on_success_returns_true($objectStore, $container, $object) { $object->delete()->willReturn(null); $container->dataObject("test")->willReturn($object); @@ -155,7 +154,7 @@ public function it_deletes_file($objectStore, $container, $object) * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - public function it_deletes_file_fails($objectStore, $container, $object) + public function it_deletes_file_returns_false_on_failure($objectStore, $container, $object) { $object->delete()->willThrow(new DeleteError()); $container->dataObject("test")->willReturn($object); @@ -167,7 +166,7 @@ public function it_deletes_file_fails($objectStore, $container, $object) * @param OpenCloud\ObjectStore\Service $objectStore * @param OpenCloud\ObjectStore\Resource\Container $container */ - public function it_deletes_file_does_not_exist_returns_false($objectStore, $container) + public function it_deletes_file_if_file_does_not_exist_returns_false($objectStore, $container) { $container->dataObject("test")->willThrow(new ObjFetchError()); $objectStore->container("test")->willReturn($container); @@ -180,7 +179,7 @@ public function it_deletes_file_does_not_exist_returns_false($objectStore, $cont * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - public function it_checksum_returns_string($objectStore, $container, $object) + public function it_returns_checksum_if_file_exists($objectStore, $container, $object) { $object->getETag()->willReturn("test String"); $container->dataObject("test")->willReturn($object); @@ -193,7 +192,7 @@ public function it_checksum_returns_string($objectStore, $container, $object) * @param OpenCloud\ObjectStore\Service $objectStore * @param OpenCloud\ObjectStore\Resource\Container $container */ - public function it_checksum_file_does_not_exist_returns_false($objectStore, $container) + public function it_returns_false_when_file_does_not_exist($objectStore, $container) { $container->dataObject("test")->willThrow(new ObjFetchError()); $objectStore->container("test")->willReturn($container); @@ -206,7 +205,7 @@ public function it_checksum_file_does_not_exist_returns_false($objectStore, $con * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\Common\Collection $objectList */ - public function it_keys_returns_sorted_array($objectStore, $container, $objectList) + public function it_returns_files_as_sorted_array($objectStore, $container, $objectList) { $inputArray = array('key5', 'key2', 'key1'); $outputArray = $inputArray; diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationFactory.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationFactory.php index 7b52971fe..0fb4fd8b1 100644 --- a/src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationFactory.php +++ b/src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationFactory.php @@ -1,14 +1,6 @@ Date: Fri, 4 Oct 2013 11:57:42 -0400 Subject: [PATCH 049/426] Reformatted Code per PSR-1 & 2. Also removed the visibility from the Specs although PSR-2 requires them. --- .../Adapter/LazyOpenStackCloudFilesSpec.php | 181 +++++++++--------- spec/Gaufrette/Adapter/OpenCloudSpec.php | 122 ++++++------ .../Adapter/LazyOpenStackCloudFiles.php | 30 +-- src/Gaufrette/Adapter/OpenCloud.php | 57 +++--- .../BaseOpenStackAuthenticationFactory.php | 24 +-- .../ConnectionFactoryInterface.php | 8 +- .../OpenStackAuthenticationFactory.php | 14 +- .../RackspaceAuthenticationFactory.php | 12 +- 8 files changed, 227 insertions(+), 221 deletions(-) diff --git a/spec/Gaufrette/Adapter/LazyOpenStackCloudFilesSpec.php b/spec/Gaufrette/Adapter/LazyOpenStackCloudFilesSpec.php index dd0d0d6bc..8968a2d76 100644 --- a/spec/Gaufrette/Adapter/LazyOpenStackCloudFilesSpec.php +++ b/spec/Gaufrette/Adapter/LazyOpenStackCloudFilesSpec.php @@ -1,5 +1,4 @@ */ -class LazyOpenStackCloudFilesSpec extends ObjectBehavior { +class LazyOpenStackCloudFilesSpec extends ObjectBehavior +{ /** * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory */ - public function let($connectionFactory) + function let($connectionFactory) { - $containerName = 'test'; - $createContainer = false; + $containerName = 'test'; + $createContainer = false; $detectContentType = false; - $this->beConstructedWith($connectionFactory, $containerName, $createContainer, $detectContentType); + $this->beConstructedWith($connectionFactory, $containerName, $createContainer, $detectContentType); } - public function it_is_adapter() + function it_is_adapter() { $this->shouldHaveType('Gaufrette\Adapter'); } /** * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - * @param OpenCloud\OpenStack $connection - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\ObjectStore\Resource\DataObject $object + * @param OpenCloud\OpenStack $connection + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - public function it_reads_file($connectionFactory, $connection, $objectStore, $container, $object) + function it_reads_file($connectionFactory, $connection, $objectStore, $container, $object) { $object->saveToString()->willReturn("Hello World"); $container->dataObject("test")->willReturn($object); @@ -51,11 +51,11 @@ public function it_reads_file($connectionFactory, $connection, $objectStore, $co /** * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - * @param OpenCloud\OpenStack $connection - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\OpenStack $connection + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container */ - public function it_reads_file_on_error_returns_false($connectionFactory, $connection, $objectStore, $container) + function it_reads_file_on_error_returns_false($connectionFactory, $connection, $objectStore, $container) { $connection->objectStore()->willReturn($objectStore)->shouldBeCalledTimes(1); $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); @@ -67,16 +67,16 @@ public function it_reads_file_on_error_returns_false($connectionFactory, $connec /** * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - * @param OpenCloud\OpenStack $connection - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\ObjectStore\Resource\DataObject $object + * @param OpenCloud\OpenStack $connection + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - public function it_writes_file_key_doesnot_exist($connectionFactory, $connection, $objectStore, $container, $object) + function it_writes_file_key_doesnot_exist($connectionFactory, $connection, $objectStore, $container, $object) { - $testData = "Hello World!"; + $testData = "Hello World!"; $testDataSize = sizeof($testData); - $object->create(array('name'=>'test'))->willReturn(null); + $object->create(array ('name' => 'test'))->willReturn(null); $object->setData($testData)->willReturn(null); $object->bytes = $testDataSize; $container->dataObject('test')->willReturn(false); @@ -86,20 +86,19 @@ public function it_writes_file_key_doesnot_exist($connectionFactory, $connection $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); $this->write('test', $testData)->shouldReturn($testDataSize); - } /** * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - * @param OpenCloud\OpenStack $connection - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\ObjectStore\Resource\DataObject $object + * @param OpenCloud\OpenStack $connection + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - public function it_writes_file_key_exists($connectionFactory, $connection, $objectStore, $container, $object) + function it_writes_file_key_exists($connectionFactory, $connection, $objectStore, $container, $object) { - $testData = "Hello World!"; - $testDataSize = sizeof($testData); + $testData = "Hello World!"; + $testDataSize = sizeof($testData); $object->bytes = $testDataSize; $container->dataObject('test')->willReturn($object); $objectStore->container("test")->willReturn($container); @@ -107,20 +106,19 @@ public function it_writes_file_key_exists($connectionFactory, $connection, $obje $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); $this->write('test', $testData)->shouldReturn($testDataSize); - } /** * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - * @param OpenCloud\OpenStack $connection - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\ObjectStore\Resource\DataObject $object + * @param OpenCloud\OpenStack $connection + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - public function it_writes_file_write_fails($connectionFactory, $connection, $objectStore, $container, $object) + function it_writes_file_write_fails($connectionFactory, $connection, $objectStore, $container, $object) { $testData = "Hello World!"; - $object->create(array('name'=>'test'))->willThrow(new CreateUpdateError()); + $object->create(array ('name' => 'test'))->willThrow(new CreateUpdateError()); $object->setData($testData)->willReturn(null); $container->dataObject('test')->willReturn(false); $container->dataObject()->willReturn($object); @@ -129,17 +127,16 @@ public function it_writes_file_write_fails($connectionFactory, $connection, $obj $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); $this->write('test', $testData)->shouldReturn(false); - } /** * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - * @param OpenCloud\OpenStack $connection - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\ObjectStore\Resource\DataObject $object + * @param OpenCloud\OpenStack $connection + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - public function it_key_exists_true($connectionFactory, $connection, $objectStore, $container, $object) + function it_key_exists_true($connectionFactory, $connection, $objectStore, $container, $object) { $container->dataObject('test')->willReturn($object); $objectStore->container("test")->willReturn($container); @@ -147,17 +144,16 @@ public function it_key_exists_true($connectionFactory, $connection, $objectStore $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); $this->exists('test')->shouldReturn(true); - } /** * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - * @param OpenCloud\OpenStack $connection - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\ObjectStore\Resource\DataObject $object + * @param OpenCloud\OpenStack $connection + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - public function it_key_does_not_exist_false($connectionFactory, $connection, $objectStore, $container, $object) + function it_key_does_not_exist_false($connectionFactory, $connection, $objectStore, $container, $object) { $container->dataObject('test')->willReturn($object)->willThrow(new ObjFetchError()); $objectStore->container("test")->willReturn($container); @@ -165,17 +161,16 @@ public function it_key_does_not_exist_false($connectionFactory, $connection, $ob $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); $this->exists('test')->shouldReturn(false); - } /** * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - * @param OpenCloud\OpenStack $connection - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\ObjectStore\Resource\DataObject $object + * @param OpenCloud\OpenStack $connection + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - public function it_deletes_file($connectionFactory, $connection, $objectStore, $container, $object) + function it_deletes_file($connectionFactory, $connection, $objectStore, $container, $object) { $object->delete()->willReturn(null); $container->dataObject("test")->willReturn($object); @@ -188,12 +183,12 @@ public function it_deletes_file($connectionFactory, $connection, $objectStore, $ /** * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - * @param OpenCloud\OpenStack $connection - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\ObjectStore\Resource\DataObject $object + * @param OpenCloud\OpenStack $connection + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - public function it_deletes_file_fails($connectionFactory, $connection, $objectStore, $container, $object) + function it_deletes_file_fails($connectionFactory, $connection, $objectStore, $container, $object) { $object->delete()->willThrow(new DeleteError()); $container->dataObject("test")->willReturn($object); @@ -203,13 +198,14 @@ public function it_deletes_file_fails($connectionFactory, $connection, $objectSt $this->delete('test')->shouldReturn(false); } + /** * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - * @param OpenCloud\OpenStack $connection - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\OpenStack $connection + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container */ - public function it_deletes_file_does_not_exist_returns_false($connectionFactory, $connection, $objectStore, $container) + function it_deletes_file_does_not_exist_returns_false($connectionFactory, $connection, $objectStore, $container) { $container->dataObject("test")->willThrow(new ObjFetchError()); $objectStore->container("test")->willReturn($container); @@ -221,12 +217,12 @@ public function it_deletes_file_does_not_exist_returns_false($connectionFactory, /** * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - * @param OpenCloud\OpenStack $connection - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\ObjectStore\Resource\DataObject $object + * @param OpenCloud\OpenStack $connection + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - public function it_checksum_returns_string($connectionFactory, $connection, $objectStore, $container, $object) + function it_checksum_returns_string($connectionFactory, $connection, $objectStore, $container, $object) { $object->getETag()->willReturn("test String"); $container->dataObject("test")->willReturn($object); @@ -235,15 +231,15 @@ public function it_checksum_returns_string($connectionFactory, $connection, $obj $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); $this->checksum('test')->shouldReturn("test String"); - } + /** * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - * @param OpenCloud\OpenStack $connection - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\OpenStack $connection + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container */ - public function it_checksum_file_does_not_exist_returns_false($connectionFactory, $connection, $objectStore, $container) + function it_checksum_file_does_not_exist_returns_false($connectionFactory, $connection, $objectStore, $container) { $container->dataObject("test")->willThrow(new ObjFetchError()); $objectStore->container("test")->willReturn($container); @@ -252,28 +248,32 @@ public function it_checksum_file_does_not_exist_returns_false($connectionFactory $this->checksum('test')->shouldReturn(false); } + /** * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - * @param OpenCloud\OpenStack $connection - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\Common\Collection $objectList + * @param OpenCloud\OpenStack $connection + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\Common\Collection $objectList */ - public function it_keys_returns_sorted_array($connectionFactory, $connection, $objectStore, $container, $objectList) + function it_keys_returns_sorted_array($connectionFactory, $connection, $objectStore, $container, $objectList) { - $inputArray = array('key5', 'key2', 'key1'); + $inputArray = array ('key5', 'key2', 'key1'); $outputArray = $inputArray; sort($outputArray); $index = 0; - $objectList->next()->will(function() use($inputArray, &$index){ - if ($index < count($inputArray)) { - $objectItem = new \stdClass(); - $objectItem->name = $inputArray[$index]; - $index++; - return $objectItem; - } - })->shouldBeCalledTimes(count($inputArray)+1); + $objectList->next()->will( + function () use ($inputArray, &$index) { + if ($index < count($inputArray)) { + $objectItem = new \stdClass(); + $objectItem->name = $inputArray[$index]; + $index++; + + return $objectItem; + } + } + ) ->shouldBeCalledTimes(count($inputArray) + 1); $container->objectList()->willReturn($objectList); $objectStore->container("test")->willReturn($container); @@ -282,5 +282,4 @@ public function it_keys_returns_sorted_array($connectionFactory, $connection, $o $this->keys()->shouldReturn($outputArray); } - -} +} diff --git a/spec/Gaufrette/Adapter/OpenCloudSpec.php b/spec/Gaufrette/Adapter/OpenCloudSpec.php index cf438cc38..a42c52f57 100644 --- a/spec/Gaufrette/Adapter/OpenCloudSpec.php +++ b/spec/Gaufrette/Adapter/OpenCloudSpec.php @@ -1,5 +1,4 @@ */ -class OpenCloudSpec extends ObjectBehavior { +class OpenCloudSpec extends ObjectBehavior +{ /** * @param OpenCloud\ObjectStore\Service $objectStore */ - public function let($objectStore) + function let($objectStore) { $this->beConstructedWith($objectStore, 'test', false, false); } - public function it_is_adapter() + function it_is_adapter() { $this->shouldHaveType('Gaufrette\Adapter'); } /** - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - public function it_reads_file($objectStore, $container, $object) + function it_reads_file($objectStore, $container, $object) { $object->saveToString()->willReturn("Hello World"); $container->dataObject("test")->willReturn($object); @@ -41,10 +41,10 @@ public function it_reads_file($objectStore, $container, $object) } /** - * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Service $objectStore * @param OpenCloud\ObjectStore\Resource\Container $container */ - public function it_reads_file_on_error_returns_false($objectStore, $container) + function it_reads_file_on_error_returns_false($objectStore, $container) { $container->dataObject("test")->willThrow(new ObjFetchError()); $objectStore->container("test")->willReturn($container); @@ -53,15 +53,15 @@ public function it_reads_file_on_error_returns_false($objectStore, $container) } /** - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - public function it_writes_file_if_key_does_not_exist_returns_size($objectStore, $container, $object) + function it_writes_file_if_key_does_not_exist_returns_size($objectStore, $container, $object) { - $testData = "Hello World!"; + $testData = "Hello World!"; $testDataSize = sizeof($testData); - $object->create(array('name'=>'test'))->willReturn(null); + $object->create(array ('name' => 'test'))->willReturn(null); $object->setData($testData)->willReturn(null); $object->bytes = $testDataSize; $container->dataObject('test')->willReturn(false); @@ -69,78 +69,73 @@ public function it_writes_file_if_key_does_not_exist_returns_size($objectStore, $objectStore->container("test")->willReturn($container); $this->write('test', $testData)->shouldReturn($testDataSize); - } /** - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - public function it_writes_file_if_key_exists_returns_size($objectStore, $container, $object) + function it_writes_file_if_key_exists_returns_size($objectStore, $container, $object) { - $testData = "Hello World!"; - $testDataSize = sizeof($testData); + $testData = "Hello World!"; + $testDataSize = sizeof($testData); $object->bytes = $testDataSize; $container->dataObject('test')->willReturn($object); $objectStore->container("test")->willReturn($container); $this->write('test', $testData)->shouldReturn($testDataSize); - } /** - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - public function it_writes_file_and_write_fails_returns_false($objectStore, $container, $object) + function it_writes_file_and_write_fails_returns_false($objectStore, $container, $object) { $testData = "Hello World!"; - $object->create(array('name'=>'test'))->willThrow(new CreateUpdateError()); + $object->create(array ('name' => 'test'))->willThrow(new CreateUpdateError()); $object->setData($testData)->willReturn(null); $container->dataObject('test')->willReturn(false); $container->dataObject()->willReturn($object); $objectStore->container("test")->willReturn($container); $this->write('test', $testData)->shouldReturn(false); - } /** - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - public function if_key_exists_return_true($objectStore, $container, $object) + function if_key_exists_return_true($objectStore, $container, $object) { $container->dataObject('test')->willReturn($object); $objectStore->container("test")->willReturn($container); $this->exists('test')->shouldReturn(true); - } /** - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - public function if_key_does_not_exist_return_false($objectStore, $container, $object) + function if_key_does_not_exist_return_false($objectStore, $container, $object) { $container->dataObject('test')->willReturn($object)->willThrow(new ObjFetchError()); $objectStore->container("test")->willReturn($container); $this->exists('test')->shouldReturn(false); - } /** - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - public function it_deletes_file_on_success_returns_true($objectStore, $container, $object) + function it_deletes_file_on_success_returns_true($objectStore, $container, $object) { $object->delete()->willReturn(null); $container->dataObject("test")->willReturn($object); @@ -150,11 +145,11 @@ public function it_deletes_file_on_success_returns_true($objectStore, $container } /** - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - public function it_deletes_file_returns_false_on_failure($objectStore, $container, $object) + function it_deletes_file_returns_false_on_failure($objectStore, $container, $object) { $object->delete()->willThrow(new DeleteError()); $container->dataObject("test")->willReturn($object); @@ -162,11 +157,12 @@ public function it_deletes_file_returns_false_on_failure($objectStore, $containe $this->delete('test')->shouldReturn(false); } + /** - * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Service $objectStore * @param OpenCloud\ObjectStore\Resource\Container $container */ - public function it_deletes_file_if_file_does_not_exist_returns_false($objectStore, $container) + function it_deletes_file_if_file_does_not_exist_returns_false($objectStore, $container) { $container->dataObject("test")->willThrow(new ObjFetchError()); $objectStore->container("test")->willReturn($container); @@ -175,24 +171,24 @@ public function it_deletes_file_if_file_does_not_exist_returns_false($objectStor } /** - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - public function it_returns_checksum_if_file_exists($objectStore, $container, $object) + function it_returns_checksum_if_file_exists($objectStore, $container, $object) { $object->getETag()->willReturn("test String"); $container->dataObject("test")->willReturn($object); $objectStore->container("test")->willReturn($container); $this->checksum('test')->shouldReturn("test String"); - } + /** - * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Service $objectStore * @param OpenCloud\ObjectStore\Resource\Container $container */ - public function it_returns_false_when_file_does_not_exist($objectStore, $container) + function it_returns_false_when_file_does_not_exist($objectStore, $container) { $container->dataObject("test")->willThrow(new ObjFetchError()); $objectStore->container("test")->willReturn($container); @@ -201,30 +197,32 @@ public function it_returns_false_when_file_does_not_exist($objectStore, $contain } /** - * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Service $objectStore * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\Common\Collection $objectList + * @param OpenCloud\Common\Collection $objectList */ - public function it_returns_files_as_sorted_array($objectStore, $container, $objectList) + function it_returns_files_as_sorted_array($objectStore, $container, $objectList) { - $inputArray = array('key5', 'key2', 'key1'); + $inputArray = array ('key5', 'key2', 'key1'); $outputArray = $inputArray; sort($outputArray); $index = 0; - $objectList->next()->will(function() use($inputArray, &$index){ - if ($index < count($inputArray)) { - $objectItem = new \stdClass(); - $objectItem->name = $inputArray[$index]; - $index++; - return $objectItem; - } - })->shouldBeCalledTimes(count($inputArray)+1); + $objectList->next()->will( + function () use ($inputArray, &$index) { + if ($index < count($inputArray)) { + $objectItem = new \stdClass(); + $objectItem->name = $inputArray[$index]; + $index++; + + return $objectItem; + } + } + ) ->shouldBeCalledTimes(count($inputArray) + 1); $container->objectList()->willReturn($objectList); $objectStore->container("test")->willReturn($container); $this->keys()->shouldReturn($outputArray); } - -} +} diff --git a/src/Gaufrette/Adapter/LazyOpenStackCloudFiles.php b/src/Gaufrette/Adapter/LazyOpenStackCloudFiles.php index 9a8667fa3..e31fa0bd6 100644 --- a/src/Gaufrette/Adapter/LazyOpenStackCloudFiles.php +++ b/src/Gaufrette/Adapter/LazyOpenStackCloudFiles.php @@ -1,5 +1,4 @@ + * @author Chris Warner */ -class LazyOpenStackCloudFiles extends OpenCloud { +class LazyOpenStackCloudFiles extends OpenCloud +{ /** * @var ConnectionFactoryInterface @@ -24,15 +24,15 @@ class LazyOpenStackCloudFiles extends OpenCloud { /** * @param ConnectionFactoryInterface $connectionFactory - * @param string $containerName - * @param bool $createContainer - * @param bool $detectContentType + * @param string $containerName + * @param bool $createContainer + * @param bool $detectContentType */ public function __construct($connectionFactory, $containerName, $createContainer = false, $detectContentType = true) { $this->connectionFactory = $connectionFactory; - $this->containerName = $containerName; - $this->createContainer = $createContainer; + $this->containerName = $containerName; + $this->createContainer = $createContainer; $this->detectContentType = $detectContentType; } @@ -42,6 +42,7 @@ public function __construct($connectionFactory, $containerName, $createContainer public function read($key) { $this->connect(); + return parent::read($key); } @@ -51,6 +52,7 @@ public function read($key) public function write($key, $content) { $this->connect(); + return parent::write($key, $content); } @@ -60,6 +62,7 @@ public function write($key, $content) public function exists($key) { $this->connect(); + return parent::exists($key); } @@ -69,6 +72,7 @@ public function exists($key) public function keys() { $this->connect(); + return parent::keys(); } @@ -78,6 +82,7 @@ public function keys() public function delete($key) { $this->connect(); + return parent::delete($key); } @@ -87,6 +92,7 @@ public function delete($key) public function rename($sourceKey, $targetKey) { $this->connect(); + parent::rename($sourceKey, $targetKey); } @@ -96,6 +102,7 @@ public function rename($sourceKey, $targetKey) public function isDirectory($key) { $this->connect(); + return parent::isDirectory($key); } @@ -105,6 +112,7 @@ public function isDirectory($key) public function checksum($key) { $this->connect(); + return parent::checksum($key); } @@ -114,17 +122,17 @@ public function checksum($key) public function mtime($key) { $this->connect(); + return parent::mtime($key); } protected function connect() { - if(!$this->connected){ + if (!$this->connected) { /** @var OpenStack $connection */ $connection = $this->connectionFactory->create(); $this->objectStore = $connection->objectStore(); } } - -} +} diff --git a/src/Gaufrette/Adapter/OpenCloud.php b/src/Gaufrette/Adapter/OpenCloud.php index 747775f06..955fe22fe 100644 --- a/src/Gaufrette/Adapter/OpenCloud.php +++ b/src/Gaufrette/Adapter/OpenCloud.php @@ -39,11 +39,12 @@ class OpenCloud implements Adapter, */ protected $container; - public function __construct(Service $objectStore, $containerName, $createContainer = false, $detectContentType = true) - { - $this->objectStore = $objectStore; - $this->containerName = $containerName; - $this->createContainer = $createContainer; + public function __construct( + Service $objectStore, $containerName, $createContainer = false, $detectContentType = true + ) { + $this->objectStore = $objectStore; + $this->containerName = $containerName; + $this->createContainer = $createContainer; $this->detectContentType = $detectContentType; } @@ -52,10 +53,10 @@ private function initialize() if (!$this->container instanceof Container) { if ($this->createContainer) { - $container = $this->objectStore->Container(); + $container = $this->objectStore->Container(); $container->name = $this->containerName; $container->Create(); - }else{ + } else { $container = $this->objectStore->Container($this->containerName); } $this->container = $container; @@ -77,10 +78,10 @@ public function read($key) // if there is a fetch error, tryGetObject returns false // If it returns false, php throws a fatal error because boolean is a non-object. $object = $this->tryGetObject($key); - if ($object) - { + if ($object) { return $object->SaveToString(); } + return $object; } @@ -97,23 +98,25 @@ public function write($key, $content) $this->initialize(); $object = $this->tryGetObject($key); - try{ - if ($object === false){ + try { + if ($object === false) { $object = $this->container->DataObject(); $object->SetData($content); - $data = array('name' => $key); + $data = array ('name' => $key); - if($this->detectContentType){ - $fileInfo = new \finfo(FILEINFO_MIME_TYPE); - $contentType = $fileInfo->buffer($content); + if ($this->detectContentType) { + $fileInfo = new \finfo(FILEINFO_MIME_TYPE); + $contentType = $fileInfo->buffer($content); $data['content_type'] = $contentType; } $object->Create($data); } + return $object->bytes; - } catch(CreateUpdateError $updateError) { + } + catch (CreateUpdateError $updateError) { return false; } } @@ -128,6 +131,7 @@ public function write($key, $content) public function exists($key) { $this->initialize(); + return ($this->tryGetObject($key) !== false); } @@ -140,11 +144,12 @@ public function keys() { $this->initialize(); $objectList = $this->container->ObjectList(); - $keys = array(); - while ( $object = $objectList->Next() ) { + $keys = array (); + while ($object = $objectList->Next()) { $keys[] = $object->name; } sort($keys); + return $keys; } @@ -178,16 +183,18 @@ public function mtime($key) public function delete($key) { $this->initialize(); - try{ + try { $object = $this->tryGetObject($key); if (!$object) { return false; } $object->Delete(); - } catch (DeleteError $deleteError) { + } + catch (DeleteError $deleteError) { return false; } + return true; } @@ -228,9 +235,10 @@ public function isDirectory($key) public function checksum($key) { $this->initialize(); - $object = $this->tryGetObject($key); - if ($object) + $object = $this->tryGetObject($key); + if ($object) { return $object->getETag(); + } return false; } @@ -241,9 +249,10 @@ public function checksum($key) */ protected function tryGetObject($key) { - try{ + try { return $this->container->DataObject($key); - }catch (ObjFetchError $objFetchError) { + } + catch (ObjFetchError $objFetchError) { return false; } } diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/BaseOpenStackAuthenticationFactory.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/BaseOpenStackAuthenticationFactory.php index abaff6556..ad0734e0a 100644 --- a/src/Gaufrette/Adapter/OpenStackCloudFiles/BaseOpenStackAuthenticationFactory.php +++ b/src/Gaufrette/Adapter/OpenStackCloudFiles/BaseOpenStackAuthenticationFactory.php @@ -9,9 +9,10 @@ /** * Class BaseOpenStackAuthenticationFactory * @package Gaufrette\Adapter\OpenStackCloudFiles - * @author Chris Warner + * @author Chris Warner */ -abstract class BaseOpenStackAuthenticationFactory implements ConnectionFactoryInterface { +abstract class BaseOpenStackAuthenticationFactory implements ConnectionFactoryInterface +{ /** * @var null|OpenStack|Rackspace @@ -39,19 +40,18 @@ abstract class BaseOpenStackAuthenticationFactory implements ConnectionFactoryIn protected $region; /** - * @param string $url - * @param string $apikey - * @param string $username - * @param string $region + * @param string $url + * @param string $apikey + * @param string $username + * @param string $region * @param null|string $tenant */ function __construct($url, $apikey, $username, $region, $tenant = null) { - $this->apikey = $apikey; - $this->region = $region; - $this->tenant = $tenant; - $this->url = $url; + $this->apikey = $apikey; + $this->region = $region; + $this->tenant = $tenant; + $this->url = $url; $this->username = $username; } - -} +} diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/ConnectionFactoryInterface.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/ConnectionFactoryInterface.php index 013cb2497..b401b854c 100644 --- a/src/Gaufrette/Adapter/OpenStackCloudFiles/ConnectionFactoryInterface.php +++ b/src/Gaufrette/Adapter/OpenStackCloudFiles/ConnectionFactoryInterface.php @@ -6,11 +6,9 @@ /** * Interface ConnectionFactoryInterface * @package Gaufrette\Adapter\OpenStackCloudFiles - * @author Chris Warner + * @author Chris Warner */ interface ConnectionFactoryInterface { - - public function create(); - -} + public function create(); +} diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/OpenStackAuthenticationFactory.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/OpenStackAuthenticationFactory.php index 54a6c9c75..21a10d152 100644 --- a/src/Gaufrette/Adapter/OpenStackCloudFiles/OpenStackAuthenticationFactory.php +++ b/src/Gaufrette/Adapter/OpenStackCloudFiles/OpenStackAuthenticationFactory.php @@ -8,7 +8,7 @@ /** * Class OpenStackAuthenticationFactory * @package Gaufrette\Adapter\OpenStackCloudFiles - * @author Chris Warner + * @author Chris Warner */ class OpenStackAuthenticationFactory extends BaseOpenStackAuthenticationFactory implements ConnectionFactoryInterface { @@ -17,17 +17,15 @@ class OpenStackAuthenticationFactory extends BaseOpenStackAuthenticationFactory */ public function create() { - if (null === $this->authenciationService) - { - $this->authenciationService = new OpenStack($this->url, array($this->username, $this->apikey)); + if (null === $this->authenciationService) { + $this->authenciationService = new OpenStack($this->url, array ($this->username, $this->apikey)); $this->authenciationService->authenticate(); $this->authenciationService->setDefaults('cloudFiles', null, $this->region); - } elseif($this->authenciationService->hasExpired()) { + } elseif ($this->authenciationService->hasExpired()) { $this->authenciationService->authenticate(); $this->authenciationService->setDefaults('cloudFiles', null, $this->region); } - return $this->authenciationService; + return $this->authenciationService; } - -} +} diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationFactory.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationFactory.php index 0fb4fd8b1..d7a6d5e88 100644 --- a/src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationFactory.php +++ b/src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationFactory.php @@ -9,7 +9,7 @@ /** * Class RackspaceAuthenticationConnectionFactory * @package Gaufrette\Adapter\OpenStackCloudFiles - * @author Chris Warner + * @author Chris Warner */ class RackspaceAuthenticationFactory extends BaseOpenStackAuthenticationFactory implements ConnectionFactoryInterface { @@ -19,17 +19,13 @@ class RackspaceAuthenticationFactory extends BaseOpenStackAuthenticationFactory */ public function create() { - if (null === $this->authenciationService) - { - $this->authenciationService = new Rackspace($this->url, array($this->username, $this->apikey)); + if (null === $this->authenciationService) { + $this->authenciationService = new Rackspace($this->url, array ($this->username, $this->apikey)); $this->authenciationService->authenticate(); - - } elseif ($this->authenciationService->expired()) { $this->authenciationService->authenticate(); } return $this->authenciationService; } - -} +} From c12f0c499e98d6c4a73b1eac4080a60b18e481ff Mon Sep 17 00:00:00 2001 From: Adam Woods Date: Wed, 9 Oct 2013 16:46:02 +1100 Subject: [PATCH 050/426] Changes private functions to protected for improved extendability. --- src/Gaufrette/Adapter/AwsS3.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index 3a8232bb2..bc430b128 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -198,7 +198,7 @@ public function isDirectory($key) * @throws \RuntimeException if the bucket does not exists or could not be * created */ - private function ensureBucketExists() + protected function ensureBucketExists() { if ($this->bucketExists) { return true; @@ -226,7 +226,7 @@ private function ensureBucketExists() return true; } - private function getOptions($key, array $options = array()) + protected function getOptions($key, array $options = array()) { $options['Bucket'] = $this->bucket; $options['Key'] = $this->computePath($key); @@ -234,7 +234,7 @@ private function getOptions($key, array $options = array()) return $options + $this->getMetadata($key); } - private function computePath($key) + protected function computePath($key) { if (empty($this->options['directory'])) { return $key; From 0e4f4a772577ecf7ba4bd599fbe73d17fbeb2ab2 Mon Sep 17 00:00:00 2001 From: Adam Woods Date: Wed, 9 Oct 2013 16:50:10 +1100 Subject: [PATCH 051/426] Adds .idea directory to .gitignore file for users who use PHPStorm. --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a91fe0141..1859ab608 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,4 @@ tests/adapters/* !tests/adapters/*.dist bin/* !bin/configure_test_env.sh +.idea From 2b5ce5e94c4e701bdfbc2d62d2e579ad29eb498d Mon Sep 17 00:00:00 2001 From: Adam Woods Date: Wed, 9 Oct 2013 17:44:30 +1100 Subject: [PATCH 052/426] Adds ability to set global options for the AwsS3 adapter. --- src/Gaufrette/Adapter/AwsS3.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index 3a8232bb2..b420aff3b 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -231,7 +231,13 @@ private function getOptions($key, array $options = array()) $options['Bucket'] = $this->bucket; $options['Key'] = $this->computePath($key); - return $options + $this->getMetadata($key); + /** + * Merge global options for adapter, which are set in the constructor, with metadata. + * Metadata will override global options. + */ + $options = array_merge($this->options, $options, $this->getMetadata($key)); + + return $options; } private function computePath($key) From e79c318a2d4b957ffef95459a18aabb3ba0cb2ec Mon Sep 17 00:00:00 2001 From: Adam Woods Date: Thu, 10 Oct 2013 10:17:39 +1100 Subject: [PATCH 053/426] Removes IDE-specific ignore. --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 1859ab608..a91fe0141 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,3 @@ tests/adapters/* !tests/adapters/*.dist bin/* !bin/configure_test_env.sh -.idea From b8d0bfd19a163f268fc14b4fd0e73d59f3b76455 Mon Sep 17 00:00:00 2001 From: Gesthispania Date: Thu, 10 Oct 2013 09:30:31 +0200 Subject: [PATCH 054/426] Add SSL/TSL option to FTP Adapter --- src/Gaufrette/Adapter/Ftp.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index e7d021fa9..dc358af81 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -26,6 +26,7 @@ class Ftp implements Adapter, protected $passive; protected $create; protected $mode; + protected $ssl; protected $fileData = array(); /** @@ -45,6 +46,7 @@ public function __construct($directory, $host, $options = array()) $this->passive = isset($options['passive']) ? $options['passive'] : false; $this->create = isset($options['create']) ? $options['create'] : false; $this->mode = isset($options['mode']) ? $options['mode'] : FTP_BINARY; + $this->ssl = isset($options['ssl']) ? $options['ssl'] : false; } /** @@ -492,7 +494,11 @@ private function getConnection() private function connect() { // open ftp connection - $this->connection = ftp_connect($this->host, $this->port); + if (!$this->ssl) { + $this->connection = ftp_connect($this->host, $this->port); + } else { + $this->connection = ftp_ssl_connect($this->host, $this->port); + } if (!$this->connection) { throw new \RuntimeException(sprintf('Could not connect to \'%s\' (port: %s).', $this->host, $this->port)); } From 0ca2cc82f4eeace44a52938b480efef5255c11ce Mon Sep 17 00:00:00 2001 From: Gesthispania Date: Thu, 10 Oct 2013 10:02:46 +0200 Subject: [PATCH 055/426] FTP SSL activated and check if host is capable to use it --- src/Gaufrette/Adapter/Ftp.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index dc358af81..1a5c10222 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -497,7 +497,11 @@ private function connect() if (!$this->ssl) { $this->connection = ftp_connect($this->host, $this->port); } else { - $this->connection = ftp_ssl_connect($this->host, $this->port); + if(function_exists('ftp_ssl_connect')) { + $this->connection = ftp_ssl_connect($this->host, $this->port); + } else { + throw new \RuntimeException('This Server Has No SSL-FTP Available.'); + } } if (!$this->connection) { throw new \RuntimeException(sprintf('Could not connect to \'%s\' (port: %s).', $this->host, $this->port)); From ec6edca770b64818fe12e4fa65bb1e72a76b45ae Mon Sep 17 00:00:00 2001 From: Gesthispania Date: Thu, 10 Oct 2013 10:16:16 +0200 Subject: [PATCH 056/426] Update Ftp.php --- src/Gaufrette/Adapter/Ftp.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index 1a5c10222..d23d597ac 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -46,7 +46,7 @@ public function __construct($directory, $host, $options = array()) $this->passive = isset($options['passive']) ? $options['passive'] : false; $this->create = isset($options['create']) ? $options['create'] : false; $this->mode = isset($options['mode']) ? $options['mode'] : FTP_BINARY; - $this->ssl = isset($options['ssl']) ? $options['ssl'] : false; + $this->ssl = isset($options['ssl']) ? $options['ssl'] : false; } /** From 28a6c1c70e76f2276804bd32b079a93aff90ab26 Mon Sep 17 00:00:00 2001 From: Teoh Han Hui Date: Fri, 1 Nov 2013 16:48:14 +0800 Subject: [PATCH 057/426] AwsS3 adapter: Added ACL option and BC for "ContentType" metadata --- src/Gaufrette/Adapter/AwsS3.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index 3a8232bb2..5c69a2428 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -25,7 +25,13 @@ public function __construct(S3Client $service, $bucket, array $options = array() { $this->service = $service; $this->bucket = $bucket; - $this->options = array_replace(array('create' => false, 'directory' => ''), $options); + $this->options = array_replace( + array( + 'create' => false, + 'directory' => '', + 'acl' => 'private', + ), $options + ); } /** @@ -54,6 +60,12 @@ public function getUrl($key, array $options = array()) */ public function setMetadata($key, $metadata) { + // BC with AmazonS3 adapter + if (isset($metadata['contentType'])) { + $metadata['ContentType'] = $metadata['contentType']; + unset($metadata['contentType']); + } + $this->metadata[$key] = $metadata; } @@ -228,6 +240,7 @@ private function ensureBucketExists() private function getOptions($key, array $options = array()) { + $options['ACL'] = $this->options['acl']; $options['Bucket'] = $this->bucket; $options['Key'] = $this->computePath($key); From 75094746821f55239acf2f86e25da9e0861c33d2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Maciej=20Czy=C5=BCewski?= Date: Fri, 1 Nov 2013 14:25:49 +0100 Subject: [PATCH 058/426] Update README.markdown --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 94eaeb778..77b9939bd 100644 --- a/README.markdown +++ b/README.markdown @@ -36,7 +36,7 @@ use Gaufrette\Filesystem; use Gaufrette\Adapter\Local as LocalAdapter; $adapter = new LocalAdapter('/var/media'); -$filesystem = new Filesystem($adapter) +$filesystem = new Filesystem($adapter); ``` ### Use the filesystem From 8e9a901b4535ef24e6e84db894dace69336d0b25 Mon Sep 17 00:00:00 2001 From: Vladislav Vlastovskiy Date: Mon, 11 Nov 2013 13:16:51 +0300 Subject: [PATCH 059/426] Added check exists dir --- src/Gaufrette/Adapter/Local.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/Local.php b/src/Gaufrette/Adapter/Local.php index 2de7f1eb2..d0a005a8c 100644 --- a/src/Gaufrette/Adapter/Local.php +++ b/src/Gaufrette/Adapter/Local.php @@ -239,7 +239,9 @@ protected function createDirectory($directory) umask($umask); if (!$created) { - throw new \RuntimeException(sprintf('The directory \'%s\' could not be created.', $directory)); + if (!is_dir($directory)) { + throw new \RuntimeException(sprintf('The directory \'%s\' could not be created.', $directory)); + } } } } From d28c0fd7f0ae16407ea9c1d3bf7e1a1259af99c9 Mon Sep 17 00:00:00 2001 From: Leszek Prabucki Date: Thu, 14 Nov 2013 20:21:43 +0100 Subject: [PATCH 060/426] Update README.markdown --- README.markdown | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/README.markdown b/README.markdown index 77b9939bd..4264c9832 100644 --- a/README.markdown +++ b/README.markdown @@ -25,6 +25,19 @@ solution. Try it! ------- +### Installation + +Development version: + +```bash +php composer.phar require knplabs/gaufrette:0.2.*@dev +``` +Stable version: + +```bash +php composer.phar require knplabs/gaufrette:0.1.* +``` + ### Setup your filesystem Following an example with the local filesystem adapter. To setup other adapters, look up the [testcases](https://github.com/KnpLabs/Gaufrette/tree/master/tests/Gaufrette/Functional/adapters). From d6170eb5b98a8a6b5934fe1765d07635afc0462e Mon Sep 17 00:00:00 2001 From: Leszek Prabucki Date: Fri, 15 Nov 2013 13:55:13 +0100 Subject: [PATCH 061/426] Add suggested libs when we want to use SFtp and PhpseclibSftp adapters --- composer.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/composer.json b/composer.json index a9fca2246..68e18f832 100644 --- a/composer.json +++ b/composer.json @@ -61,6 +61,8 @@ "knplabs/knp-gaufrette-bundle": "to use with Symfony2", "dropbox-php/dropbox-php": "to use the Dropbox adapter", "rackspace/php-opencloud" : "to use Opencloud adapter", + "herzult/php-ssh": "to use SFtp adapter", + "phpseclib/phpseclib": "to user PhpseclibSftp adapter", "aws/aws-sdk-php": "to use the Amazon S3 adapter", "amazonwebservices/aws-sdk-for-php": "to use the legacy Amazon S3 adapters", "phpseclib/phpseclib": "to use the SFTP", From 4d75272cd7b50d8d4d4f7a28c46aa193726852dc Mon Sep 17 00:00:00 2001 From: Leszek Prabucki Date: Fri, 15 Nov 2013 13:56:16 +0100 Subject: [PATCH 062/426] Update composer.json --- composer.json | 1 - 1 file changed, 1 deletion(-) diff --git a/composer.json b/composer.json index 68e18f832..3f80402bb 100644 --- a/composer.json +++ b/composer.json @@ -65,7 +65,6 @@ "phpseclib/phpseclib": "to user PhpseclibSftp adapter", "aws/aws-sdk-php": "to use the Amazon S3 adapter", "amazonwebservices/aws-sdk-for-php": "to use the legacy Amazon S3 adapters", - "phpseclib/phpseclib": "to use the SFTP", "doctrine/dbal": "to use the Doctrine DBAL adapter", "microsoft/windowsazure": "to use Microsoft Azure Blob Storage adapter", "ext-zip": "to use the Zip adapter", From 2633721877cae79ad461f3ca06f3f77fb4fce02e Mon Sep 17 00:00:00 2001 From: Douglas Greenshields Date: Sun, 17 Nov 2013 17:00:58 +0000 Subject: [PATCH 063/426] replace fake file manipulation functions with vfsStream mock filesystem in specs --- composer.json | 3 +- composer.lock | 195 +++++++++++------- spec/Gaufrette/Adapter/LocalSpec.php | 65 +++--- .../Gaufrette/Adapter/MockFilesystem/dir/file | 0 .../Gaufrette/Adapter/MockFilesystem/filename | 1 + spec/Gaufrette/Adapter/SafeLocalSpec.php | 16 +- spec/Gaufrette/Adapter/functions.php | 58 +----- spec/Gaufrette/Util/PathSpec.php | 2 + src/Gaufrette/Util/Path.php | 2 +- 9 files changed, 163 insertions(+), 179 deletions(-) create mode 100644 spec/Gaufrette/Adapter/MockFilesystem/dir/file create mode 100644 spec/Gaufrette/Adapter/MockFilesystem/filename diff --git a/composer.json b/composer.json index 3f80402bb..c73accc94 100644 --- a/composer.json +++ b/composer.json @@ -55,7 +55,8 @@ "dropbox-php/dropbox-php": "*", "herzult/php-ssh": "*", "phpunit/phpunit": "3.7.*", - "microsoft/windowsazure": "dev-master" + "microsoft/windowsazure": "dev-master", + "mikey179/vfsStream": "~1.2.0" }, "suggest": { "knplabs/knp-gaufrette-bundle": "to use with Symfony2", diff --git a/composer.lock b/composer.lock index 277d8dfcd..33e840ac1 100644 --- a/composer.lock +++ b/composer.lock @@ -3,7 +3,7 @@ "This file locks the dependencies of your project to a known state", "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" ], - "hash": "7b64d4c027c38f15e08ce5dcbed5bfbf", + "hash": "18ce61df78a3b95768776edf14952f62", "packages": [ ], @@ -64,12 +64,12 @@ "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "5ac62533b924b92eb7756df8c5f95a434eca7c9b" + "reference": "b494eb871d0aa3ba3b0a47a895fb18c6226edb88" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/5ac62533b924b92eb7756df8c5f95a434eca7c9b", - "reference": "5ac62533b924b92eb7756df8c5f95a434eca7c9b", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b494eb871d0aa3ba3b0a47a895fb18c6226edb88", + "reference": "b494eb871d0aa3ba3b0a47a895fb18c6226edb88", "shasum": "" }, "require": { @@ -122,7 +122,7 @@ "s3", "sdk" ], - "time": "2013-10-02 20:19:48" + "time": "2013-11-14 17:24:28" }, { "name": "doctrine/annotations", @@ -164,7 +164,8 @@ { "name": "Jonathan Wage", "email": "jonwage@gmail.com", - "homepage": "http://www.jwage.com/" + "homepage": "http://www.jwage.com/", + "role": "Creator" }, { "name": "Guilherme Blanco", @@ -197,16 +198,16 @@ }, { "name": "doctrine/cache", - "version": "v1.2.0", + "version": "v1.3.0", "source": { "type": "git", "url": "https://github.com/doctrine/cache.git", - "reference": "d0e4447707a064a5814b18cb0dcc2f24185f6179" + "reference": "e16d7adf45664a50fa86f515b6d5e7f670130449" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/d0e4447707a064a5814b18cb0dcc2f24185f6179", - "reference": "d0e4447707a064a5814b18cb0dcc2f24185f6179", + "url": "https://api.github.com/repos/doctrine/cache/zipball/e16d7adf45664a50fa86f515b6d5e7f670130449", + "reference": "e16d7adf45664a50fa86f515b6d5e7f670130449", "shasum": "" }, "require": { @@ -215,6 +216,10 @@ "conflict": { "doctrine/common": ">2.2,<2.4" }, + "require-dev": { + "phpunit/phpunit": ">=3.7", + "satooshi/php-coveralls": "~0.6" + }, "type": "library", "extra": { "branch-alias": { @@ -232,9 +237,10 @@ ], "authors": [ { - "name": "Jonathan Wage", + "name": "Jonathan H. Wage", "email": "jonwage@gmail.com", - "homepage": "http://www.jwage.com/" + "homepage": "http://www.jwage.com/", + "role": "Creator" }, { "name": "Guilherme Blanco", @@ -262,7 +268,7 @@ "cache", "caching" ], - "time": "2013-09-26 19:23:25" + "time": "2013-10-25 19:04:14" }, { "name": "doctrine/collections", @@ -298,9 +304,10 @@ ], "authors": [ { - "name": "Jonathan Wage", + "name": "Jonathan H. Wage", "email": "jonwage@gmail.com", - "homepage": "http://www.jwage.com/" + "homepage": "http://www.jwage.com/", + "role": "Creator" }, { "name": "Guilherme Blanco", @@ -372,7 +379,8 @@ { "name": "Jonathan Wage", "email": "jonwage@gmail.com", - "homepage": "http://www.jwage.com/" + "homepage": "http://www.jwage.com/", + "role": "Creator" }, { "name": "Guilherme Blanco", @@ -411,12 +419,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "a67755c2726360ee697bae6105d29dcffb9f000c" + "reference": "5fdedc4f8f304e8035580807bd36d59e97a87265" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/a67755c2726360ee697bae6105d29dcffb9f000c", - "reference": "a67755c2726360ee697bae6105d29dcffb9f000c", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/5fdedc4f8f304e8035580807bd36d59e97a87265", + "reference": "5fdedc4f8f304e8035580807bd36d59e97a87265", "shasum": "" }, "require": { @@ -449,7 +457,8 @@ { "name": "Jonathan Wage", "email": "jonwage@gmail.com", - "homepage": "http://www.jwage.com/" + "homepage": "http://www.jwage.com/", + "role": "Creator" }, { "name": "Guilherme Blanco", @@ -473,7 +482,7 @@ "persistence", "queryobject" ], - "time": "2013-10-02 10:10:08" + "time": "2013-11-15 14:43:05" }, { "name": "doctrine/inflector", @@ -511,7 +520,8 @@ { "name": "Jonathan Wage", "email": "jonwage@gmail.com", - "homepage": "http://www.jwage.com/" + "homepage": "http://www.jwage.com/", + "role": "Creator" }, { "name": "Guilherme Blanco", @@ -529,7 +539,7 @@ { "name": "Johannes Schmitt", "email": "schmittjoh@gmail.com", - "homepage": "https://github.com/schmittjoh", + "homepage": "http://jmsyst.com", "role": "Developer of wrapped JMSSerializerBundle" } ], @@ -666,12 +676,12 @@ "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "b170b028c6bb5799640e46c8803015b0f9a45ed9" + "reference": "d2e8ac50742672ca0947e3f5459a4163eb5f9cbc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/b170b028c6bb5799640e46c8803015b0f9a45ed9", - "reference": "b170b028c6bb5799640e46c8803015b0f9a45ed9", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d2e8ac50742672ca0947e3f5459a4163eb5f9cbc", + "reference": "d2e8ac50742672ca0947e3f5459a4163eb5f9cbc", "shasum": "" }, "require": { @@ -750,7 +760,7 @@ "rest", "web service" ], - "time": "2013-10-02 20:47:00" + "time": "2013-11-14 20:23:30" }, { "name": "herzult/php-ssh", @@ -807,12 +817,12 @@ "source": { "type": "git", "url": "https://github.com/WindowsAzure/azure-sdk-for-php.git", - "reference": "d8daa05ad1852e0c70a85d15a6db88f7e3b23405" + "reference": "74a0475b4fe28fbe9f73660bd9ae2248148912f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WindowsAzure/azure-sdk-for-php/zipball/d8daa05ad1852e0c70a85d15a6db88f7e3b23405", - "reference": "d8daa05ad1852e0c70a85d15a6db88f7e3b23405", + "url": "https://api.github.com/repos/WindowsAzure/azure-sdk-for-php/zipball/74a0475b4fe28fbe9f73660bd9ae2248148912f9", + "reference": "74a0475b4fe28fbe9f73660bd9ae2248148912f9", "shasum": "" }, "require": { @@ -842,7 +852,37 @@ "php", "sdk" ], - "time": "2012-12-17 23:41:42" + "time": "2013-10-17 08:08:49" + }, + { + "name": "mikey179/vfsStream", + "version": "v1.2.0", + "source": { + "type": "git", + "url": "https://github.com/mikey179/vfsStream.git", + "reference": "063fb10633f10c5ccbcac26227e94f46d9336f90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mikey179/vfsStream/zipball/063fb10633f10c5ccbcac26227e94f46d9336f90", + "reference": "063fb10633f10c5ccbcac26227e94f46d9336f90", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "org\\bovigo\\vfs\\": "src/main/php" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD" + ], + "homepage": "http://vfs.bovigo.org/", + "time": "2013-04-01 10:41:02" }, { "name": "pear-pear.php.net/Archive_Tar", @@ -1098,12 +1138,12 @@ "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "77447a3fc7869773007fa3beeb8d6f8de842ec2a" + "reference": "f9aa658309d5fb6af6ef032f4072f588f011216b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/77447a3fc7869773007fa3beeb8d6f8de842ec2a", - "reference": "77447a3fc7869773007fa3beeb8d6f8de842ec2a", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/f9aa658309d5fb6af6ef032f4072f588f011216b", + "reference": "f9aa658309d5fb6af6ef032f4072f588f011216b", "shasum": "" }, "require": { @@ -1164,20 +1204,20 @@ "x.509", "x509" ], - "time": "2013-09-20 17:14:01" + "time": "2013-11-15 21:07:14" }, { "name": "phpspec/php-diff", - "version": "v1.0.1", + "version": "v1.0.2", "source": { "type": "git", "url": "https://github.com/phpspec/php-diff.git", - "reference": "8d82ac415225fac373a4073ba14b1fe286aa2312" + "reference": "30e103d19519fe678ae64a60d77884ef3d71b28a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/php-diff/zipball/8d82ac415225fac373a4073ba14b1fe286aa2312", - "reference": "8d82ac415225fac373a4073ba14b1fe286aa2312", + "url": "https://api.github.com/repos/phpspec/php-diff/zipball/30e103d19519fe678ae64a60d77884ef3d71b28a", + "reference": "30e103d19519fe678ae64a60d77884ef3d71b28a", "shasum": "" }, "type": "library", @@ -1197,7 +1237,7 @@ } ], "description": "A comprehensive library for generating differences between two hashable objects (strings or arrays).", - "time": "2012-11-08 08:55:45" + "time": "2013-11-01 13:02:21" }, { "name": "phpspec/phpspec", @@ -1205,12 +1245,12 @@ "source": { "type": "git", "url": "https://github.com/phpspec/phpspec.git", - "reference": "79f91276fc8c853596b8180103d93762072e457a" + "reference": "c631a7c980d5be303ba513c1598a663393a59028" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/phpspec/zipball/79f91276fc8c853596b8180103d93762072e457a", - "reference": "79f91276fc8c853596b8180103d93762072e457a", + "url": "https://api.github.com/repos/phpspec/phpspec/zipball/c631a7c980d5be303ba513c1598a663393a59028", + "reference": "c631a7c980d5be303ba513c1598a663393a59028", "shasum": "" }, "require": { @@ -1265,7 +1305,7 @@ "testing", "tests" ], - "time": "2013-09-02 20:20:07" + "time": "2013-11-13 10:07:05" }, { "name": "phpspec/prophecy", @@ -1273,12 +1313,12 @@ "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "82d11835df7eccf904ea0c84fbb7304080346bc1" + "reference": "8177460b13b087a7e52faffa9492f2d3e91d19fc" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/82d11835df7eccf904ea0c84fbb7304080346bc1", - "reference": "82d11835df7eccf904ea0c84fbb7304080346bc1", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/8177460b13b087a7e52faffa9492f2d3e91d19fc", + "reference": "8177460b13b087a7e52faffa9492f2d3e91d19fc", "shasum": "" }, "require-dev": { @@ -1320,7 +1360,7 @@ "spy", "stub" ], - "time": "2013-09-30 08:21:23" + "time": "2013-11-06 13:00:43" }, { "name": "phpunit/php-code-coverage", @@ -1328,12 +1368,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "466e7cd2554b4e264c9e3f31216d25ac0e5f3d94" + "reference": "10c5b2c8b72a801eaad0031cd6a6a3686909c5a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/466e7cd2554b4e264c9e3f31216d25ac0e5f3d94", - "reference": "466e7cd2554b4e264c9e3f31216d25ac0e5f3d94", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/10c5b2c8b72a801eaad0031cd6a6a3686909c5a9", + "reference": "10c5b2c8b72a801eaad0031cd6a6a3686909c5a9", "shasum": "" }, "require": { @@ -1381,7 +1421,7 @@ "testing", "xunit" ], - "time": "2013-09-10 08:14:32" + "time": "2013-11-01 09:17:11" }, { "name": "phpunit/php-file-iterator", @@ -1389,12 +1429,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "2deb24c65ea78e126daa8d45b2089ddc29ec1d26" + "reference": "acd690379117b042d1c8af1fafd61bde001bf6bb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2deb24c65ea78e126daa8d45b2089ddc29ec1d26", - "reference": "2deb24c65ea78e126daa8d45b2089ddc29ec1d26", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/acd690379117b042d1c8af1fafd61bde001bf6bb", + "reference": "acd690379117b042d1c8af1fafd61bde001bf6bb", "shasum": "" }, "require": { @@ -1426,7 +1466,7 @@ "filesystem", "iterator" ], - "time": "2013-01-07 10:47:05" + "time": "2013-10-10 15:34:57" }, { "name": "phpunit/php-text-template", @@ -1522,12 +1562,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "5220af2a7929aa35cf663d97c89ad3d50cf5fa3e" + "reference": "292f4d5772dad5a12775be69f4a8dd663b20f103" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/5220af2a7929aa35cf663d97c89ad3d50cf5fa3e", - "reference": "5220af2a7929aa35cf663d97c89ad3d50cf5fa3e", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/292f4d5772dad5a12775be69f4a8dd663b20f103", + "reference": "292f4d5772dad5a12775be69f4a8dd663b20f103", "shasum": "" }, "require": { @@ -1564,7 +1604,7 @@ "keywords": [ "tokenizer" ], - "time": "2013-09-13 04:58:23" + "time": "2013-10-21 14:03:39" }, { "name": "phpunit/phpunit", @@ -1572,12 +1612,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "4b024e753e3421837afbcca962c8724c58b39376" + "reference": "236f65cc97d6beaa8fcb8a27b19bd278f3912677" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4b024e753e3421837afbcca962c8724c58b39376", - "reference": "4b024e753e3421837afbcca962c8724c58b39376", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/236f65cc97d6beaa8fcb8a27b19bd278f3912677", + "reference": "236f65cc97d6beaa8fcb8a27b19bd278f3912677", "shasum": "" }, "require": { @@ -1638,7 +1678,7 @@ "testing", "xunit" ], - "time": "2013-09-16 03:09:52" + "time": "2013-11-06 01:58:51" }, { "name": "phpunit/phpunit-mock-objects", @@ -1755,7 +1795,8 @@ }, { "name": "Jamie Hannaford", - "email": "jamie@limetree.org" + "email": "jamie.hannaford@rackspace.com", + "homepage": "https://github.com/jamiehannaford" } ], "description": "Rackspace open cloud php library", @@ -1772,12 +1813,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/Console.git", - "reference": "ed9b8a905b21e3d4bab8b6e0e016a076aefcc5bc" + "reference": "846e8fa8fdb5fab85cdb053b26687dd073c33051" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Console/zipball/ed9b8a905b21e3d4bab8b6e0e016a076aefcc5bc", - "reference": "ed9b8a905b21e3d4bab8b6e0e016a076aefcc5bc", + "url": "https://api.github.com/repos/symfony/Console/zipball/846e8fa8fdb5fab85cdb053b26687dd073c33051", + "reference": "846e8fa8fdb5fab85cdb053b26687dd073c33051", "shasum": "" }, "require": { @@ -1816,7 +1857,7 @@ ], "description": "Symfony Console Component", "homepage": "http://symfony.com", - "time": "2013-10-02 07:34:37" + "time": "2013-11-13 21:30:16" }, { "name": "symfony/event-dispatcher", @@ -1825,12 +1866,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/EventDispatcher.git", - "reference": "9b4fe5757870682eb2251e283228a66d938265a8" + "reference": "d51d78b34c1d9dcc384ba48155105fe99284dd67" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/9b4fe5757870682eb2251e283228a66d938265a8", - "reference": "9b4fe5757870682eb2251e283228a66d938265a8", + "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/d51d78b34c1d9dcc384ba48155105fe99284dd67", + "reference": "d51d78b34c1d9dcc384ba48155105fe99284dd67", "shasum": "" }, "require": { @@ -1870,7 +1911,7 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "http://symfony.com", - "time": "2013-09-19 09:47:34" + "time": "2013-10-17 11:48:11" }, { "name": "symfony/finder", @@ -1926,12 +1967,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/Yaml.git", - "reference": "ef4878bcca171ebe7466be0505b85a710818d788" + "reference": "1f7cabb841e62ec49615bd965ac780fd994b3f64" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Yaml/zipball/ef4878bcca171ebe7466be0505b85a710818d788", - "reference": "ef4878bcca171ebe7466be0505b85a710818d788", + "url": "https://api.github.com/repos/symfony/Yaml/zipball/1f7cabb841e62ec49615bd965ac780fd994b3f64", + "reference": "1f7cabb841e62ec49615bd965ac780fd994b3f64", "shasum": "" }, "require": { @@ -1964,7 +2005,7 @@ ], "description": "Symfony Yaml Component", "homepage": "http://symfony.com", - "time": "2013-09-22 18:04:51" + "time": "2013-10-17 11:48:11" } ], "aliases": [ diff --git a/spec/Gaufrette/Adapter/LocalSpec.php b/spec/Gaufrette/Adapter/LocalSpec.php index 6390d4e65..bd786a6c5 100644 --- a/spec/Gaufrette/Adapter/LocalSpec.php +++ b/spec/Gaufrette/Adapter/LocalSpec.php @@ -2,22 +2,16 @@ namespace spec\Gaufrette\Adapter; -//hack - mock php built-in functions -require_once 'functions.php'; - +use org\bovigo\vfs\vfsStream; use PhpSpec\ObjectBehavior; class LocalSpec extends ObjectBehavior { function let() { - $this->beConstructedWith('/home/somedir'); - } - - function letgo() - { - global $iteratorToArray; - $iteratorToArray = array(); + vfsStream::setup('test'); + vfsStream::copyFromFileSystem(__DIR__.'/MockFilesystem'); + $this->beConstructedWith(vfsStream::url('test')); } function it_is_adapter() @@ -37,7 +31,7 @@ function it_is_stream_factory() function it_reads_file() { - $this->read('filename')->shouldReturn('/home/somedir/filename content'); + $this->read('filename')->shouldReturn("content\n"); } function it_writes_file() @@ -47,7 +41,7 @@ function it_writes_file() function it_renames_file() { - $this->rename('filename', 'aaa/filename2')->shouldReturn('/home/somedir/filename to /home/somedir/aaa/filename2'); + $this->rename('filename', 'aaa/filename2')->shouldReturn(true); } function it_checks_if_file_exists() @@ -58,15 +52,15 @@ function it_checks_if_file_exists() function it_fetches_keys() { - global $iteratorToArray; - $iteratorToArray = array('/home/somedir/filename', '/home/somedir/filename1', '/home/somedir/aaa/filename'); - - $this->keys()->shouldReturn(array('aaa', 'aaa/filename', 'filename', 'filename1')); + $expectedKeys = array('filename', 'dir', 'dir/file'); + sort($expectedKeys); + $this->keys()->shouldReturn($expectedKeys); } function it_fetches_mtime() { - $this->mtime('filename')->shouldReturn(12345); + $mtime = filemtime(vfsStream::url('test/filename')); + $this->mtime('filename')->shouldReturn($mtime); } function it_deletes_file() @@ -86,75 +80,68 @@ function it_creates_local_stream() $this->createStream('filename')->shouldReturnAnInstanceOf('Gaufrette\Stream\Local'); } - function it_allows_to_work_with_symbolic_links() - { - $this->beConstructedWith('symbolicLink'); - - $this->read('filename')->shouldReturn('/home/somedir/filename content'); - } - function it_does_not_allow_to_read_path_above_main_file_directory() { $this - ->shouldThrow(new \OutOfBoundsException('The path "/home/filename" is out of the filesystem.')) + ->shouldThrow(new \OutOfBoundsException(sprintf('The path "%s" is out of the filesystem.', vfsStream::url('filename')))) ->duringRead('../filename') ; $this - ->shouldThrow(new \OutOfBoundsException('The path "/home/filename" is out of the filesystem.')) + ->shouldThrow(new \OutOfBoundsException(sprintf('The path "%s" is out of the filesystem.', vfsStream::url('filename')))) ->duringExists('../filename') ; } function it_fails_when_directory_does_not_exists() { - $this->beConstructedWith('/home/other'); + $this->beConstructedWith(vfsStream::url('other')); $this - ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) + ->shouldThrow(new \RuntimeException(sprintf('The directory "%s" does not exist.', vfsStream::url('other')))) ->duringRead('filename') ; $this - ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) + ->shouldThrow(new \RuntimeException(sprintf('The directory "%s" does not exist.', vfsStream::url('other')))) ->duringWrite('filename', 'some content') ; $this - ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) + ->shouldThrow(new \RuntimeException(sprintf('The directory "%s" does not exist.', vfsStream::url('other')))) ->duringRename('filename', 'otherFilename') ; $this - ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) + ->shouldThrow(new \RuntimeException(sprintf('The directory "%s" does not exist.', vfsStream::url('other')))) ->duringExists('filename') ; $this - ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) + ->shouldThrow(new \RuntimeException(sprintf('The directory "%s" does not exist.', vfsStream::url('other')))) ->duringKeys() ; $this - ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) + ->shouldThrow(new \RuntimeException(sprintf('The directory "%s" does not exist.', vfsStream::url('other')))) ->duringMtime('filename') ; $this - ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) + ->shouldThrow(new \RuntimeException(sprintf('The directory "%s" does not exist.', vfsStream::url('other')))) ->duringDelete('filename') ; $this - ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) + ->shouldThrow(new \RuntimeException(sprintf('The directory "%s" does not exist.', vfsStream::url('other')))) ->duringIsDirectory('filename') ; $this - ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) + ->shouldThrow(new \RuntimeException(sprintf('The directory "%s" does not exist.', vfsStream::url('other')))) ->duringCreateStream('filename') ; $this - ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) + ->shouldThrow(new \RuntimeException(sprintf('The directory "%s" does not exist.', vfsStream::url('other')))) ->duringChecksum('filename') ; } function it_creates_directory_when_does_not_exists() { - $this->beConstructedWith('/home/other', true); + $this->beConstructedWith(vfsStream::url('test/other'), true); - $this->read('filename')->shouldReturn('/home/other/filename content'); + $this->exists('/')->shouldReturn(true); } } diff --git a/spec/Gaufrette/Adapter/MockFilesystem/dir/file b/spec/Gaufrette/Adapter/MockFilesystem/dir/file new file mode 100644 index 000000000..e69de29bb diff --git a/spec/Gaufrette/Adapter/MockFilesystem/filename b/spec/Gaufrette/Adapter/MockFilesystem/filename new file mode 100644 index 000000000..d95f3ad14 --- /dev/null +++ b/spec/Gaufrette/Adapter/MockFilesystem/filename @@ -0,0 +1 @@ +content diff --git a/spec/Gaufrette/Adapter/SafeLocalSpec.php b/spec/Gaufrette/Adapter/SafeLocalSpec.php index b964d4fd8..2aa076fb7 100644 --- a/spec/Gaufrette/Adapter/SafeLocalSpec.php +++ b/spec/Gaufrette/Adapter/SafeLocalSpec.php @@ -2,16 +2,16 @@ namespace spec\Gaufrette\Adapter; -//hack - mock php built-in functions -require_once 'functions.php'; - +use org\bovigo\vfs\vfsStream; use PhpSpec\ObjectBehavior; class SafeLocalSpec extends ObjectBehavior { function let() { - $this->beConstructedWith('/home/l3l0'); + vfsStream::setup('test'); + vfsStream::copyFromFileSystem(__DIR__.'/MockFilesystem'); + $this->beConstructedWith(vfsStream::url('test')); } function it_is_local_adapter() @@ -21,14 +21,12 @@ function it_is_local_adapter() function it_computes_path_using_base64() { - $this->read('filename')->shouldReturn('/home/l3l0/'.base64_encode('filename').' content'); + rename(vfsStream::url('test/filename'), vfsStream::url('test/'.base64_encode('filename'))); + $this->read('filename')->shouldReturn("content\n"); } function it_computes_key_back_using_base64() { - global $iteratorToArray; - $iteratorToArray = array('/home/l3l0/'.base64_encode('filename'), '/home/l3l0/'.base64_encode('filename1'), '/home/l3l0/'.base64_encode('aaa/filename')); - - $this->keys()->shouldReturn(array('aaa', 'aaa/filename', 'filename', 'filename1')); + $this->keys()->shouldReturn(array(base64_decode('dir/file'), base64_decode('filename'))); } } diff --git a/spec/Gaufrette/Adapter/functions.php b/spec/Gaufrette/Adapter/functions.php index e89b03ba8..166205fcc 100644 --- a/spec/Gaufrette/Adapter/functions.php +++ b/spec/Gaufrette/Adapter/functions.php @@ -222,37 +222,21 @@ function ftp_login($connection, $username, $password) return true; } -function file_get_contents($path) -{ - return sprintf('%s content', $path); -} - function time() { return \strtotime('2012-10-10 23:10:10'); } -function file_put_contents($path, $content) -{ - return strlen($content); -} - -function rename($from, $to) -{ - return $from.' to '.$to; -} - function file_exists($path) { - return in_array($path, array('/home/l3l0/filename', '/home/somedir/filename', 'ssh+ssl://localhost/home/l3l0/filename')) ? true : false; -} - -function iterator_to_array($iterator) -{ - global $iteratorToArray; + //fake it for ssh+ssl: protocol for SFTP testing, otherwise delegate to global + if (strpos($path, 'ssh+ssl:') === 0) { + return in_array($path, array('/home/l3l0/filename', '/home/somedir/filename', 'ssh+ssl://localhost/home/l3l0/filename')) ? true : false; + } - return $iteratorToArray; + return \file_exists($path); } + function extension_loaded($name) { global $extensionLoaded; @@ -269,36 +253,6 @@ function opendir($url) return true; } -function filemtime($key) -{ - return 12345; -} - -function unlink($key) -{ - return in_array($key, array('/home/l3l0/filename', '/home/somedir/filename')) ? true : false; -} - -function is_dir($key) -{ - return (in_array($key, array('/home/l3l0', '/home/l3l0/dir', '/home/somedir', '/home/somedir/dir', '/home/l3l1'))) ? true : false; -} - -function realpath($link) -{ - return ('symbolicLink' === $link) ? '/home/somedir' : $link; -} - -function is_link($link) -{ - return ('symbolicLink' === $link) ? true : false; -} - -function mkdir($directory, $mode, $recursive) -{ - return (in_array($directory, array('/home/other', '/home/somedir/aaa'))) ? true : false; -} - function apc_fetch($path) { return sprintf('%s content', $path); diff --git a/spec/Gaufrette/Util/PathSpec.php b/spec/Gaufrette/Util/PathSpec.php index e0a7e62f2..6be3c02fa 100644 --- a/spec/Gaufrette/Util/PathSpec.php +++ b/spec/Gaufrette/Util/PathSpec.php @@ -11,6 +11,7 @@ function it_checks_if_path_is_absolute() $this->isAbsolute('/home/path')->shouldBe(true); $this->isAbsolute('home/path')->shouldBe(false); $this->isAbsolute('../home/path')->shouldBe(false); + $this->isAbsolute('protocol://home/path')->shouldBe(true); } function it_normalizes_file_path() @@ -20,5 +21,6 @@ function it_normalizes_file_path() $this->normalize('..\other.txt')->shouldReturn('../other.txt'); $this->normalize('/home/other/../new')->shouldReturn('/home/new'); $this->normalize('/home/other/./new')->shouldReturn('/home/other/new'); + $this->normalize('protocol://home/other.txt')->shouldReturn('protocol://home/other.txt'); } } diff --git a/src/Gaufrette/Util/Path.php b/src/Gaufrette/Util/Path.php index b92f556a2..fbe17d68c 100644 --- a/src/Gaufrette/Util/Path.php +++ b/src/Gaufrette/Util/Path.php @@ -65,7 +65,7 @@ public static function isAbsolute($path) */ public static function getAbsolutePrefix($path) { - preg_match('|^(?P([a-zA-Z]:)?/)|', $path, $matches); + preg_match('|^(?P([a-zA-Z]+:)?//?)|', $path, $matches); if (empty($matches['prefix'])) { return ''; From 9c4dc425f094c63d20f984e6dc3ab77d6068e67f Mon Sep 17 00:00:00 2001 From: Douglas Greenshields Date: Tue, 1 Oct 2013 13:17:25 +0100 Subject: [PATCH 064/426] remove unnecessary iterator_to_array which can cause memory exhaustion --- src/Gaufrette/Adapter/Local.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Gaufrette/Adapter/Local.php b/src/Gaufrette/Adapter/Local.php index d0a005a8c..bbec7051c 100644 --- a/src/Gaufrette/Adapter/Local.php +++ b/src/Gaufrette/Adapter/Local.php @@ -88,16 +88,15 @@ public function keys() $this->ensureDirectoryExists($this->directory, $this->create); try { - $iterator = new \RecursiveIteratorIterator( + $files = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $this->directory, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS ) ); } catch (\Exception $e) { - $iterator = new \EmptyIterator; + $files = new \EmptyIterator; } - $files = iterator_to_array($iterator); $keys = array(); foreach ($files as $file) { From 47677aa8de8a0df63f927bc7daa6b2ff50bcf168 Mon Sep 17 00:00:00 2001 From: Adam Woods Date: Wed, 9 Oct 2013 17:24:11 +1100 Subject: [PATCH 065/426] Adds autodetection of ContentType for AwsS3 adapter. --- src/Gaufrette/Adapter/AwsS3.php | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index e8caab037..fe12390f0 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -116,6 +116,17 @@ public function write($key, $content) $this->ensureBucketExists(); $options = $this->getOptions($key, array('Body' => $content)); + /** + * If the ContentType was not already set in the metadata, then we autodetect + * it to prevent everything being served up as binary/octet-stream. + */ + if (!isset($options['ContentType'])) { + $finfo = new \finfo(FILEINFO_MIME_TYPE); + $mimeType = $finfo->buffer($content); + + $options['ContentType'] = $mimeType; + } + try { $this->service->putObject($options); return strlen($content); From 980aabfa27ac64c42696e56fd7a4c15ed956729b Mon Sep 17 00:00:00 2001 From: Adam Woods Date: Fri, 22 Nov 2013 10:59:52 +1100 Subject: [PATCH 066/426] Makes content-type detection configurable when adapter is instantiated. --- src/Gaufrette/Adapter/AwsS3.php | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index fe12390f0..411188509 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -20,8 +20,9 @@ class AwsS3 implements Adapter, protected $options; protected $bucketExists; protected $metadata = array(); + protected $detectContentType; - public function __construct(S3Client $service, $bucket, array $options = array()) + public function __construct(S3Client $service, $bucket, array $options = array(), $detectContentType = true) { $this->service = $service; $this->bucket = $bucket; @@ -30,8 +31,11 @@ public function __construct(S3Client $service, $bucket, array $options = array() 'create' => false, 'directory' => '', 'acl' => 'private', - ), $options + ), + $options ); + + $this->detectContentType = $detectContentType; } /** @@ -120,7 +124,7 @@ public function write($key, $content) * If the ContentType was not already set in the metadata, then we autodetect * it to prevent everything being served up as binary/octet-stream. */ - if (!isset($options['ContentType'])) { + if (!isset($options['ContentType']) && $this->detectContentType) { $finfo = new \finfo(FILEINFO_MIME_TYPE); $mimeType = $finfo->buffer($content); From bb6a3dc5e7e93644cd116b2e3ac5fa8f477e763b Mon Sep 17 00:00:00 2001 From: Adam Woods Date: Fri, 22 Nov 2013 11:00:46 +1100 Subject: [PATCH 067/426] Adds description to composer.json about how the suggested ext-fileinfo extension is used. --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index c73accc94..d0d09a737 100644 --- a/composer.json +++ b/composer.json @@ -73,7 +73,7 @@ "ext-curl": "*", "ext-mbstring": "*", "ext-mongo": "*", - "ext-fileinfo": "*" + "ext-fileinfo": "This extension is used to automatically detect the content-type of a file in the AwsS3, OpenCloud, and AzureBlogStorage adapters" }, "autoload": { "psr-0": { "Gaufrette": "src/" } From f65852badb8b4fd457aff4dbad28658411bba892 Mon Sep 17 00:00:00 2001 From: Adam Woods Date: Mon, 25 Nov 2013 10:33:50 +1100 Subject: [PATCH 068/426] Defaults content-type detection to false in AwsS3 adapter to prevent breaking existing usage. --- src/Gaufrette/Adapter/AwsS3.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index 411188509..d4b612510 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -22,7 +22,7 @@ class AwsS3 implements Adapter, protected $metadata = array(); protected $detectContentType; - public function __construct(S3Client $service, $bucket, array $options = array(), $detectContentType = true) + public function __construct(S3Client $service, $bucket, array $options = array(), $detectContentType = false) { $this->service = $service; $this->bucket = $bucket; From 52f5ae0557c20af1237565317ca83d0e5b8add69 Mon Sep 17 00:00:00 2001 From: Markus Poerschke Date: Tue, 3 Dec 2013 16:19:02 +0100 Subject: [PATCH 069/426] SizeCalculation from FileSystem Using the FilesSystem for calculating the size instead of reading the content. This can save a lot of memory using the Local adapter that implements the new SizeCalculator interface. --- src/Gaufrette/Adapter/Local.php | 8 +++++++- src/Gaufrette/Adapter/SizeCalculator.php | 20 ++++++++++++++++++++ src/Gaufrette/File.php | 3 ++- src/Gaufrette/Filesystem.php | 18 ++++++++++++++++++ src/Gaufrette/Util/Size.php | 12 ++++++++++++ 5 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 src/Gaufrette/Adapter/SizeCalculator.php diff --git a/src/Gaufrette/Adapter/Local.php b/src/Gaufrette/Adapter/Local.php index bbec7051c..f37ba6574 100644 --- a/src/Gaufrette/Adapter/Local.php +++ b/src/Gaufrette/Adapter/Local.php @@ -16,7 +16,8 @@ */ class Local implements Adapter, StreamFactory, - ChecksumCalculator + ChecksumCalculator, + SizeCalculator { protected $directory; private $create; @@ -152,6 +153,11 @@ public function checksum($key) return Util\Checksum::fromFile($this->computePath($key)); } + public function size($key) + { + return Util\Size::fromFile($this->computePath($key)); + } + /** * Computes the key from the specified path * diff --git a/src/Gaufrette/Adapter/SizeCalculator.php b/src/Gaufrette/Adapter/SizeCalculator.php new file mode 100644 index 000000000..02e7d29af --- /dev/null +++ b/src/Gaufrette/Adapter/SizeCalculator.php @@ -0,0 +1,20 @@ + + */ +interface SizeCalculator +{ + /** + * Returns the size of the specified key + * + * @param string $key + * + * @return string + */ + public function size($key); +} diff --git a/src/Gaufrette/File.php b/src/Gaufrette/File.php index 95c8752ce..0821dc37f 100644 --- a/src/Gaufrette/File.php +++ b/src/Gaufrette/File.php @@ -3,6 +3,7 @@ namespace Gaufrette; use Gaufrette\Adapter\MetadataSupporter; +use Gaufrette\Adapter\SizeCalculator; use Gaufrette\Exception\FileNotFound; /** @@ -103,7 +104,7 @@ public function getSize() } try { - return $this->size = Util\Size::fromContent($this->getContent()); + return $this->size = $this->filesystem->size($this->getKey()); } catch (FileNotFound $exception) { } diff --git a/src/Gaufrette/Filesystem.php b/src/Gaufrette/Filesystem.php index cdedf94f6..7d6e92695 100644 --- a/src/Gaufrette/Filesystem.php +++ b/src/Gaufrette/Filesystem.php @@ -234,6 +234,24 @@ public function checksum($key) return Util\Checksum::fromContent($this->read($key)); } + /** + * Returns the size of the specified file's content + * + * @param string $key + * + * @return integer File size in Bytes + */ + public function size($key) + { + $this->assertHasFile($key); + + if ($this->adapter instanceof Adapter\SizeCalculator) { + return $this->adapter->size($key); + } + + return Util\Checksum::fromContent($this->read($key)); + } + /** * {@inheritDoc} */ diff --git a/src/Gaufrette/Util/Size.php b/src/Gaufrette/Util/Size.php index a6cb81376..91537cdf1 100644 --- a/src/Gaufrette/Util/Size.php +++ b/src/Gaufrette/Util/Size.php @@ -24,4 +24,16 @@ public static function fromContent($content) // accidentally mistake some bytes as a UTF BOM. return mb_strlen($content, '8bit'); } + + /** + * Returns the size in bytes from the given file + * + * @param string $filename + * + * @return string + */ + public static function fromFile($filename) + { + return filesize($filename); + } } From c60cf2aa908d31eb203bfee0483dc0e8b2c82826 Mon Sep 17 00:00:00 2001 From: Markus Poerschke Date: Tue, 3 Dec 2013 17:36:36 +0100 Subject: [PATCH 070/426] Fixed spec tests for new size calulation --- spec/Gaufrette/FileSpec.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/spec/Gaufrette/FileSpec.php b/spec/Gaufrette/FileSpec.php index 86290b956..89cb79865 100644 --- a/spec/Gaufrette/FileSpec.php +++ b/spec/Gaufrette/FileSpec.php @@ -125,9 +125,9 @@ function it_sets_size_for_new_file($filesystem) /** * @param \Gaufrette\Filesystem $filesystem */ - function it_calculates_size_from_content($filesystem) + function it_calculates_size_from_filesystem($filesystem) { - $filesystem->read('filename')->shouldBeCalled()->willReturn('some content'); + $filesystem->size('filename')->shouldBeCalled()->willReturn(12); $this->getSize()->shouldReturn(12); } @@ -148,7 +148,7 @@ function it_allows_to_set_size($filesystem) */ function it_gets_zero_size_when_file_not_found($filesystem) { - $filesystem->read('filename')->willThrow(new \Gaufrette\Exception\FileNotFound('filename')); + $filesystem->size('filename')->willThrow(new \Gaufrette\Exception\FileNotFound('filename')); $this->getSize()->shouldReturn(0); } From cb3684a7f3348eb38865b0a06b85ad2adfc1d0ff Mon Sep 17 00:00:00 2001 From: Anthon Pang Date: Tue, 3 Dec 2013 12:06:22 -0500 Subject: [PATCH 071/426] Don't override environment's umask. --- src/Gaufrette/Adapter/Local.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Gaufrette/Adapter/Local.php b/src/Gaufrette/Adapter/Local.php index bbec7051c..e01a5836d 100644 --- a/src/Gaufrette/Adapter/Local.php +++ b/src/Gaufrette/Adapter/Local.php @@ -233,9 +233,7 @@ protected function ensureDirectoryExists($directory, $create = false) */ protected function createDirectory($directory) { - $umask = umask(0); $created = mkdir($directory, 0777, true); - umask($umask); if (!$created) { if (!is_dir($directory)) { From f81fffc8cbff30d172fd1e0ff95258fd73dbd93d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Czy=C5=BCewski?= Date: Sun, 8 Dec 2013 17:55:28 +0100 Subject: [PATCH 072/426] Added tests for fileRegister --- .../Functional/Adapter/FunctionalTestCase.php | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php b/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php index 608606aa9..7f502e3b0 100644 --- a/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php +++ b/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php @@ -197,4 +197,31 @@ public function shouldWorkWithHiddenFiles() $this->filesystem->delete('.foo'); $this->assertFalse($this->filesystem->has('.foo')); } + + /** + * @test + * @group functional + */ + public function shouldKeepFileObjectInRegister() + { + $FileObjectA = $this->filesystem->createFile('somefile'); + $FileObjectB = $this->filesystem->createFile('somefile'); + + $this->assertTrue($FileObjectA === $FileObjectB); + } + + /** + * @test + * @group functional + */ + public function shouldWrtieToSameFile() + { + $FileObjectA = $this->filesystem->createFile('somefile'); + $FileObjectA->setContent('ABC'); + + $FileObjectB = $this->filesystem->createFile('somefile'); + $FileObjectB->setContent('DEF'); + + $this->assertEquals('DEF', $FileObjectB->getContent()); + } } From 55ce854db7232c7867281425b1c443879b316e3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Czy=C5=BCewski?= Date: Mon, 9 Dec 2013 17:06:23 +0100 Subject: [PATCH 073/426] AzureBlobStorage test for "shouldFetchKeys" faild when "somefile" left --- tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php b/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php index 7f502e3b0..ba9b68c6a 100644 --- a/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php +++ b/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php @@ -223,5 +223,7 @@ public function shouldWrtieToSameFile() $FileObjectB->setContent('DEF'); $this->assertEquals('DEF', $FileObjectB->getContent()); + + $this->filesystem->delete('somefile'); } } From e5a3c5a71ce1c8ab68a47f7d5e8031a0b7e5a469 Mon Sep 17 00:00:00 2001 From: Massimiliano Arione Date: Tue, 10 Dec 2013 14:45:25 +0100 Subject: [PATCH 074/426] Add an optional mode for mkdir This is a back-compatible change, to be able to choose a mode for mkdir. --- src/Gaufrette/Adapter/Local.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Gaufrette/Adapter/Local.php b/src/Gaufrette/Adapter/Local.php index ae2846ae9..e171576a3 100644 --- a/src/Gaufrette/Adapter/Local.php +++ b/src/Gaufrette/Adapter/Local.php @@ -21,6 +21,7 @@ class Local implements Adapter, { protected $directory; private $create; + private $mode; /** * Constructor @@ -28,11 +29,12 @@ class Local implements Adapter, * @param string $directory Directory where the filesystem is located * @param boolean $create Whether to create the directory if it does not * exist (default FALSE) + * @param integer $mode Mode for mkdir * * @throws RuntimeException if the specified directory does not exist and * could not be created */ - public function __construct($directory, $create = false) + public function __construct($directory, $create = false, $mode = 0777) { $this->directory = Util\Path::normalize($directory); @@ -41,6 +43,7 @@ public function __construct($directory, $create = false) } $this->create = $create; + $this->mode = $mode; } /** @@ -239,7 +242,7 @@ protected function ensureDirectoryExists($directory, $create = false) */ protected function createDirectory($directory) { - $created = mkdir($directory, 0777, true); + $created = mkdir($directory, $this->mode, true); if (!$created) { if (!is_dir($directory)) { From 309a0f3e74dd33460ba693073a63d0e408ca394f Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Wed, 11 Dec 2013 09:42:37 +0100 Subject: [PATCH 075/426] fixed leak of resource in sftp->ensureDirectoryExists() cleanup after a successfull call to opendir(), so the OS will not get out of resource handles on long running processes. --- src/Gaufrette/Adapter/Sftp.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/Sftp.php b/src/Gaufrette/Adapter/Sftp.php index d2f8cb0f9..782928374 100644 --- a/src/Gaufrette/Adapter/Sftp.php +++ b/src/Gaufrette/Adapter/Sftp.php @@ -204,9 +204,15 @@ protected function ensureDirectoryExists($directory, $create = false) { $url = $this->sftp->getUrl($directory); - if (false === @opendir($url) && (!$create || !$this->createDirectory($directory))) { + $resource = @opendir($url); + if (false === $resource && (!$create || !$this->createDirectory($directory))) { throw new \RuntimeException(sprintf('The directory \'%s\' does not exist and could not be created.', $directory)); } + + // make sure we don't leak the resource + if (is_resource($resource)) { + closedir($resource); + } } /** From 2f11a064f9c9a85ba6cbbb37c138f686ae380dd8 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Sun, 15 Dec 2013 20:10:11 +0100 Subject: [PATCH 076/426] Close underlying connection on destruction --- src/Gaufrette/Adapter/MogileFS.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Gaufrette/Adapter/MogileFS.php b/src/Gaufrette/Adapter/MogileFS.php index 46506b368..9ca2994d1 100644 --- a/src/Gaufrette/Adapter/MogileFS.php +++ b/src/Gaufrette/Adapter/MogileFS.php @@ -354,4 +354,11 @@ private function putFile($path, $data) return true; } + + /** + * Closes the underlying connection + */ + public function __destruct() { + $this->close(); + } } From dbc9e5120db879f4b93868896ec9a0698cc90c17 Mon Sep 17 00:00:00 2001 From: Adam Woods Date: Mon, 16 Dec 2013 16:25:38 +1100 Subject: [PATCH 077/426] Cleans up use statements and docblocks for File class. --- src/Gaufrette/File.php | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Gaufrette/File.php b/src/Gaufrette/File.php index 0821dc37f..2e9601165 100644 --- a/src/Gaufrette/File.php +++ b/src/Gaufrette/File.php @@ -3,7 +3,6 @@ namespace Gaufrette; use Gaufrette\Adapter\MetadataSupporter; -use Gaufrette\Adapter\SizeCalculator; use Gaufrette\Exception\FileNotFound; /** @@ -18,7 +17,7 @@ class File /** * Content variable is lazy. It will not be read from filesystem until it's requested first time - * @var content + * @var mixed content */ protected $content = null; @@ -71,7 +70,7 @@ public function getKey() /** * Returns the content * - * @throws Gaufrette\Exception\FileNotFound + * @throws FileNotFound * * @param array $metadata optional metadata which should be send when read * @return string @@ -122,7 +121,7 @@ public function getMtime() } /** - * @param int size of the file + * @param int $size size of the file */ public function setSize($size) { @@ -147,7 +146,7 @@ public function setContent($content, $metadata = array()) } /** - * @param string name of the file + * @param string $name name of the file */ public function setName($name) { @@ -167,7 +166,7 @@ public function exists() /** * Deletes the file from the filesystem * - * @throws Gaufrette\Exception\FileNotFound + * @throws FileNotFound * @throws \RuntimeException when cannot delete file * @param array $metadata optional metadata which should be send when write * @return boolean TRUE on success @@ -182,7 +181,7 @@ public function delete($metadata = array()) /** * Creates a new file stream instance of the file * - * @return FileStream + * @return Stream */ public function createStream() { From 704419e6a69106a1f505c2fa446e8f1676035d78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Czy=C5=BCewski?= Date: Tue, 17 Dec 2013 20:34:24 +0100 Subject: [PATCH 078/426] Extracted, private isFileInRegister function --- src/Gaufrette/Filesystem.php | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/Gaufrette/Filesystem.php b/src/Gaufrette/Filesystem.php index ce640f324..96e40e1b0 100644 --- a/src/Gaufrette/Filesystem.php +++ b/src/Gaufrette/Filesystem.php @@ -76,7 +76,7 @@ public function rename($sourceKey, $targetKey) throw new \RuntimeException(sprintf('Could not rename the "%s" key to "%s".', $sourceKey, $targetKey)); } - if(array_key_exists($sourceKey, $this->fileRegister)) { + if($this->isFileInRegister($sourceKey)) { $this->fileRegister[$targetKey] = $this->fileRegister[$sourceKey]; unset($this->fileRegister[$sourceKey]); } @@ -163,7 +163,7 @@ public function delete($key) $this->assertHasFile($key); if ($this->adapter->delete($key)) { - if(array_key_exists($key, $this->fileRegister)) { + if($this->isFileInRegister($key)) { unset($this->fileRegister[$key]); } return true; @@ -266,7 +266,7 @@ public function createStream($key) */ public function createFile($key) { - if(false === array_key_exists($key, $this->fileRegister)) { + if(false === $this->isFileInRegister($key)) { if ($this->adapter instanceof Adapter\FileFactory) { $this->fileRegister[$key] = $this->adapter->createFile($key, $this); } else { @@ -293,4 +293,16 @@ private function assertHasFile($key) throw new Exception\FileNotFound($key); } } + + /** + * Checks if matching File object by given key exists in the fileRegister + * + * @param string $key + * + * @return bool + */ + private function isFileInRegister($key) + { + return array_key_exists($key, $this->fileRegister); + } } From 053e39e9f98d4298c47106190e969b21020c034d Mon Sep 17 00:00:00 2001 From: Markus Poerschke Date: Tue, 7 Jan 2014 09:55:08 +0100 Subject: [PATCH 079/426] Fixed file size calculation for Filesystems that do not implement SizeCalculator --- src/Gaufrette/Filesystem.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gaufrette/Filesystem.php b/src/Gaufrette/Filesystem.php index 7d6e92695..8559f2257 100644 --- a/src/Gaufrette/Filesystem.php +++ b/src/Gaufrette/Filesystem.php @@ -249,7 +249,7 @@ public function size($key) return $this->adapter->size($key); } - return Util\Checksum::fromContent($this->read($key)); + return Util\Size::fromContent($this->read($key)); } /** From 1767fd2d9ab9e24ab27138e33be478089c4a56a9 Mon Sep 17 00:00:00 2001 From: Mark Anderson Date: Wed, 8 Jan 2014 21:27:18 +0100 Subject: [PATCH 080/426] Update S3 docs. Using AwsS3 and S3Client with new AWS SDK. --- README.markdown | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/README.markdown b/README.markdown index 4264c9832..e78e0c28b 100644 --- a/README.markdown +++ b/README.markdown @@ -126,6 +126,14 @@ Specifying a custom CA certificate is not required when using the `Gaufrette\Adapter\AmazonS3` adapter because it uses the newest version of the AWS SDK for PHP. +If you use the newer adapter ``\AwsS3`` you will need to use the S3Client factory method, and the plug that into the Adapter. + +```php +$service = S3Client::factory(array('key' => 'your_key_here', 'secret' => 'your_secret' )); +$client = new AwsS3($service,'your-bucket-name'); +``` + + Using OpenCloud --------------- To use the OpenCloud adapter you will need to create a connection using the [OpenCloud SDK](https://github.com/rackspace/php-opencloud). From a01503c7a95a84fe0fa10822876866e01a1bbeed Mon Sep 17 00:00:00 2001 From: Leszek Prabucki Date: Mon, 20 Jan 2014 16:44:30 +0100 Subject: [PATCH 081/426] Fix stream_wrapper_unregister error issue This should fix issue #256 --- src/Gaufrette/StreamWrapper.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Gaufrette/StreamWrapper.php b/src/Gaufrette/StreamWrapper.php index cf508095e..b2956742c 100644 --- a/src/Gaufrette/StreamWrapper.php +++ b/src/Gaufrette/StreamWrapper.php @@ -69,7 +69,9 @@ protected static function createFilesystemMap() */ protected static function streamWrapperUnregister($scheme) { - return @stream_wrapper_unregister($scheme); + if (in_array($scheme, stream_get_wrappers())) { + return stream_wrapper_unregister($scheme); + } } /** From 0104d65f9587c37563fd37dfda1d317b64a14965 Mon Sep 17 00:00:00 2001 From: allanbrault Date: Fri, 24 Jan 2014 17:19:20 +0100 Subject: [PATCH 082/426] Fix readme typo --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index e78e0c28b..a357d603d 100644 --- a/README.markdown +++ b/README.markdown @@ -226,7 +226,7 @@ $filesystem = new Gaufrette\Filesystem($adapter); Using FTP adapters --------------- -Some FTP servers need valid configuration so Gaufrette can working with them as expected. +Some FTP servers need valid configuration so Gaufrette can work with them as expected. ### Pure Ftpd From a97a287f14df241f6ab1c50d2308f1bf33300960 Mon Sep 17 00:00:00 2001 From: Teoh Han Hui Date: Tue, 28 Jan 2014 04:35:51 +0000 Subject: [PATCH 083/426] AwsS3 adapter: fixed rename --- src/Gaufrette/Adapter/AwsS3.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index d4b612510..5605de7f4 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -102,7 +102,12 @@ public function read($key) public function rename($sourceKey, $targetKey) { $this->ensureBucketExists(); - $options = $this->getOptions($targetKey, array('CopySource' => $this->computePath($sourceKey))); + $options = $this->getOptions( + $targetKey, + array( + 'CopySource' => $this->bucket.'/'.$this->computePath($sourceKey), + ) + ); try { $this->service->copyObject($options); From 21883154ff504fb4737195be1c2248e28d48b777 Mon Sep 17 00:00:00 2001 From: Julius Beckmann Date: Tue, 28 Jan 2014 14:33:53 +0100 Subject: [PATCH 084/426] Creating new directories did not work. When using write() or rename() no new directories have been created, if __construct(..., ..., false) was given. Seemed to be typo, because all calls to ensureDirectoryExists give a value for $create. Because of this, the default value was removed. --- src/Gaufrette/Adapter/PhpseclibSftp.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Gaufrette/Adapter/PhpseclibSftp.php b/src/Gaufrette/Adapter/PhpseclibSftp.php index ddeef2548..95c67b080 100644 --- a/src/Gaufrette/Adapter/PhpseclibSftp.php +++ b/src/Gaufrette/Adapter/PhpseclibSftp.php @@ -185,12 +185,12 @@ protected function initialize() $this->initialized = true; } - protected function ensureDirectoryExists($directory, $create = false) + protected function ensureDirectoryExists($directory, $create) { $pwd = $this->sftp->pwd(); if ($this->sftp->chdir($directory)) { $this->sftp->chdir($pwd); - } elseif ($this->create) { + } elseif ($create) { if (!$this->sftp->mkdir($directory, 0777, true)) { throw new \RuntimeException(sprintf('The directory \'%s\' does not exist and could not be created (%s).', $this->directory, $this->sftp->getLastSFTPError())); } From e9be6445552b7bfff41c3f75547f04774d7b0761 Mon Sep 17 00:00:00 2001 From: Enrico Stahn Date: Wed, 19 Feb 2014 02:57:03 +1100 Subject: [PATCH 085/426] Throw expected exception even-though a custom error handler is specified --- spec/Gaufrette/Stream/LocalSpec.php | 27 +++++++++++++++++++++++++++ src/Gaufrette/Stream/Local.php | 7 ++++++- 2 files changed, 33 insertions(+), 1 deletion(-) create mode 100644 spec/Gaufrette/Stream/LocalSpec.php diff --git a/spec/Gaufrette/Stream/LocalSpec.php b/spec/Gaufrette/Stream/LocalSpec.php new file mode 100644 index 000000000..af983d5d3 --- /dev/null +++ b/spec/Gaufrette/Stream/LocalSpec.php @@ -0,0 +1,27 @@ +beConstructedWith(vfsStream::url('other')); + $this->shouldThrow('\RuntimeException')->duringOpen(new StreamMode('r')); + } + + function it_throws_runtime_exception_when_file_doesnt_exists_and_custom_error_handler_specified() + { + $custom_error_handler = function ($errno, $errstr, $errfile, $errline ) { + throw new \ErrorException($errstr, 0, $errno, $errfile, $errline); + }; + set_error_handler($custom_error_handler); + + $this->beConstructedWith(vfsStream::url('other')); + $this->shouldThrow('\RuntimeException')->duringOpen(new StreamMode('r')); + } +} diff --git a/src/Gaufrette/Stream/Local.php b/src/Gaufrette/Stream/Local.php index c9bfe5af6..841494606 100644 --- a/src/Gaufrette/Stream/Local.php +++ b/src/Gaufrette/Stream/Local.php @@ -4,6 +4,7 @@ use Gaufrette\Stream; use Gaufrette\StreamMode; +use PhpSpec\Exception\Exception; /** * Local stream @@ -31,7 +32,11 @@ public function __construct($path) */ public function open(StreamMode $mode) { - $fileHandle = @fopen($this->path, $mode->getMode()); + try { + $fileHandle = @fopen($this->path, $mode->getMode()); + } catch (\Exception $e) { + $fileHandle = false; + } if (false === $fileHandle) { throw new \RuntimeException(sprintf('File "%s" cannot be opened', $this->path)); From 30c6ecc58ef3eac068c2a1167a22cbcf5bfdffc6 Mon Sep 17 00:00:00 2001 From: Enrico Stahn Date: Wed, 19 Feb 2014 10:09:49 +1100 Subject: [PATCH 086/426] Remove use statement for "PhpSpec\Exception\Exception" --- src/Gaufrette/Stream/Local.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Gaufrette/Stream/Local.php b/src/Gaufrette/Stream/Local.php index 841494606..45177fba8 100644 --- a/src/Gaufrette/Stream/Local.php +++ b/src/Gaufrette/Stream/Local.php @@ -4,7 +4,6 @@ use Gaufrette\Stream; use Gaufrette\StreamMode; -use PhpSpec\Exception\Exception; /** * Local stream From d01ea25e397ccde4990fe103ed5252822ea505a9 Mon Sep 17 00:00:00 2001 From: Enrico Stahn Date: Wed, 19 Feb 2014 11:28:20 +1100 Subject: [PATCH 087/426] Restore error handler after test steps are finished --- spec/Gaufrette/Stream/LocalSpec.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/spec/Gaufrette/Stream/LocalSpec.php b/spec/Gaufrette/Stream/LocalSpec.php index af983d5d3..872045193 100644 --- a/spec/Gaufrette/Stream/LocalSpec.php +++ b/spec/Gaufrette/Stream/LocalSpec.php @@ -16,12 +16,14 @@ function it_throws_runtime_exception_when_file_doesnt_exists() function it_throws_runtime_exception_when_file_doesnt_exists_and_custom_error_handler_specified() { - $custom_error_handler = function ($errno, $errstr, $errfile, $errline ) { + $custom_error_handler = function ($errno, $errstr, $errfile, $errline) { throw new \ErrorException($errstr, 0, $errno, $errfile, $errline); }; set_error_handler($custom_error_handler); $this->beConstructedWith(vfsStream::url('other')); $this->shouldThrow('\RuntimeException')->duringOpen(new StreamMode('r')); + + restore_error_handler(); } } From 36ec18e0c9b4d7f047a951ebcd48eeaa71ea98c4 Mon Sep 17 00:00:00 2001 From: Richtermeister Date: Mon, 24 Feb 2014 12:27:32 -0800 Subject: [PATCH 088/426] Upgrading OpenCloud Adapter to 1.9.* --- composer.json | 2 +- composer.lock | 380 ++++++++++++++--------- spec/Gaufrette/Adapter/OpenCloudSpec.php | 185 +++++------ src/Gaufrette/Adapter/OpenCloud.php | 156 +++++----- 4 files changed, 412 insertions(+), 311 deletions(-) diff --git a/composer.json b/composer.json index d0d09a737..c4ffca675 100644 --- a/composer.json +++ b/composer.json @@ -47,7 +47,7 @@ "require-dev": { "aws/aws-sdk-php": "~2", "amazonwebservices/aws-sdk-for-php": "1.5.*", - "rackspace/php-opencloud" : "1.6.*", + "rackspace/php-opencloud" : "1.9.*", "phpspec/phpspec": "2.0.*", "phpseclib/phpseclib": "dev-master", "rackspace/php-cloudfiles": "*", diff --git a/composer.lock b/composer.lock index 33e840ac1..ea1314d33 100644 --- a/composer.lock +++ b/composer.lock @@ -3,7 +3,7 @@ "This file locks the dependencies of your project to a known state", "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" ], - "hash": "18ce61df78a3b95768776edf14952f62", + "hash": "25a293184541f0f60c2bce5160b4eefb", "packages": [ ], @@ -64,16 +64,16 @@ "source": { "type": "git", "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "b494eb871d0aa3ba3b0a47a895fb18c6226edb88" + "reference": "74896cc3dab67035052b5c5e4d7e80c7a58889fe" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/b494eb871d0aa3ba3b0a47a895fb18c6226edb88", - "reference": "b494eb871d0aa3ba3b0a47a895fb18c6226edb88", + "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/74896cc3dab67035052b5c5e4d7e80c7a58889fe", + "reference": "74896cc3dab67035052b5c5e4d7e80c7a58889fe", "shasum": "" }, "require": { - "guzzle/guzzle": "~3.7.0", + "guzzle/guzzle": ">=3.7.0,<3.9.0", "php": ">=5.3.3" }, "require-dev": { @@ -94,7 +94,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.4.x-dev" + "dev-master": "2.5.x-dev" } }, "autoload": { @@ -112,30 +112,32 @@ "homepage": "http://aws.amazon.com" } ], - "description": "AWS SDK for PHP", - "homepage": "http://aws.amazon.com/sdkforphp2", + "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", + "homepage": "http://aws.amazon.com/sdkforphp", "keywords": [ "amazon", "aws", + "cloud", "dynamodb", "ec2", + "glacier", "s3", "sdk" ], - "time": "2013-11-14 17:24:28" + "time": "2014-02-19 17:59:07" }, { "name": "doctrine/annotations", - "version": "v1.1.2", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/doctrine/annotations.git", - "reference": "40db0c96985aab2822edbc4848b3bd2429e02670" + "reference": "088ea3b5dfbe52adf7b12623caa897e9ee1d795e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/40db0c96985aab2822edbc4848b3bd2429e02670", - "reference": "40db0c96985aab2822edbc4848b3bd2429e02670", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/088ea3b5dfbe52adf7b12623caa897e9ee1d795e", + "reference": "088ea3b5dfbe52adf7b12623caa897e9ee1d795e", "shasum": "" }, "require": { @@ -148,7 +150,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.2.x-dev" } }, "autoload": { @@ -194,20 +196,20 @@ "docblock", "parser" ], - "time": "2013-06-16 21:33:03" + "time": "2014-02-23 21:16:16" }, { "name": "doctrine/cache", - "version": "v1.3.0", + "version": "dev-master", "source": { "type": "git", "url": "https://github.com/doctrine/cache.git", - "reference": "e16d7adf45664a50fa86f515b6d5e7f670130449" + "reference": "36c4eee5051629524389da376ba270f15765e49f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/e16d7adf45664a50fa86f515b6d5e7f670130449", - "reference": "e16d7adf45664a50fa86f515b6d5e7f670130449", + "url": "https://api.github.com/repos/doctrine/cache/zipball/36c4eee5051629524389da376ba270f15765e49f", + "reference": "36c4eee5051629524389da376ba270f15765e49f", "shasum": "" }, "require": { @@ -223,7 +225,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.4.x-dev" } }, "autoload": { @@ -237,7 +239,7 @@ ], "authors": [ { - "name": "Jonathan H. Wage", + "name": "Jonathan Wage", "email": "jonwage@gmail.com", "homepage": "http://www.jwage.com/", "role": "Creator" @@ -268,7 +270,7 @@ "cache", "caching" ], - "time": "2013-10-25 19:04:14" + "time": "2013-12-18 17:21:03" }, { "name": "doctrine/collections", @@ -276,12 +278,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/collections.git", - "reference": "bcb53776a096a0c64579cc8d8ec0db62f1109fbc" + "reference": "b99c5c46c87126201899afe88ec490a25eedd6a2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/bcb53776a096a0c64579cc8d8ec0db62f1109fbc", - "reference": "bcb53776a096a0c64579cc8d8ec0db62f1109fbc", + "url": "https://api.github.com/repos/doctrine/collections/zipball/b99c5c46c87126201899afe88ec490a25eedd6a2", + "reference": "b99c5c46c87126201899afe88ec490a25eedd6a2", "shasum": "" }, "require": { @@ -304,7 +306,7 @@ ], "authors": [ { - "name": "Jonathan H. Wage", + "name": "Jonathan Wage", "email": "jonwage@gmail.com", "homepage": "http://www.jwage.com/", "role": "Creator" @@ -336,7 +338,7 @@ "collections", "iterator" ], - "time": "2013-08-29 16:56:45" + "time": "2014-02-03 23:07:43" }, { "name": "doctrine/common", @@ -344,12 +346,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/common.git", - "reference": "c94d6ff79e25418b1225e187c782bf4742f23a8b" + "reference": "9a7e20e779360f3b8a02c27a89d47d5a6fdce8d1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/common/zipball/c94d6ff79e25418b1225e187c782bf4742f23a8b", - "reference": "c94d6ff79e25418b1225e187c782bf4742f23a8b", + "url": "https://api.github.com/repos/doctrine/common/zipball/9a7e20e779360f3b8a02c27a89d47d5a6fdce8d1", + "reference": "9a7e20e779360f3b8a02c27a89d47d5a6fdce8d1", "shasum": "" }, "require": { @@ -411,7 +413,7 @@ "persistence", "spl" ], - "time": "2013-09-07 10:20:35" + "time": "2014-02-03 23:31:47" }, { "name": "doctrine/dbal", @@ -419,12 +421,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/dbal.git", - "reference": "5fdedc4f8f304e8035580807bd36d59e97a87265" + "reference": "646edacd87d5b9d201bf19aaf55ee0e4d5e470c2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/5fdedc4f8f304e8035580807bd36d59e97a87265", - "reference": "5fdedc4f8f304e8035580807bd36d59e97a87265", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/646edacd87d5b9d201bf19aaf55ee0e4d5e470c2", + "reference": "646edacd87d5b9d201bf19aaf55ee0e4d5e470c2", "shasum": "" }, "require": { @@ -482,7 +484,7 @@ "persistence", "queryobject" ], - "time": "2013-11-15 14:43:05" + "time": "2014-02-23 12:02:55" }, { "name": "doctrine/inflector", @@ -490,12 +492,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/inflector.git", - "reference": "8b4b3ccec7aafc596e2fc1e593c9f2e78f939c8c" + "reference": "a81c334f2764b09e2f13a55cfd8fe3233946f728" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/8b4b3ccec7aafc596e2fc1e593c9f2e78f939c8c", - "reference": "8b4b3ccec7aafc596e2fc1e593c9f2e78f939c8c", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/a81c334f2764b09e2f13a55cfd8fe3233946f728", + "reference": "a81c334f2764b09e2f13a55cfd8fe3233946f728", "shasum": "" }, "require": { @@ -551,7 +553,7 @@ "singularize", "string" ], - "time": "2013-04-10 16:14:30" + "time": "2013-12-21 19:19:50" }, { "name": "doctrine/lexer", @@ -559,12 +561,12 @@ "source": { "type": "git", "url": "https://github.com/doctrine/lexer.git", - "reference": "bc0e1f0cc285127a38c6c8ea88bc5dba2fd53e94" + "reference": "f12a5f74e5f4a9e3f558f3288504e121edfad891" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/bc0e1f0cc285127a38c6c8ea88bc5dba2fd53e94", - "reference": "bc0e1f0cc285127a38c6c8ea88bc5dba2fd53e94", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/f12a5f74e5f4a9e3f558f3288504e121edfad891", + "reference": "f12a5f74e5f4a9e3f558f3288504e121edfad891", "shasum": "" }, "require": { @@ -608,7 +610,7 @@ "lexer", "parser" ], - "time": "2013-03-07 12:15:25" + "time": "2013-12-20 21:39:00" }, { "name": "dropbox-php/dropbox-php", @@ -676,12 +678,12 @@ "source": { "type": "git", "url": "https://github.com/guzzle/guzzle.git", - "reference": "d2e8ac50742672ca0947e3f5459a4163eb5f9cbc" + "reference": "8d39ffa70ac1aecd297e93d4185092f51877edb4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/d2e8ac50742672ca0947e3f5459a4163eb5f9cbc", - "reference": "d2e8ac50742672ca0947e3f5459a4163eb5f9cbc", + "url": "https://api.github.com/repos/guzzle/guzzle/zipball/8d39ffa70ac1aecd297e93d4185092f51877edb4", + "reference": "8d39ffa70ac1aecd297e93d4185092f51877edb4", "shasum": "" }, "require": { @@ -719,19 +721,19 @@ "phpunit/phpunit": "3.7.*", "psr/log": "1.0.*", "symfony/class-loader": "*", - "zendframework/zend-cache": "2.0.*", - "zendframework/zend-log": "2.0.*" + "zendframework/zend-cache": "<2.3", + "zendframework/zend-log": "<2.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.7-dev" + "dev-master": "3.8-dev" } }, "autoload": { "psr-0": { - "Guzzle\\Tests": "tests/", - "Guzzle": "src/" + "Guzzle": "src/", + "Guzzle\\Tests": "tests/" } }, "notification-url": "https://packagist.org/downloads/", @@ -760,7 +762,7 @@ "rest", "web service" ], - "time": "2013-11-14 20:23:30" + "time": "2014-02-20 17:01:46" }, { "name": "herzult/php-ssh", @@ -768,12 +770,12 @@ "source": { "type": "git", "url": "https://github.com/Herzult/php-ssh.git", - "reference": "772b7ae229b22112bf0b80e2e18229673c446f81" + "reference": "bea40b828ef2de2f9161255ce42256f26cd8a5b8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Herzult/php-ssh/zipball/772b7ae229b22112bf0b80e2e18229673c446f81", - "reference": "772b7ae229b22112bf0b80e2e18229673c446f81", + "url": "https://api.github.com/repos/Herzult/php-ssh/zipball/bea40b828ef2de2f9161255ce42256f26cd8a5b8", + "reference": "bea40b828ef2de2f9161255ce42256f26cd8a5b8", "shasum": "" }, "require": { @@ -809,7 +811,7 @@ "ssh", "ssh2" ], - "time": "2013-05-24 20:02:36" + "time": "2013-11-21 17:54:47" }, { "name": "microsoft/windowsazure", @@ -817,12 +819,12 @@ "source": { "type": "git", "url": "https://github.com/WindowsAzure/azure-sdk-for-php.git", - "reference": "74a0475b4fe28fbe9f73660bd9ae2248148912f9" + "reference": "61d4e12495358491f62cc5bd8716335b0b313488" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WindowsAzure/azure-sdk-for-php/zipball/74a0475b4fe28fbe9f73660bd9ae2248148912f9", - "reference": "74a0475b4fe28fbe9f73660bd9ae2248148912f9", + "url": "https://api.github.com/repos/WindowsAzure/azure-sdk-for-php/zipball/61d4e12495358491f62cc5bd8716335b0b313488", + "reference": "61d4e12495358491f62cc5bd8716335b0b313488", "shasum": "" }, "require": { @@ -852,7 +854,7 @@ "php", "sdk" ], - "time": "2013-10-17 08:08:49" + "time": "2014-01-23 05:45:10" }, { "name": "mikey179/vfsStream", @@ -938,10 +940,10 @@ }, { "name": "pear-pear.php.net/HTTP_Request2", - "version": "2.1.1", + "version": "2.2.1", "dist": { "type": "file", - "url": "http://pear.php.net/get/HTTP_Request2-2.1.1.tgz", + "url": "http://pear.php.net/get/HTTP_Request2-2.2.1.tgz", "reference": null, "shasum": null }, @@ -951,7 +953,7 @@ "php": ">=5.2.0.0" }, "replace": { - "pear-pear/http_request2": "== 2.1.1.0" + "pear-pear/http_request2": "== 2.2.1.0" }, "type": "pear-library", "autoload": { @@ -1019,18 +1021,18 @@ }, { "name": "pear-pear.php.net/Net_URL2", - "version": "2.0.0", + "version": "2.0.5", "dist": { "type": "file", - "url": "http://pear.php.net/get/Net_URL2-2.0.0.tgz", + "url": "http://pear.php.net/get/Net_URL2-2.0.5.tgz", "reference": null, "shasum": null }, "require": { - "php": ">=5.0.0.0" + "php": ">=5.1.4.0" }, "replace": { - "pear-pear/net_url2": "== 2.0.0.0" + "pear-pear/net_url2": "== 2.0.5.0" }, "type": "pear-library", "autoload": { @@ -1041,7 +1043,7 @@ "include-path": [ "/" ], - "description": "Provides parsing of URLs into their constituent parts (scheme, host, path etc.), URL generation, and resolving of relative URLs." + "description": "Provides parsing of URLs into their constituent parts (scheme, host, path etc.), URL generation, and resolving of\n relative URLs." }, { "name": "pear-pear.php.net/PEAR", @@ -1132,27 +1134,79 @@ ], "description": "Selection of methods that are often needed when working with XML documents. Functionality includes creating of attribute lists from arrays, creation of tags, validation of XML names and more." }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "cfb3ebea556b24df8f8f3745d61478293cb5a166" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/cfb3ebea556b24df8f8f3745d61478293cb5a166", + "reference": "cfb3ebea556b24df8f8f3745d61478293cb5a166", + "shasum": "" + }, + "require": { + "php": ">=5.3.3" + }, + "require-dev": { + "phpunit/phpunit": "3.7.*@stable" + }, + "suggest": { + "dflydev/markdown": "1.0.*", + "erusev/parsedown": "~0.7" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "psr-0": { + "phpDocumentor": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "mike.vanriel@naenius.com" + } + ], + "time": "2013-12-05 08:16:55" + }, { "name": "phpseclib/phpseclib", "version": "dev-master", "source": { "type": "git", "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "f9aa658309d5fb6af6ef032f4072f588f011216b" + "reference": "76c9692908a6c25601df2db373043fcbb335332e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/f9aa658309d5fb6af6ef032f4072f588f011216b", - "reference": "f9aa658309d5fb6af6ef032f4072f588f011216b", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/76c9692908a6c25601df2db373043fcbb335332e", + "reference": "76c9692908a6c25601df2db373043fcbb335332e", "shasum": "" }, "require": { "php": ">=5.0.0" }, + "require-dev": { + "squizlabs/php_codesniffer": "1.*" + }, "suggest": { "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", "ext-mcrypt": "Install the Mcrypt extension in order to speed up a wide variety of cryptographic operations.", - "pear-pear/PHP_Compat": "Install PHP_Compat to get phpseclib working on PHP >= 4.3.3." + "pear-pear/PHP_Compat": "Install PHP_Compat to get phpseclib working on PHP < 4.3.3." }, "type": "library", "extra": { @@ -1182,6 +1236,21 @@ { "name": "Jim Wigginton", "email": "terrafrost@php.net", + "role": "Lead Developer" + }, + { + "name": "Patrick Monnerat", + "email": "pm@datasphere.ch", + "role": "Developer" + }, + { + "name": "Andreas Fischer", + "email": "bantu@phpbb.com", + "role": "Developer" + }, + { + "name": "Hans-Jürgen Petrich", + "email": "petrich@tronic-media.com", "role": "Developer" } ], @@ -1192,6 +1261,7 @@ "aes", "asn.1", "asn1", + "blowfish", "crypto", "cryptography", "encryption", @@ -1201,10 +1271,11 @@ "signature", "signing", "ssh", + "twofish", "x.509", "x509" ], - "time": "2013-11-15 21:07:14" + "time": "2014-02-23 01:03:22" }, { "name": "phpspec/php-diff", @@ -1245,25 +1316,29 @@ "source": { "type": "git", "url": "https://github.com/phpspec/phpspec.git", - "reference": "c631a7c980d5be303ba513c1598a663393a59028" + "reference": "c41c96dcadf2bcaacaa342a84e794254c5bb763a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/phpspec/zipball/c631a7c980d5be303ba513c1598a663393a59028", - "reference": "c631a7c980d5be303ba513c1598a663393a59028", + "url": "https://api.github.com/repos/phpspec/phpspec/zipball/c41c96dcadf2bcaacaa342a84e794254c5bb763a", + "reference": "c41c96dcadf2bcaacaa342a84e794254c5bb763a", "shasum": "" }, "require": { "php": ">=5.3.3", "phpspec/php-diff": "~1.0.0", - "phpspec/prophecy": "~1.0.2", + "phpspec/prophecy": "~1.1", "symfony/console": "~2.1", "symfony/event-dispatcher": "~2.1", "symfony/finder": "~2.1", "symfony/yaml": "~2.1" }, + "require-dev": { + "behat/behat": "~2.5", + "bossa/phpspec2-expect": "dev-master" + }, "suggest": { - "whatthejeff/nyancat-scoreboard": "~1.1 – Enables the Nyan Cat formatter" + "phpspec/nyan-formatters": "~1.0 – Adds Nyan formatters" }, "bin": [ "bin/phpspec" @@ -1305,7 +1380,7 @@ "testing", "tests" ], - "time": "2013-11-13 10:07:05" + "time": "2014-02-23 20:29:41" }, { "name": "phpspec/prophecy", @@ -1313,21 +1388,24 @@ "source": { "type": "git", "url": "https://github.com/phpspec/prophecy.git", - "reference": "8177460b13b087a7e52faffa9492f2d3e91d19fc" + "reference": "6bd3659919ae6472454aea07482200077293e75f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/8177460b13b087a7e52faffa9492f2d3e91d19fc", - "reference": "8177460b13b087a7e52faffa9492f2d3e91d19fc", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/6bd3659919ae6472454aea07482200077293e75f", + "reference": "6bd3659919ae6472454aea07482200077293e75f", "shasum": "" }, + "require": { + "phpdocumentor/reflection-docblock": "~2.0" + }, "require-dev": { "phpspec/phpspec": "2.0.*" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0.x-dev" + "dev-master": "1.1.x-dev" } }, "autoload": { @@ -1360,7 +1438,7 @@ "spy", "stub" ], - "time": "2013-11-06 13:00:43" + "time": "2014-02-18 21:47:37" }, { "name": "phpunit/php-code-coverage", @@ -1368,18 +1446,18 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "10c5b2c8b72a801eaad0031cd6a6a3686909c5a9" + "reference": "e8ffe3fd66479fe502c4a566e9828075f3fffaec" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/10c5b2c8b72a801eaad0031cd6a6a3686909c5a9", - "reference": "10c5b2c8b72a801eaad0031cd6a6a3686909c5a9", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/e8ffe3fd66479fe502c4a566e9828075f3fffaec", + "reference": "e8ffe3fd66479fe502c4a566e9828075f3fffaec", "shasum": "" }, "require": { "php": ">=5.3.3", "phpunit/php-file-iterator": ">=1.3.0@stable", - "phpunit/php-text-template": ">=1.1.1@stable", + "phpunit/php-text-template": ">=1.2.0@stable", "phpunit/php-token-stream": ">=1.1.3@stable" }, "require-dev": { @@ -1421,7 +1499,7 @@ "testing", "xunit" ], - "time": "2013-11-01 09:17:11" + "time": "2014-02-24 18:18:21" }, { "name": "phpunit/php-file-iterator", @@ -1474,12 +1552,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "1eeef106193d2f8c539728e566bb4793071a9e18" + "reference": "206dfefc0ffe9cebf65c413e3d0e809c82fbf00a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/1eeef106193d2f8c539728e566bb4793071a9e18", - "reference": "1eeef106193d2f8c539728e566bb4793071a9e18", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/206dfefc0ffe9cebf65c413e3d0e809c82fbf00a", + "reference": "206dfefc0ffe9cebf65c413e3d0e809c82fbf00a", "shasum": "" }, "require": { @@ -1510,7 +1588,7 @@ "keywords": [ "template" ], - "time": "2013-01-07 10:56:17" + "time": "2014-01-30 17:20:04" }, { "name": "phpunit/php-timer", @@ -1612,12 +1690,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "236f65cc97d6beaa8fcb8a27b19bd278f3912677" + "reference": "5728eadfb7859241d7aafd26592cee0ad1876d2d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/236f65cc97d6beaa8fcb8a27b19bd278f3912677", - "reference": "236f65cc97d6beaa8fcb8a27b19bd278f3912677", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/5728eadfb7859241d7aafd26592cee0ad1876d2d", + "reference": "5728eadfb7859241d7aafd26592cee0ad1876d2d", "shasum": "" }, "require": { @@ -1678,7 +1756,7 @@ "testing", "xunit" ], - "time": "2013-11-06 01:58:51" + "time": "2014-02-15 22:46:11" }, { "name": "phpunit/phpunit-mock-objects", @@ -1686,12 +1764,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "3e40f3b3f18c044a24688fe406440d7fd537744a" + "reference": "c39c4511c3b007539eb170c32cbc2af49a07351a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/3e40f3b3f18c044a24688fe406440d7fd537744a", - "reference": "3e40f3b3f18c044a24688fe406440d7fd537744a", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/c39c4511c3b007539eb170c32cbc2af49a07351a", + "reference": "c39c4511c3b007539eb170c32cbc2af49a07351a", "shasum": "" }, "require": { @@ -1699,7 +1777,6 @@ "phpunit/php-text-template": ">=1.1.1@stable" }, "require-dev": { - "pear-pear/pear": "1.9.4", "phpunit/phpunit": "3.7.*@dev" }, "suggest": { @@ -1736,7 +1813,7 @@ "mock", "xunit" ], - "time": "2013-07-23 04:42:59" + "time": "2014-02-16 12:43:56" }, { "name": "rackspace/php-cloudfiles", @@ -1758,31 +1835,39 @@ "cloudfiles.php" ] }, - "time": "2013-02-20 21:04:46" + "time": "2014-01-07 21:27:14" }, { "name": "rackspace/php-opencloud", - "version": "V1.6.0", + "version": "v1.9.2", "source": { "type": "git", "url": "https://github.com/rackspace/php-opencloud.git", - "reference": "7be280fde422651d0966c70b07f6477b37dd4270" + "reference": "6551de7aebcebb369d025662f99ab27c9b4527ac" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/rackspace/php-opencloud/zipball/7be280fde422651d0966c70b07f6477b37dd4270", - "reference": "7be280fde422651d0966c70b07f6477b37dd4270", + "url": "https://api.github.com/repos/rackspace/php-opencloud/zipball/6551de7aebcebb369d025662f99ab27c9b4527ac", + "reference": "6551de7aebcebb369d025662f99ab27c9b4527ac", "shasum": "" }, "require": { - "ext-curl": "*", - "php": ">=5.3.0" + "guzzle/http": "3.8.*@dev", + "php": ">=5.3.3" + }, + "require-dev": { + "guzzle/guzzle": "dev-master", + "psr/log": "1.0.*", + "satooshi/php-coveralls": "0.6.*@dev" }, "type": "library", "autoload": { - "classmap": [ - "lib/" - ] + "psr-0": { + "OpenCloud": [ + "lib/", + "tests/" + ] + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1799,12 +1884,15 @@ "homepage": "https://github.com/jamiehannaford" } ], - "description": "Rackspace open cloud php library", + "description": "PHP SDK for Rackspace/OpenStack APIs", "keywords": [ - "cloudfiles", - "opencloud" + "Openstack", + "nova", + "opencloud", + "rackspace", + "swift" ], - "time": "2013-09-03 15:49:02" + "time": "2014-02-06 20:53:21" }, { "name": "symfony/console", @@ -1813,12 +1901,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/Console.git", - "reference": "846e8fa8fdb5fab85cdb053b26687dd073c33051" + "reference": "ceb261293b65e9df55b98a9665ef29d56de0c48f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Console/zipball/846e8fa8fdb5fab85cdb053b26687dd073c33051", - "reference": "846e8fa8fdb5fab85cdb053b26687dd073c33051", + "url": "https://api.github.com/repos/symfony/Console/zipball/ceb261293b65e9df55b98a9665ef29d56de0c48f", + "reference": "ceb261293b65e9df55b98a9665ef29d56de0c48f", "shasum": "" }, "require": { @@ -1833,7 +1921,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.4-dev" + "dev-master": "2.5-dev" } }, "autoload": { @@ -1848,7 +1936,9 @@ "authors": [ { "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" }, { "name": "Symfony Community", @@ -1857,7 +1947,7 @@ ], "description": "Symfony Console Component", "homepage": "http://symfony.com", - "time": "2013-11-13 21:30:16" + "time": "2014-02-11 13:52:21" }, { "name": "symfony/event-dispatcher", @@ -1866,19 +1956,21 @@ "source": { "type": "git", "url": "https://github.com/symfony/EventDispatcher.git", - "reference": "d51d78b34c1d9dcc384ba48155105fe99284dd67" + "reference": "8b345992d9e850798f46a6c68804ac9e4965e91a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/d51d78b34c1d9dcc384ba48155105fe99284dd67", - "reference": "d51d78b34c1d9dcc384ba48155105fe99284dd67", + "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/8b345992d9e850798f46a6c68804ac9e4965e91a", + "reference": "8b345992d9e850798f46a6c68804ac9e4965e91a", "shasum": "" }, "require": { "php": ">=5.3.3" }, "require-dev": { - "symfony/dependency-injection": "~2.0" + "psr/log": "~1.0", + "symfony/dependency-injection": "~2.0", + "symfony/stopwatch": "~2.2" }, "suggest": { "symfony/dependency-injection": "", @@ -1887,7 +1979,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.4-dev" + "dev-master": "2.5-dev" } }, "autoload": { @@ -1902,7 +1994,9 @@ "authors": [ { "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" }, { "name": "Symfony Community", @@ -1911,7 +2005,7 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "http://symfony.com", - "time": "2013-10-17 11:48:11" + "time": "2014-02-11 13:52:21" }, { "name": "symfony/finder", @@ -1920,12 +2014,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/Finder.git", - "reference": "e2ce3164ab58b4d54612e630571f158035ee8603" + "reference": "a708807d3a54b258abb5bda6832ced8b9bbf8ffb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Finder/zipball/e2ce3164ab58b4d54612e630571f158035ee8603", - "reference": "e2ce3164ab58b4d54612e630571f158035ee8603", + "url": "https://api.github.com/repos/symfony/Finder/zipball/a708807d3a54b258abb5bda6832ced8b9bbf8ffb", + "reference": "a708807d3a54b258abb5bda6832ced8b9bbf8ffb", "shasum": "" }, "require": { @@ -1934,7 +2028,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.4-dev" + "dev-master": "2.5-dev" } }, "autoload": { @@ -1949,7 +2043,9 @@ "authors": [ { "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" }, { "name": "Symfony Community", @@ -1958,7 +2054,7 @@ ], "description": "Symfony Finder Component", "homepage": "http://symfony.com", - "time": "2013-09-19 09:47:34" + "time": "2014-02-24 16:21:51" }, { "name": "symfony/yaml", @@ -1967,12 +2063,12 @@ "source": { "type": "git", "url": "https://github.com/symfony/Yaml.git", - "reference": "1f7cabb841e62ec49615bd965ac780fd994b3f64" + "reference": "e49a47d60348665261f6e279ba383241deb73cab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Yaml/zipball/1f7cabb841e62ec49615bd965ac780fd994b3f64", - "reference": "1f7cabb841e62ec49615bd965ac780fd994b3f64", + "url": "https://api.github.com/repos/symfony/Yaml/zipball/e49a47d60348665261f6e279ba383241deb73cab", + "reference": "e49a47d60348665261f6e279ba383241deb73cab", "shasum": "" }, "require": { @@ -1981,7 +2077,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.4-dev" + "dev-master": "2.5-dev" } }, "autoload": { @@ -1996,7 +2092,9 @@ "authors": [ { "name": "Fabien Potencier", - "email": "fabien@symfony.com" + "email": "fabien@symfony.com", + "homepage": "http://fabien.potencier.org", + "role": "Lead Developer" }, { "name": "Symfony Community", @@ -2005,7 +2103,7 @@ ], "description": "Symfony Yaml Component", "homepage": "http://symfony.com", - "time": "2013-10-17 11:48:11" + "time": "2014-02-24 16:21:51" } ], "aliases": [ diff --git a/spec/Gaufrette/Adapter/OpenCloudSpec.php b/spec/Gaufrette/Adapter/OpenCloudSpec.php index a42c52f57..276f780f2 100644 --- a/spec/Gaufrette/Adapter/OpenCloudSpec.php +++ b/spec/Gaufrette/Adapter/OpenCloudSpec.php @@ -1,23 +1,29 @@ + * @author Daniel Richter */ class OpenCloudSpec extends ObjectBehavior { /** * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container */ - function let($objectStore) + function let($objectStore, $container) { + $objectStore->getContainer("test")->willReturn($container); $this->beConstructedWith($objectStore, 'test', false, false); } @@ -27,202 +33,207 @@ function it_is_adapter() } /** - * @param OpenCloud\ObjectStore\Service $objectStore * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - function it_reads_file($objectStore, $container, $object) + function it_reads_file($container, $object) { - $object->saveToString()->willReturn("Hello World"); - $container->dataObject("test")->willReturn($object); - $objectStore->container("test")->willReturn($container); + $object->getContent()->willReturn("Hello World"); + $container->getObject("test")->willReturn($object); $this->read('test')->shouldReturn('Hello World'); } /** - * @param OpenCloud\ObjectStore\Service $objectStore * @param OpenCloud\ObjectStore\Resource\Container $container */ - function it_reads_file_on_error_returns_false($objectStore, $container) + function it_reads_file_on_error_returns_false($container) { - $container->dataObject("test")->willThrow(new ObjFetchError()); - $objectStore->container("test")->willReturn($container); + $container->getObject("test")->willThrow(new ObjectNotFoundException()); $this->read('test')->shouldReturn(false); } /** - * @param OpenCloud\ObjectStore\Service $objectStore * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - function it_writes_file_if_key_does_not_exist_returns_size($objectStore, $container, $object) + function it_writes_file_returns_size($container, $object) { $testData = "Hello World!"; - $testDataSize = sizeof($testData); - $object->create(array ('name' => 'test'))->willReturn(null); - $object->setData($testData)->willReturn(null); - $object->bytes = $testDataSize; - $container->dataObject('test')->willReturn(false); - $container->dataObject()->willReturn($object); - $objectStore->container("test")->willReturn($container); + $testDataSize = strlen($testData); - $this->write('test', $testData)->shouldReturn($testDataSize); - } - - /** - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\ObjectStore\Resource\DataObject $object - */ - function it_writes_file_if_key_exists_returns_size($objectStore, $container, $object) - { - $testData = "Hello World!"; - $testDataSize = sizeof($testData); - $object->bytes = $testDataSize; - $container->dataObject('test')->willReturn($object); - $objectStore->container("test")->willReturn($container); + $object->getContentLength()->willReturn($testDataSize); + $container->uploadObject('test', $testData)->willReturn($object); $this->write('test', $testData)->shouldReturn($testDataSize); } /** - * @param OpenCloud\ObjectStore\Service $objectStore * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - function it_writes_file_and_write_fails_returns_false($objectStore, $container, $object) + function it_writes_file_and_write_fails_returns_false($container) { $testData = "Hello World!"; - $object->create(array ('name' => 'test'))->willThrow(new CreateUpdateError()); - $object->setData($testData)->willReturn(null); - $container->dataObject('test')->willReturn(false); - $container->dataObject()->willReturn($object); - $objectStore->container("test")->willReturn($container); + + $container->uploadObject('test', $testData)->willThrow(new CreateUpdateError()); $this->write('test', $testData)->shouldReturn(false); } /** - * @param OpenCloud\ObjectStore\Service $objectStore * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - function if_key_exists_return_true($objectStore, $container, $object) + function it_returns_true_if_key_exists($container, $object) { - $container->dataObject('test')->willReturn($object); - $objectStore->container("test")->willReturn($container); + $container->getObject('test')->willReturn($object); $this->exists('test')->shouldReturn(true); } /** - * @param OpenCloud\ObjectStore\Service $objectStore * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - function if_key_does_not_exist_return_false($objectStore, $container, $object) + function it_returns_false_if_key_does_not_exist($container) { - $container->dataObject('test')->willReturn($object)->willThrow(new ObjFetchError()); - $objectStore->container("test")->willReturn($container); + $container->getObject('test')->willThrow(new ObjectNotFoundException()); $this->exists('test')->shouldReturn(false); } /** - * @param OpenCloud\ObjectStore\Service $objectStore * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - function it_deletes_file_on_success_returns_true($objectStore, $container, $object) + function it_deletes_file_on_success_returns_true($container, $object) { $object->delete()->willReturn(null); - $container->dataObject("test")->willReturn($object); - $objectStore->container("test")->willReturn($container); + $container->getObject("test")->willReturn($object); $this->delete('test')->shouldReturn(true); } /** - * @param OpenCloud\ObjectStore\Service $objectStore * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - function it_deletes_file_returns_false_on_failure($objectStore, $container, $object) + function it_deletes_file_returns_false_on_failure($container, $object) { $object->delete()->willThrow(new DeleteError()); - $container->dataObject("test")->willReturn($object); - $objectStore->container("test")->willReturn($container); + $container->getObject("test")->willReturn($object); $this->delete('test')->shouldReturn(false); } /** - * @param OpenCloud\ObjectStore\Service $objectStore * @param OpenCloud\ObjectStore\Resource\Container $container */ - function it_deletes_file_if_file_does_not_exist_returns_false($objectStore, $container) + function it_deletes_file_if_file_does_not_exist_returns_false($container) { - $container->dataObject("test")->willThrow(new ObjFetchError()); - $objectStore->container("test")->willReturn($container); + $container->getObject("test")->willThrow(new ObjectNotFoundException()); $this->delete('test')->shouldReturn(false); } /** - * @param OpenCloud\ObjectStore\Service $objectStore * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - function it_returns_checksum_if_file_exists($objectStore, $container, $object) + function it_returns_checksum_if_file_exists($container, $object) { - $object->getETag()->willReturn("test String"); - $container->dataObject("test")->willReturn($object); - $objectStore->container("test")->willReturn($container); + $object->getEtag()->willReturn("test String"); + $container->getObject("test")->willReturn($object); $this->checksum('test')->shouldReturn("test String"); } /** - * @param OpenCloud\ObjectStore\Service $objectStore * @param OpenCloud\ObjectStore\Resource\Container $container */ - function it_returns_false_when_file_does_not_exist($objectStore, $container) + function it_returns_false_when_file_does_not_exist($container) { - $container->dataObject("test")->willThrow(new ObjFetchError()); - $objectStore->container("test")->willReturn($container); + $container->getObject("test")->willThrow(new ObjectNotFoundException()); $this->checksum('test')->shouldReturn(false); } /** - * @param OpenCloud\ObjectStore\Service $objectStore * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\Common\Collection $objectList + * @param OpenCloud\ObjectStore\Resource\DataObject $object1 + * @param OpenCloud\ObjectStore\Resource\DataObject $object2 + * @param OpenCloud\ObjectStore\Resource\DataObject $object3 */ - function it_returns_files_as_sorted_array($objectStore, $container, $objectList) + function it_returns_files_as_sorted_array($container, $objectList, $object1, $object2, $object3) { - $inputArray = array ('key5', 'key2', 'key1'); - $outputArray = $inputArray; - sort($outputArray); + $outputArray = array('key1', 'key2', 'key5'); $index = 0; + $object1->getName()->willReturn('key5'); + $object2->getName()->willReturn('key2'); + $object3->getName()->willReturn('key1'); + + $objects = array($object1, $object2, $object3); + $objectList->next()->will( - function () use ($inputArray, &$index) { - if ($index < count($inputArray)) { - $objectItem = new \stdClass(); - $objectItem->name = $inputArray[$index]; + function () use ($objects, &$index) { + if ($index < count($objects)) { $index++; - return $objectItem; + return $objects[$index - 1]; } } - ) ->shouldBeCalledTimes(count($inputArray) + 1); + ) ->shouldBeCalledTimes(count($objects) + 1); $container->objectList()->willReturn($objectList); - $objectStore->container("test")->willReturn($container); $this->keys()->shouldReturn($outputArray); } + + /** + * @param OpenCloud\ObjectStore\Service $objectStore + */ + function it_throws_exception_if_container_does_not_exist($objectStore) + { + $containerName = 'container-does-not-exist'; + + $objectStore->getContainer($containerName)->willThrow(new BadResponseException()); + $this->beConstructedWith($objectStore, $containerName); + + $this->shouldThrow('\RuntimeException')->duringExists('test'); + } + + /** + * @param OpenCloud\ObjectStore\Service $objectStore + * @param OpenCloud\ObjectStore\Resource\Container $container + */ + function it_creates_container($objectStore, $container) + { + $containerName = 'container-does-not-yet-exist'; + $filename = 'test'; + + $objectStore->getContainer($containerName)->willThrow(new BadResponseException()); + $objectStore->createContainer($containerName)->willReturn($container); + $container->getObject($filename)->willThrow(new ObjectNotFoundException()); + + $this->beConstructedWith($objectStore, $containerName, true); + + $this->exists($filename)->shouldReturn(false); + } + + /** + * @param OpenCloud\ObjectStore\Service $objectStore + */ + function it_throws_exeption_if_container_creation_fails($objectStore) + { + $containerName = 'container-does-not-yet-exist'; + + $objectStore->getContainer($containerName)->willThrow(new BadResponseException()); + $objectStore->createContainer($containerName)->willReturn(false); + + $this->beConstructedWith($objectStore, $containerName, true); + + $this->shouldThrow('\RuntimeException')->duringExists('test'); + } } diff --git a/src/Gaufrette/Adapter/OpenCloud.php b/src/Gaufrette/Adapter/OpenCloud.php index 955fe22fe..5ad398e5d 100644 --- a/src/Gaufrette/Adapter/OpenCloud.php +++ b/src/Gaufrette/Adapter/OpenCloud.php @@ -3,64 +3,82 @@ namespace Gaufrette\Adapter; use Gaufrette\Adapter; +use Guzzle\Http\Exception\BadResponseException; use OpenCloud\Common\Exceptions\DeleteError; use OpenCloud\ObjectStore\Resource\Container; use OpenCloud\ObjectStore\Service; use OpenCloud\Common\Exceptions\CreateUpdateError; -use OpenCloud\Common\Exceptions\ObjFetchError; +use OpenCloud\ObjectStore\Exception\ObjectNotFoundException; /** * OpenCloud adapter * * @package Gaufrette * @author James Watson + * @author Daniel Richter */ class OpenCloud implements Adapter, ChecksumCalculator { /** - * @var ObjectStore + * @var Service */ protected $objectStore; + /** * @var string */ protected $containerName; + /** * @var bool */ protected $createContainer; - /** - * @var bool - */ - protected $detectContentType; + /** * @var Container */ protected $container; - public function __construct( - Service $objectStore, $containerName, $createContainer = false, $detectContentType = true - ) { - $this->objectStore = $objectStore; - $this->containerName = $containerName; - $this->createContainer = $createContainer; - $this->detectContentType = $detectContentType; + /** + * Constructor + * + * @param Service $objectStore + * @param string $containerName The name of the container + * @param bool $createContainer Whether to create the container if it does not exist + */ + public function __construct(Service $objectStore, $containerName, $createContainer = false) + { + $this->objectStore = $objectStore; + $this->containerName = $containerName; + $this->createContainer = $createContainer; } - private function initialize() + /** + * Returns an initialized container + * + * @throws \Exception + * @return Container + */ + protected function getContainer() { - if (!$this->container instanceof Container) { - - if ($this->createContainer) { - $container = $this->objectStore->Container(); - $container->name = $this->containerName; - $container->Create(); - } else { - $container = $this->objectStore->Container($this->containerName); + if ($this->container) { + return $this->container; + } + + try { + return $this->container = $this->objectStore->getContainer($this->containerName); + } catch (BadResponseException $e) { //OpenCloud lib does not wrap this exception + if (!$this->createContainer) { + throw new \RuntimeException(sprintf('Container %s does not exist.', $this->containerName)); } - $this->container = $container; } + + if (!$container = $this->objectStore->createContainer($this->containerName)) { + throw new \RuntimeException(sprintf('Container %s could not be created.', $this->containerName)); + } + + return $this->container = $container; } /** @@ -72,17 +90,11 @@ private function initialize() */ public function read($key) { - $this->initialize(); - - // This method can return boolean or the object - // if there is a fetch error, tryGetObject returns false - // If it returns false, php throws a fatal error because boolean is a non-object. - $object = $this->tryGetObject($key); - if ($object) { - return $object->SaveToString(); + if ($object = $this->tryGetObject($key)) { + return $object->getContent(); } - return $object; + return false; } /** @@ -95,30 +107,14 @@ public function read($key) */ public function write($key, $content) { - $this->initialize(); - $object = $this->tryGetObject($key); - try { - if ($object === false) { - $object = $this->container->DataObject(); - $object->SetData($content); - - $data = array ('name' => $key); - - if ($this->detectContentType) { - $fileInfo = new \finfo(FILEINFO_MIME_TYPE); - $contentType = $fileInfo->buffer($content); - $data['content_type'] = $contentType; - } - - $object->Create($data); - } - - return $object->bytes; + $object = $this->getContainer()->uploadObject($key, $content); } catch (CreateUpdateError $updateError) { return false; } + + return $object->getContentLength(); } /** @@ -130,9 +126,7 @@ public function write($key, $content) */ public function exists($key) { - $this->initialize(); - - return ($this->tryGetObject($key) !== false); + return $this->tryGetObject($key) !== false; } /** @@ -142,12 +136,13 @@ public function exists($key) */ public function keys() { - $this->initialize(); - $objectList = $this->container->ObjectList(); - $keys = array (); - while ($object = $objectList->Next()) { - $keys[] = $object->name; + $objectList = $this->getContainer()->objectList(); + $keys = array (); + + while ($object = $objectList->next()) { + $keys[] = $object->getName(); } + sort($keys); return $keys; @@ -162,12 +157,8 @@ public function keys() */ public function mtime($key) { - $this->initialize(); - - $object = $this->tryGetObject($key); - - if ($object) { - return $object->last_modified; + if ($object = $this->tryGetObject($key)) { + return $object->getLastModified(); } return false; @@ -182,16 +173,14 @@ public function mtime($key) */ public function delete($key) { - $this->initialize(); + if (!$object = $this->tryGetObject($key)) { + return false; + } + try { - $object = $this->tryGetObject($key); - if (!$object) { - return false; - } - $object->Delete(); + $object->delete(); } catch (DeleteError $deleteError) { - return false; } @@ -208,9 +197,13 @@ public function delete($key) */ public function rename($sourceKey, $targetKey) { - $this->initialize(); - $this->write($targetKey, $this->read($sourceKey)); - $this->delete($sourceKey); + if (false !== $this->write($targetKey, $this->read($sourceKey))) { + $this->delete($sourceKey); + + return true; + } + + return false; } /** @@ -234,9 +227,7 @@ public function isDirectory($key) */ public function checksum($key) { - $this->initialize(); - $object = $this->tryGetObject($key); - if ($object) { + if ($object = $this->tryGetObject($key)) { return $object->getETag(); } @@ -244,15 +235,16 @@ public function checksum($key) } /** - * @param $key - * @return \OpenCloud\ObjectStore\Resource\DataObject + * @param string $key + * + * @return \OpenCloud\ObjectStore\Resource\DataObject|false */ protected function tryGetObject($key) { try { - return $this->container->DataObject($key); + return $this->getContainer()->getObject($key); } - catch (ObjFetchError $objFetchError) { + catch (ObjectNotFoundException $objFetchError) { return false; } } From 23057b0b2eee107a0b98179f6bbbe38eb02e3d9d Mon Sep 17 00:00:00 2001 From: Richtermeister Date: Tue, 25 Feb 2014 11:43:51 -0800 Subject: [PATCH 089/426] Add support for new lazy OpenCloud --- spec/Gaufrette/Adapter/LazyOpenCloudSpec.php | 42 +++++++++++++++++ spec/Gaufrette/Adapter/OpenCloudSpec.php | 4 +- src/Gaufrette/Adapter/LazyOpenCloud.php | 44 ++++++++++++++++++ src/Gaufrette/Adapter/OpenCloud.php | 6 +-- .../ObjectStoreFactoryInterface.php | 13 ++++++ .../OpenStackObjectStoreFactory.php | 45 +++++++++++++++++++ 6 files changed, 149 insertions(+), 5 deletions(-) create mode 100644 spec/Gaufrette/Adapter/LazyOpenCloudSpec.php create mode 100644 src/Gaufrette/Adapter/LazyOpenCloud.php create mode 100644 src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactoryInterface.php create mode 100644 src/Gaufrette/Adapter/OpenStackCloudFiles/OpenStackObjectStoreFactory.php diff --git a/spec/Gaufrette/Adapter/LazyOpenCloudSpec.php b/spec/Gaufrette/Adapter/LazyOpenCloudSpec.php new file mode 100644 index 000000000..b2543f22c --- /dev/null +++ b/spec/Gaufrette/Adapter/LazyOpenCloudSpec.php @@ -0,0 +1,42 @@ + + */ +class LazyOpenCloudSpec extends ObjectBehavior +{ + /** + * @param \Gaufrette\Adapter\OpenStackCloudFiles\ObjectStoreFactoryInterface $objectStoreFactory + */ + function let($objectStoreFactory) + { + $this->beConstructedWith($objectStoreFactory, 'test-container-name'); + } + + function it_is_adapter() + { + $this->shouldHaveType('Gaufrette\Adapter'); + } + + /** + * @param \Gaufrette\Adapter\OpenStackCloudFiles\ObjectStoreFactoryInterface $objectStoreFactory + * @param \OpenCloud\ObjectStore\Service $objectStore + * @param \OpenCloud\ObjectStore\Resource\Container $container + */ + function it_initializes_object_store($objectStoreFactory, $objectStore, $container) + { + $objectStoreFactory->getObjectStore()->shouldBeCalled()->willReturn($objectStore); + $objectStore->getContainer("test-container-name")->shouldBeCalled()->willReturn($container); + $container->getObject("test-file-name")->willThrow(new ObjectNotFoundException()); + + $this->exists("test-file-name"); + } +} diff --git a/spec/Gaufrette/Adapter/OpenCloudSpec.php b/spec/Gaufrette/Adapter/OpenCloudSpec.php index 276f780f2..9c4c1744c 100644 --- a/spec/Gaufrette/Adapter/OpenCloudSpec.php +++ b/spec/Gaufrette/Adapter/OpenCloudSpec.php @@ -19,12 +19,12 @@ class OpenCloudSpec extends ObjectBehavior { /** * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container + * @param OpenCloud\ObjectStore\Resource\Container $container */ function let($objectStore, $container) { $objectStore->getContainer("test")->willReturn($container); - $this->beConstructedWith($objectStore, 'test', false, false); + $this->beConstructedWith($objectStore, 'test', false); } function it_is_adapter() diff --git a/src/Gaufrette/Adapter/LazyOpenCloud.php b/src/Gaufrette/Adapter/LazyOpenCloud.php new file mode 100644 index 000000000..d68bb733a --- /dev/null +++ b/src/Gaufrette/Adapter/LazyOpenCloud.php @@ -0,0 +1,44 @@ + + */ +class LazyOpenCloud extends OpenCloud +{ + /** + * @var ObjectStoreFactoryInterface + */ + protected $objectStoreFactory; + + /** + * @param ObjectStoreFactoryInterface $objectStoreFactory + * @param string $containerName + * @param bool $createContainer + */ + public function __construct(ObjectStoreFactoryInterface $objectStoreFactory, $containerName, $createContainer = false) + { + $this->objectStoreFactory = $objectStoreFactory; + $this->containerName = $containerName; + $this->createContainer = $createContainer; + } + + /** + * Override parent to lazy-load object store + * + * {@inheritdoc} + */ + protected function getContainer() + { + if (!$this->objectStore) { + $this->objectStore = $this->objectStoreFactory->getObjectStore(); + } + + return parent::getContainer(); + } +} diff --git a/src/Gaufrette/Adapter/OpenCloud.php b/src/Gaufrette/Adapter/OpenCloud.php index 5ad398e5d..8f24c53ba 100644 --- a/src/Gaufrette/Adapter/OpenCloud.php +++ b/src/Gaufrette/Adapter/OpenCloud.php @@ -57,7 +57,7 @@ public function __construct(Service $objectStore, $containerName, $createContain /** * Returns an initialized container * - * @throws \Exception + * @throws \RuntimeException * @return Container */ protected function getContainer() @@ -70,12 +70,12 @@ protected function getContainer() return $this->container = $this->objectStore->getContainer($this->containerName); } catch (BadResponseException $e) { //OpenCloud lib does not wrap this exception if (!$this->createContainer) { - throw new \RuntimeException(sprintf('Container %s does not exist.', $this->containerName)); + throw new \RuntimeException(sprintf('Container "%s" does not exist.', $this->containerName)); } } if (!$container = $this->objectStore->createContainer($this->containerName)) { - throw new \RuntimeException(sprintf('Container %s could not be created.', $this->containerName)); + throw new \RuntimeException(sprintf('Container "%s" could not be created.', $this->containerName)); } return $this->container = $container; diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactoryInterface.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactoryInterface.php new file mode 100644 index 000000000..b2686f3a8 --- /dev/null +++ b/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactoryInterface.php @@ -0,0 +1,13 @@ +connection = $connection; + $this->region = $region; + $this->urlType = $urlType; + } + + /** + * {@inheritdoc} + */ + public function getObjectStore() + { + return $this->connection->objectStoreService('cloudFiles', $this->region, $this->urlType); + } +} From 7e951f027085ff94982ca5d1edbeb03165ec30dd Mon Sep 17 00:00:00 2001 From: Richtermeister Date: Tue, 25 Feb 2014 13:27:15 -0800 Subject: [PATCH 090/426] Fixing specs, keeping existing adapters backwards-compatible. --- .../Adapter/LazyOpenStackCloudFilesSpec.php | 260 +----------------- .../Adapter/LazyOpenStackCloudFiles.php | 113 +------- .../BaseOpenStackAuthenticationFactory.php | 1 + .../ConnectionFactoryInterface.php | 1 + ...toreFactory.php => ObjectStoreFactory.php} | 7 +- .../ObjectStoreFactoryInterface.php | 5 + .../OpenStackAuthenticationFactory.php | 9 +- .../RackspaceAuthenticationFactory.php | 7 +- 8 files changed, 42 insertions(+), 361 deletions(-) rename src/Gaufrette/Adapter/OpenStackCloudFiles/{OpenStackObjectStoreFactory.php => ObjectStoreFactory.php} (84%) diff --git a/spec/Gaufrette/Adapter/LazyOpenStackCloudFilesSpec.php b/spec/Gaufrette/Adapter/LazyOpenStackCloudFilesSpec.php index 8968a2d76..b41b18a47 100644 --- a/spec/Gaufrette/Adapter/LazyOpenStackCloudFilesSpec.php +++ b/spec/Gaufrette/Adapter/LazyOpenStackCloudFilesSpec.php @@ -2,10 +2,12 @@ namespace spec\Gaufrette\Adapter; +use OpenCloud\ObjectStore\Exception\ObjectNotFoundException; use PhpSpec\ObjectBehavior; use OpenCloud\Common\Exceptions\ObjFetchError; use OpenCloud\Common\Exceptions\CreateUpdateError; use OpenCloud\Common\Exceptions\DeleteError; +use Prophecy\Argument; /** * Class LazyOpenStackCloudFilesSpec @@ -22,9 +24,8 @@ function let($connectionFactory) { $containerName = 'test'; $createContainer = false; - $detectContentType = false; - $this->beConstructedWith($connectionFactory, $containerName, $createContainer, $detectContentType); + $this->beConstructedWith($connectionFactory, $containerName, $createContainer); } function it_is_adapter() @@ -33,253 +34,18 @@ function it_is_adapter() } /** - * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - * @param OpenCloud\OpenStack $connection - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\ObjectStore\Resource\DataObject $object - */ - function it_reads_file($connectionFactory, $connection, $objectStore, $container, $object) - { - $object->saveToString()->willReturn("Hello World"); - $container->dataObject("test")->willReturn($object); - $objectStore->container("test")->willReturn($container); - $connection->objectStore()->willReturn($objectStore)->shouldBeCalledTimes(1); - $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); - $this->read('test')->shouldReturn('Hello World'); - } - - /** - * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - * @param OpenCloud\OpenStack $connection - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container - */ - function it_reads_file_on_error_returns_false($connectionFactory, $connection, $objectStore, $container) - { - $connection->objectStore()->willReturn($objectStore)->shouldBeCalledTimes(1); - $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); - $container->dataObject("test")->willThrow(new ObjFetchError()); - $objectStore->container("test")->willReturn($container); - - $this->read('test')->shouldReturn(false); - } - - /** - * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - * @param OpenCloud\OpenStack $connection - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\ObjectStore\Resource\DataObject $object - */ - function it_writes_file_key_doesnot_exist($connectionFactory, $connection, $objectStore, $container, $object) - { - $testData = "Hello World!"; - $testDataSize = sizeof($testData); - $object->create(array ('name' => 'test'))->willReturn(null); - $object->setData($testData)->willReturn(null); - $object->bytes = $testDataSize; - $container->dataObject('test')->willReturn(false); - $container->dataObject()->willReturn($object); - $objectStore->container("test")->willReturn($container); - $connection->objectStore()->willReturn($objectStore)->shouldBeCalledTimes(1); - $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); - - $this->write('test', $testData)->shouldReturn($testDataSize); - } - - /** - * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - * @param OpenCloud\OpenStack $connection - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\ObjectStore\Resource\DataObject $object - */ - function it_writes_file_key_exists($connectionFactory, $connection, $objectStore, $container, $object) - { - $testData = "Hello World!"; - $testDataSize = sizeof($testData); - $object->bytes = $testDataSize; - $container->dataObject('test')->willReturn($object); - $objectStore->container("test")->willReturn($container); - $connection->objectStore()->willReturn($objectStore)->shouldBeCalledTimes(1); - $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); - - $this->write('test', $testData)->shouldReturn($testDataSize); - } - - /** - * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - * @param OpenCloud\OpenStack $connection - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\ObjectStore\Resource\DataObject $object - */ - function it_writes_file_write_fails($connectionFactory, $connection, $objectStore, $container, $object) - { - $testData = "Hello World!"; - $object->create(array ('name' => 'test'))->willThrow(new CreateUpdateError()); - $object->setData($testData)->willReturn(null); - $container->dataObject('test')->willReturn(false); - $container->dataObject()->willReturn($object); - $objectStore->container("test")->willReturn($container); - $connection->objectStore()->willReturn($objectStore)->shouldBeCalledTimes(1); - $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); - - $this->write('test', $testData)->shouldReturn(false); - } - - /** - * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - * @param OpenCloud\OpenStack $connection - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\ObjectStore\Resource\DataObject $object + * @param \Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory + * @param \OpenCloud\OpenStack $openStack + * @param \OpenCloud\ObjectStore\Service $objectStore + * @param \OpenCloud\ObjectStore\Resource\Container $container */ - function it_key_exists_true($connectionFactory, $connection, $objectStore, $container, $object) + function it_initializes_object_store($connectionFactory, $openStack, $objectStore, $container) { - $container->dataObject('test')->willReturn($object); - $objectStore->container("test")->willReturn($container); - $connection->objectStore()->willReturn($objectStore)->shouldBeCalledTimes(1); - $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); - - $this->exists('test')->shouldReturn(true); - } - - /** - * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - * @param OpenCloud\OpenStack $connection - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\ObjectStore\Resource\DataObject $object - */ - function it_key_does_not_exist_false($connectionFactory, $connection, $objectStore, $container, $object) - { - $container->dataObject('test')->willReturn($object)->willThrow(new ObjFetchError()); - $objectStore->container("test")->willReturn($container); - $connection->objectStore()->willReturn($objectStore)->shouldBeCalledTimes(1); - $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); - - $this->exists('test')->shouldReturn(false); - } - - /** - * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - * @param OpenCloud\OpenStack $connection - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\ObjectStore\Resource\DataObject $object - */ - function it_deletes_file($connectionFactory, $connection, $objectStore, $container, $object) - { - $object->delete()->willReturn(null); - $container->dataObject("test")->willReturn($object); - $objectStore->container("test")->willReturn($container); - $connection->objectStore()->willReturn($objectStore)->shouldBeCalledTimes(1); - $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); - - $this->delete('test')->shouldReturn(true); - } - - /** - * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - * @param OpenCloud\OpenStack $connection - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\ObjectStore\Resource\DataObject $object - */ - function it_deletes_file_fails($connectionFactory, $connection, $objectStore, $container, $object) - { - $object->delete()->willThrow(new DeleteError()); - $container->dataObject("test")->willReturn($object); - $objectStore->container("test")->willReturn($container); - $connection->objectStore()->willReturn($objectStore)->shouldBeCalledTimes(1); - $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); - - $this->delete('test')->shouldReturn(false); - } - - /** - * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - * @param OpenCloud\OpenStack $connection - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container - */ - function it_deletes_file_does_not_exist_returns_false($connectionFactory, $connection, $objectStore, $container) - { - $container->dataObject("test")->willThrow(new ObjFetchError()); - $objectStore->container("test")->willReturn($container); - $connection->objectStore()->willReturn($objectStore)->shouldBeCalledTimes(1); - $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); - - $this->delete('test')->shouldReturn(false); - } - - /** - * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - * @param OpenCloud\OpenStack $connection - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\ObjectStore\Resource\DataObject $object - */ - function it_checksum_returns_string($connectionFactory, $connection, $objectStore, $container, $object) - { - $object->getETag()->willReturn("test String"); - $container->dataObject("test")->willReturn($object); - $objectStore->container("test")->willReturn($container); - $connection->objectStore()->willReturn($objectStore)->shouldBeCalledTimes(1); - $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); - - $this->checksum('test')->shouldReturn("test String"); - } - - /** - * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - * @param OpenCloud\OpenStack $connection - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container - */ - function it_checksum_file_does_not_exist_returns_false($connectionFactory, $connection, $objectStore, $container) - { - $container->dataObject("test")->willThrow(new ObjFetchError()); - $objectStore->container("test")->willReturn($container); - $connection->objectStore()->willReturn($objectStore)->shouldBeCalledTimes(1); - $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); - - $this->checksum('test')->shouldReturn(false); - } - - /** - * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - * @param OpenCloud\OpenStack $connection - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\Common\Collection $objectList - */ - function it_keys_returns_sorted_array($connectionFactory, $connection, $objectStore, $container, $objectList) - { - $inputArray = array ('key5', 'key2', 'key1'); - $outputArray = $inputArray; - sort($outputArray); - $index = 0; - - $objectList->next()->will( - function () use ($inputArray, &$index) { - if ($index < count($inputArray)) { - $objectItem = new \stdClass(); - $objectItem->name = $inputArray[$index]; - $index++; - - return $objectItem; - } - } - ) ->shouldBeCalledTimes(count($inputArray) + 1); - - $container->objectList()->willReturn($objectList); - $objectStore->container("test")->willReturn($container); - $connection->objectStore()->willReturn($objectStore)->shouldBeCalledTimes(1); - $connectionFactory->create()->willReturn($connection)->shouldBeCalledTimes(1); + $connectionFactory->create()->shouldBeCalled()->willReturn($openStack); + $openStack->objectStoreService(Argument::cetera())->willReturn($objectStore); + $objectStore->getContainer(Argument::any())->shouldBeCalled()->willReturn($container); + $container->getObject(Argument::any())->willThrow(new ObjectNotFoundException()); - $this->keys()->shouldReturn($outputArray); + $this->exists("test-file-name"); } } diff --git a/src/Gaufrette/Adapter/LazyOpenStackCloudFiles.php b/src/Gaufrette/Adapter/LazyOpenStackCloudFiles.php index e31fa0bd6..72e502cd7 100644 --- a/src/Gaufrette/Adapter/LazyOpenStackCloudFiles.php +++ b/src/Gaufrette/Adapter/LazyOpenStackCloudFiles.php @@ -8,131 +8,38 @@ * Class LazyOpenStackCloudFiles * @package Gaufrette\Adapter * @author Chris Warner + * @deprecated in favor of LazyOpenCloud */ class LazyOpenStackCloudFiles extends OpenCloud { - /** * @var ConnectionFactoryInterface */ protected $connectionFactory; - /** - * @var bool - */ - protected $connected = false; - /** * @param ConnectionFactoryInterface $connectionFactory * @param string $containerName * @param bool $createContainer - * @param bool $detectContentType */ - public function __construct($connectionFactory, $containerName, $createContainer = false, $detectContentType = true) + public function __construct($connectionFactory, $containerName, $createContainer = false) { $this->connectionFactory = $connectionFactory; $this->containerName = $containerName; $this->createContainer = $createContainer; - $this->detectContentType = $detectContentType; - } - - /** - * @inheritdoc - */ - public function read($key) - { - $this->connect(); - - return parent::read($key); - } - - /** - * @@inheritdoc - */ - public function write($key, $content) - { - $this->connect(); - - return parent::write($key, $content); - } - - /** - * @inheritdoc - */ - public function exists($key) - { - $this->connect(); - - return parent::exists($key); - } - - /** - * @inheritdoc - */ - public function keys() - { - $this->connect(); - - return parent::keys(); - } - - /** - * @inheritdoc - */ - public function delete($key) - { - $this->connect(); - - return parent::delete($key); } /** - * @inheritdoc + * Override parent to lazy-load object store + * + * {@inheritdoc} */ - public function rename($sourceKey, $targetKey) + protected function getContainer() { - $this->connect(); - - parent::rename($sourceKey, $targetKey); - } - - /** - * @inheritdoc - */ - public function isDirectory($key) - { - $this->connect(); - - return parent::isDirectory($key); - } - - /** - * @inheritdoc - */ - public function checksum($key) - { - $this->connect(); - - return parent::checksum($key); - } - - /** - * @inheritdoc - */ - public function mtime($key) - { - $this->connect(); - - return parent::mtime($key); - } - - protected function connect() - { - if (!$this->connected) { - /** @var OpenStack $connection */ - $connection = $this->connectionFactory->create(); - - $this->objectStore = $connection->objectStore(); + if (!$this->objectStore) { + $this->objectStore = $this->connectionFactory->create()->objectStoreService('cloudFiles', null, 'publicURL'); } + + return parent::getContainer(); } } diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/BaseOpenStackAuthenticationFactory.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/BaseOpenStackAuthenticationFactory.php index ad0734e0a..846db776e 100644 --- a/src/Gaufrette/Adapter/OpenStackCloudFiles/BaseOpenStackAuthenticationFactory.php +++ b/src/Gaufrette/Adapter/OpenStackCloudFiles/BaseOpenStackAuthenticationFactory.php @@ -10,6 +10,7 @@ * Class BaseOpenStackAuthenticationFactory * @package Gaufrette\Adapter\OpenStackCloudFiles * @author Chris Warner + * @deprecated in favor of ObjectStoreFactory */ abstract class BaseOpenStackAuthenticationFactory implements ConnectionFactoryInterface { diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/ConnectionFactoryInterface.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/ConnectionFactoryInterface.php index b401b854c..f1b5d6f3b 100644 --- a/src/Gaufrette/Adapter/OpenStackCloudFiles/ConnectionFactoryInterface.php +++ b/src/Gaufrette/Adapter/OpenStackCloudFiles/ConnectionFactoryInterface.php @@ -7,6 +7,7 @@ * Interface ConnectionFactoryInterface * @package Gaufrette\Adapter\OpenStackCloudFiles * @author Chris Warner + * @deprecated in favor of ObjectStoreFactory */ interface ConnectionFactoryInterface { diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/OpenStackObjectStoreFactory.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactory.php similarity index 84% rename from src/Gaufrette/Adapter/OpenStackCloudFiles/OpenStackObjectStoreFactory.php rename to src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactory.php index 56e1aadb1..6f802c908 100644 --- a/src/Gaufrette/Adapter/OpenStackCloudFiles/OpenStackObjectStoreFactory.php +++ b/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactory.php @@ -4,7 +4,12 @@ use OpenCloud\OpenStack; -class OpenStackObjectStoreFactory implements ObjectStoreFactoryInterface +/** + * ObjectStoreFactory + * + * @author Daniel Richter + */ +class ObjectStoreFactory implements ObjectStoreFactoryInterface { /** * @var OpenStack diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactoryInterface.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactoryInterface.php index b2686f3a8..e679189f8 100644 --- a/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactoryInterface.php +++ b/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactoryInterface.php @@ -4,6 +4,11 @@ use OpenCloud\ObjectStore\Service; +/** + * ObjectStoreFactoryInterface + * + * @author Daniel Richter + */ interface ObjectStoreFactoryInterface { /** diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/OpenStackAuthenticationFactory.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/OpenStackAuthenticationFactory.php index 21a10d152..04745cbf5 100644 --- a/src/Gaufrette/Adapter/OpenStackCloudFiles/OpenStackAuthenticationFactory.php +++ b/src/Gaufrette/Adapter/OpenStackCloudFiles/OpenStackAuthenticationFactory.php @@ -9,6 +9,7 @@ * Class OpenStackAuthenticationFactory * @package Gaufrette\Adapter\OpenStackCloudFiles * @author Chris Warner + * @deprecated in favor of ObjectStoreFactory */ class OpenStackAuthenticationFactory extends BaseOpenStackAuthenticationFactory implements ConnectionFactoryInterface { @@ -17,13 +18,9 @@ class OpenStackAuthenticationFactory extends BaseOpenStackAuthenticationFactory */ public function create() { - if (null === $this->authenciationService) { + if (!$this->authenciationService) { $this->authenciationService = new OpenStack($this->url, array ($this->username, $this->apikey)); - $this->authenciationService->authenticate(); - $this->authenciationService->setDefaults('cloudFiles', null, $this->region); - } elseif ($this->authenciationService->hasExpired()) { - $this->authenciationService->authenticate(); - $this->authenciationService->setDefaults('cloudFiles', null, $this->region); + $this->authenciationService->getUser()->setDefaultRegion($this->region); } return $this->authenciationService; diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationFactory.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationFactory.php index d7a6d5e88..3328c9d81 100644 --- a/src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationFactory.php +++ b/src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationFactory.php @@ -10,6 +10,7 @@ * Class RackspaceAuthenticationConnectionFactory * @package Gaufrette\Adapter\OpenStackCloudFiles * @author Chris Warner + * @deprecated in favor of OpenStackObjectStoreFactory */ class RackspaceAuthenticationFactory extends BaseOpenStackAuthenticationFactory implements ConnectionFactoryInterface { @@ -19,11 +20,9 @@ class RackspaceAuthenticationFactory extends BaseOpenStackAuthenticationFactory */ public function create() { - if (null === $this->authenciationService) { + if (!$this->authenciationService) { $this->authenciationService = new Rackspace($this->url, array ($this->username, $this->apikey)); - $this->authenciationService->authenticate(); - } elseif ($this->authenciationService->expired()) { - $this->authenciationService->authenticate(); + $this->authenciationService->getUser()->setDefaultRegion($this->region); } return $this->authenciationService; From 36b9bb38ce41c915c2960bfe0d9fe8d35a875765 Mon Sep 17 00:00:00 2001 From: Richtermeister Date: Tue, 25 Feb 2014 13:51:37 -0800 Subject: [PATCH 091/426] Removing deprecated Rackspace adapters/factories/specs --- composer.json | 20 -- composer.lock | 42 +-- .../Adapter/LazyOpenStackCloudFilesSpec.php | 51 ---- .../Adapter/LazyRackspaceCloudfilesSpec.php | 130 -------- .../AuthenticationConnectionFactorySpec.php | 38 --- .../Adapter/RackspaceCloudfilesSpec.php | 284 ------------------ .../Adapter/LazyOpenStackCloudFiles.php | 45 --- .../Adapter/LazyRackspaceCloudfiles.php | 129 -------- .../BaseOpenStackAuthenticationFactory.php | 58 ---- .../ConnectionFactoryInterface.php | 15 - .../OpenStackAuthenticationFactory.php | 28 -- .../RackspaceAuthenticationFactory.php | 30 -- src/Gaufrette/Adapter/RackspaceCloudfiles.php | 151 ---------- .../AuthenticationConnectionFactory.php | 28 -- .../ConnectionFactoryInterface.php | 11 - 15 files changed, 10 insertions(+), 1050 deletions(-) delete mode 100644 spec/Gaufrette/Adapter/LazyOpenStackCloudFilesSpec.php delete mode 100644 spec/Gaufrette/Adapter/LazyRackspaceCloudfilesSpec.php delete mode 100644 spec/Gaufrette/Adapter/RackspaceCloudfiles/AuthenticationConnectionFactorySpec.php delete mode 100644 spec/Gaufrette/Adapter/RackspaceCloudfilesSpec.php delete mode 100644 src/Gaufrette/Adapter/LazyOpenStackCloudFiles.php delete mode 100644 src/Gaufrette/Adapter/LazyRackspaceCloudfiles.php delete mode 100644 src/Gaufrette/Adapter/OpenStackCloudFiles/BaseOpenStackAuthenticationFactory.php delete mode 100644 src/Gaufrette/Adapter/OpenStackCloudFiles/ConnectionFactoryInterface.php delete mode 100644 src/Gaufrette/Adapter/OpenStackCloudFiles/OpenStackAuthenticationFactory.php delete mode 100644 src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationFactory.php delete mode 100644 src/Gaufrette/Adapter/RackspaceCloudfiles.php delete mode 100644 src/Gaufrette/Adapter/RackspaceCloudfiles/AuthenticationConnectionFactory.php delete mode 100644 src/Gaufrette/Adapter/RackspaceCloudfiles/ConnectionFactoryInterface.php diff --git a/composer.json b/composer.json index c4ffca675..05a5a4aae 100644 --- a/composer.json +++ b/composer.json @@ -17,25 +17,6 @@ } ], "repositories": [ - { - "type": "package", - "package": { - "name": "rackspace/php-cloudfiles", - "version": "master", - "source": { - "url": "git://github.com/rackerlabs/php-cloudfiles.git", - "type": "git", - "reference": "master" - }, - "dist": { - "url": "https://github.com/rackerlabs/php-cloudfiles/zipball/master", - "type": "zip" - }, - "autoload": { - "files": ["cloudfiles.php"] - } - } - }, { "type": "pear", "url": "http://pear.php.net" @@ -50,7 +31,6 @@ "rackspace/php-opencloud" : "1.9.*", "phpspec/phpspec": "2.0.*", "phpseclib/phpseclib": "dev-master", - "rackspace/php-cloudfiles": "*", "doctrine/dbal": ">=2.3", "dropbox-php/dropbox-php": "*", "herzult/php-ssh": "*", diff --git a/composer.lock b/composer.lock index ea1314d33..2251b0964 100644 --- a/composer.lock +++ b/composer.lock @@ -3,7 +3,7 @@ "This file locks the dependencies of your project to a known state", "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" ], - "hash": "25a293184541f0f60c2bce5160b4eefb", + "hash": "0ee4a7fa8b599a2c767f6bc4c38392d3", "packages": [ ], @@ -1446,12 +1446,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "e8ffe3fd66479fe502c4a566e9828075f3fffaec" + "reference": "69e55e68481cf708a6db43aff0b504e31402fe27" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/e8ffe3fd66479fe502c4a566e9828075f3fffaec", - "reference": "e8ffe3fd66479fe502c4a566e9828075f3fffaec", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/69e55e68481cf708a6db43aff0b504e31402fe27", + "reference": "69e55e68481cf708a6db43aff0b504e31402fe27", "shasum": "" }, "require": { @@ -1499,7 +1499,7 @@ "testing", "xunit" ], - "time": "2014-02-24 18:18:21" + "time": "2014-02-25 03:34:05" }, { "name": "phpunit/php-file-iterator", @@ -1690,12 +1690,12 @@ "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "5728eadfb7859241d7aafd26592cee0ad1876d2d" + "reference": "2752cbb9ea5bd84c2811b34b6953f76965ec7a2f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/5728eadfb7859241d7aafd26592cee0ad1876d2d", - "reference": "5728eadfb7859241d7aafd26592cee0ad1876d2d", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/2752cbb9ea5bd84c2811b34b6953f76965ec7a2f", + "reference": "2752cbb9ea5bd84c2811b34b6953f76965ec7a2f", "shasum": "" }, "require": { @@ -1712,7 +1712,7 @@ "symfony/yaml": "~2.0" }, "require-dev": { - "pear-pear/pear": "1.9.4" + "pear-pear.php.net/pear": "1.9.4" }, "suggest": { "ext-json": "*", @@ -1756,7 +1756,7 @@ "testing", "xunit" ], - "time": "2014-02-15 22:46:11" + "time": "2014-02-25 03:47:29" }, { "name": "phpunit/phpunit-mock-objects", @@ -1815,28 +1815,6 @@ ], "time": "2014-02-16 12:43:56" }, - { - "name": "rackspace/php-cloudfiles", - "version": "master", - "source": { - "type": "git", - "url": "git://github.com/rackerlabs/php-cloudfiles.git", - "reference": "master" - }, - "dist": { - "type": "zip", - "url": "https://github.com/rackerlabs/php-cloudfiles/zipball/master", - "reference": null, - "shasum": null - }, - "type": "library", - "autoload": { - "files": [ - "cloudfiles.php" - ] - }, - "time": "2014-01-07 21:27:14" - }, { "name": "rackspace/php-opencloud", "version": "v1.9.2", diff --git a/spec/Gaufrette/Adapter/LazyOpenStackCloudFilesSpec.php b/spec/Gaufrette/Adapter/LazyOpenStackCloudFilesSpec.php deleted file mode 100644 index b41b18a47..000000000 --- a/spec/Gaufrette/Adapter/LazyOpenStackCloudFilesSpec.php +++ /dev/null @@ -1,51 +0,0 @@ - - */ -class LazyOpenStackCloudFilesSpec extends ObjectBehavior -{ - - /** - * @param Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - */ - function let($connectionFactory) - { - $containerName = 'test'; - $createContainer = false; - - $this->beConstructedWith($connectionFactory, $containerName, $createContainer); - } - - function it_is_adapter() - { - $this->shouldHaveType('Gaufrette\Adapter'); - } - - /** - * @param \Gaufrette\Adapter\OpenStackCloudFiles\ConnectionFactoryInterface $connectionFactory - * @param \OpenCloud\OpenStack $openStack - * @param \OpenCloud\ObjectStore\Service $objectStore - * @param \OpenCloud\ObjectStore\Resource\Container $container - */ - function it_initializes_object_store($connectionFactory, $openStack, $objectStore, $container) - { - $connectionFactory->create()->shouldBeCalled()->willReturn($openStack); - $openStack->objectStoreService(Argument::cetera())->willReturn($objectStore); - $objectStore->getContainer(Argument::any())->shouldBeCalled()->willReturn($container); - $container->getObject(Argument::any())->willThrow(new ObjectNotFoundException()); - - $this->exists("test-file-name"); - } -} diff --git a/spec/Gaufrette/Adapter/LazyRackspaceCloudfilesSpec.php b/spec/Gaufrette/Adapter/LazyRackspaceCloudfilesSpec.php deleted file mode 100644 index 0d0451641..000000000 --- a/spec/Gaufrette/Adapter/LazyRackspaceCloudfilesSpec.php +++ /dev/null @@ -1,130 +0,0 @@ -create()->willReturn($connection); - $this->beConstructedWith($connectionFactory, 'my_container'); - } - - function it_is_rackspace_adapter() - { - $this->shouldHaveType('\Gaufrette\Adapter\RackspaceCloudfiles'); - } - - /** - * @param \CF_Container $container - * @param \CF_Object $object - */ - function it_lazily_fetches_container_before_read($connection, $container, $object) - { - $connection->get_container('my_container')->willReturn($container)->shouldBeCalledTimes(1); - $connection->create_container(Argument::cetera())->shouldNotBeCalled(); - $object->read()->willReturn('some content'); - $container->get_object('filename')->willReturn($object); - $container->get_object('filename1')->willReturn($object); - - $this->read('filename')->shouldReturn('some content'); - - //get_container() should not be called second time - $this->read('filename1')->shouldReturn('some content'); - } - - /** - * @param \CF_Container $container - * @param \CF_Object $object - */ - function it_lazily_creates_container_before_read($connectionFactory, $connection, $container, $object) - { - $this->beConstructedWith($connectionFactory, 'my_container', true); - - $connection->get_container(Argument::any())->shouldNotBeCalled(); - $connection->create_container('my_container')->willReturn($container)->shouldBeCalled(); - $object->read()->willReturn('some content'); - $container->get_object('filename')->willReturn($object); - - $this->read('filename')->shouldReturn('some content'); - } - - /** - * @param \CF_Container $container - * @param \CF_Object $object - */ - function it_lazily_fetches_container_before_write($connection, $container, $object) - { - $connection->get_container('my_container')->willReturn($container)->shouldBeCalled(); - $object - ->write('some content') - ->shouldBeCalled() - ->willReturn(true) - ; - $container - ->get_object('filename') - ->shouldBeCalled() - ->willReturn($object) - ; - - $this->write('filename', 'some content')->shouldReturn(12); - } - - /** - * @param \CF_Container $container - * @param \CF_Object $object - */ - function it_lazily_fetches_container_before_exists($connection, $container, $object) - { - $connection->get_container('my_container')->willReturn($container)->shouldBeCalled(); - $container - ->get_object('filename') - ->willReturn($object) - ; - - $this->exists('filename')->shouldReturn(true); - } - - /** - * @param \CF_Container $container - */ - function it_lazily_fetches_container_before_keys($connection, $container) - { - $connection->get_container('my_container')->willReturn($container)->shouldBeCalled(); - $container->list_objects(0, null, null)->willReturn(array('filename2', 'filename1')); - - $this->keys()->shouldReturn(array('filename1', 'filename2')); - } - - /** - * @param \CF_Container $container - * @param \CF_Object $object - */ - function it_lazily_fetches_container_before_checksum($connection, $container, $object) - { - $connection->get_container('my_container')->willReturn($container)->shouldBeCalled(); - - $object->getETag()->willReturn('123m5'); - $container->get_object('filename')->willReturn($object); - - $this->checksum('filename')->shouldReturn('123m5'); - } - - /** - * @param \CF_Container $container - */ - function it_lazily_fetches_container_before_delete($connection, $container) - { - $connection->get_container('my_container')->willReturn($container)->shouldBeCalled(); - $container->delete_object('filename')->shouldBeCalled(); - - $this->delete('filename')->shouldReturn(true); - } -} diff --git a/spec/Gaufrette/Adapter/RackspaceCloudfiles/AuthenticationConnectionFactorySpec.php b/spec/Gaufrette/Adapter/RackspaceCloudfiles/AuthenticationConnectionFactorySpec.php deleted file mode 100644 index 47549fb6b..000000000 --- a/spec/Gaufrette/Adapter/RackspaceCloudfiles/AuthenticationConnectionFactorySpec.php +++ /dev/null @@ -1,38 +0,0 @@ -beConstructedWith($authentication); - } - - function it_is_initializable() - { - $this->shouldHaveType('Gaufrette\Adapter\RackspaceCloudfiles\AuthenticationConnectionFactory'); - $this->shouldHaveType('Gaufrette\Adapter\RackspaceCloudfiles\ConnectionFactoryInterface'); - } - - function it_creates_cf_connection($authentication) - { - $authentication->authenticated()->willReturn(true); - $this->create()->shouldReturnAnInstanceOf('\CF_Connection'); - } - - function it_authenticates_when_was_not_authenticated_before($authentication) - { - $authentication->authenticated()->willReturn(false); - $authentication->authenticate()->shouldBeCalled()->will(function () use ($authentication) { - $authentication->authenticated()->willReturn(true); - }); - - $this->create()->shouldReturnAnInstanceOf('\CF_Connection'); - } -} diff --git a/spec/Gaufrette/Adapter/RackspaceCloudfilesSpec.php b/spec/Gaufrette/Adapter/RackspaceCloudfilesSpec.php deleted file mode 100644 index 4702113b4..000000000 --- a/spec/Gaufrette/Adapter/RackspaceCloudfilesSpec.php +++ /dev/null @@ -1,284 +0,0 @@ -beConstructedWith($container); - } - - function it_is_adapter() - { - $this->shouldHaveType('Gaufrette\Adapter'); - } - - function it_is_checksum_calculator() - { - $this->shouldHaveType('Gaufrette\Adapter\ChecksumCalculator'); - } - - /** - * @param \CF_Container $container - * @param \CF_Object $object - */ - function it_reads_file($container, $object) - { - $object->read()->willReturn('some content'); - $container->get_object('filename')->willReturn($object); - - $this->read('filename')->shouldReturn('some content'); - } - - /** - * @param \CF_Container $container - * @param \CF_Object $object - */ - function it_does_not_mask_exception_when_read($container, $object) - { - $object->read()->willThrow(new \RuntimeException('read')); - $container->get_object('filename')->willReturn($object); - - $this->shouldThrow(new \RuntimeException('read'))->duringRead('filename'); - } - - /** - * @param \CF_Container $container - * @param \CF_Object $object - */ - function it_does_check_if_file_exists($container, $object) - { - $container - ->get_object('filename') - ->willReturn($object) - ; - $container - ->get_object('filename2') - ->willReturn(false) - ; - $container - ->get_object('filename3') - ->willThrow(new \NoSuchObjectException) - ; - - $this->exists('filename')->shouldReturn(true); - $this->exists('filename2')->shouldReturn(false); - $this->exists('filename3')->shouldReturn(false); - } - - /** - * @param \CF_Container $container - * @param \CF_Object $object - */ - function it_does_not_mask_exception_when_check_if_exists($container, $object) - { - $container->get_object('filename')->willThrow(new \RuntimeException('exists')); - - $this->shouldThrow(new \RuntimeException('exists'))->duringRead('filename'); - } - - /** - * @param \CF_Container $container - * @param \CF_Object $object - */ - function it_should_write_file($container, $object) - { - $object - ->write('some content') - ->shouldBeCalled() - ->willReturn(true) - ; - $container - ->get_object('filename') - ->shouldBeCalled() - ->willReturn($object) - ; - - $this->write('filename', 'some content')->shouldReturn(12); - } - - /** - * @param \CF_Container $container - * @param \CF_Object $object - */ - function it_does_not_write_file($container, $object) - { - $object - ->write('some content') - ->shouldBeCalled() - ->willReturn(false) - ; - $container - ->get_object('filename') - ->shouldBeCalled() - ->willReturn($object) - ; - - $this->write('filename', 'some content')->shouldReturn(false); - } - - /** - * @param \CF_Container $container - */ - function it_does_not_mask_exception_when_write($container) - { - $container->get_object('filename')->willThrow(new \RuntimeException('write')); - - $this->shouldThrow(new \RuntimeException('write'))->duringWrite('filename', 'some content'); - } - - /** - * @param \CF_Container $container - * @param \CF_Object $object - */ - function it_creates_object_when_write($container, $object) - { - $object - ->write('some content') - ->shouldBeCalled() - ->willReturn(true) - ; - $container - ->get_object('filename') - ->shouldBeCalled() - ->willReturn(false) - ; - $container - ->create_object('filename') - ->shouldBeCalled() - ->willReturn($object) - ; - - $this->write('filename', 'some content')->shouldReturn(12); - } - - /** - * @param \CF_Container $container - * @param \CF_Object $fromObject - * @param \CF_Object $toObject - */ - function it_renames_file($container, $fromObject, $toObject) - { - $fromObject - ->read() - ->willReturn('some content') - ; - $toObject - ->write('some content') - ->willReturn(true) - ; - $container - ->delete_object('filename') - ->shouldBeCalled() - ; - $container - ->get_object('filename') - ->willReturn($fromObject) - ; - $container - ->get_object('filename1') - ->willReturn($toObject) - ; - - $this->rename('filename', 'filename1')->shouldReturn(true); - } - - /** - * @param \CF_Container $container - */ - function it_does_not_mask_exception_when_rename($container) - { - $container->get_object('filename')->willThrow(new \RuntimeException('rename')); - - $this->shouldThrow(new \RuntimeException('rename'))->duringRename('filename', 'fromFilename'); - } - - /** - * @param \CF_Container $container - */ - function it_fetches_keys($container) - { - $container->list_objects(0, null, null)->willReturn(array('filename2', 'filename1')); - - $this->keys()->shouldReturn(array('filename1', 'filename2')); - } - - /** - * @param \CF_Container $container - */ - function it_does_not_mask_exception_when_get_keys($container) - { - $container->list_objects(0, null, null)->willThrow(new \RuntimeException('keys')); - - $this->shouldThrow(new \RuntimeException('keys'))->duringKeys(); - } - - function it_does_not_support_mtime() - { - $this->mtime('filename')->shouldBe(false); - $this->mtime('filename2')->shouldBe(false); - } - - /** - * @param \CF_Container $container - * @param \CF_Object $object - */ - function it_calculates_checksum($container, $object) - { - $object->getETag()->willReturn('123m5'); - $container->get_object('filename')->willReturn($object); - - $this->checksum('filename')->shouldReturn('123m5'); - } - - /** - * @param \CF_Container $container - */ - function it_does_not_mask_exception_when_calculate_checksum($container) - { - $container->get_object('filename')->willThrow(new \RuntimeException('checksum')); - - $this->shouldThrow(new \RuntimeException('checksum'))->duringChecksum('filename'); - } - - /** - * @param \CF_Container $container - */ - function it_deletes_file($container) - { - $container->delete_object('filename')->shouldBeCalled(); - - $this->delete('filename')->shouldReturn(true); - } - - /** - * @param \CF_Container $container - */ - function it_does_not_delete_file_if_not_found($container) - { - $container->delete_object('filename')->willThrow(new \NoSuchObjectException); - - $this->delete('filename')->shouldReturn(false); - } - - /** - * @param \CF_Container $container - */ - function it_does_not_mask_exception_when_delete($container) - { - $container->delete_object('filename')->willThrow(new \RuntimeException('delete')); - - $this->shouldThrow(new \RuntimeException('delete'))->duringDelete('filename'); - } - - function it_does_not_support_directory() - { - $this->isDirectory('filename')->shouldReturn(false); - } -} diff --git a/src/Gaufrette/Adapter/LazyOpenStackCloudFiles.php b/src/Gaufrette/Adapter/LazyOpenStackCloudFiles.php deleted file mode 100644 index 72e502cd7..000000000 --- a/src/Gaufrette/Adapter/LazyOpenStackCloudFiles.php +++ /dev/null @@ -1,45 +0,0 @@ - - * @deprecated in favor of LazyOpenCloud - */ -class LazyOpenStackCloudFiles extends OpenCloud -{ - /** - * @var ConnectionFactoryInterface - */ - protected $connectionFactory; - - /** - * @param ConnectionFactoryInterface $connectionFactory - * @param string $containerName - * @param bool $createContainer - */ - public function __construct($connectionFactory, $containerName, $createContainer = false) - { - $this->connectionFactory = $connectionFactory; - $this->containerName = $containerName; - $this->createContainer = $createContainer; - } - - /** - * Override parent to lazy-load object store - * - * {@inheritdoc} - */ - protected function getContainer() - { - if (!$this->objectStore) { - $this->objectStore = $this->connectionFactory->create()->objectStoreService('cloudFiles', null, 'publicURL'); - } - - return parent::getContainer(); - } -} diff --git a/src/Gaufrette/Adapter/LazyRackspaceCloudfiles.php b/src/Gaufrette/Adapter/LazyRackspaceCloudfiles.php deleted file mode 100644 index fb7d81e72..000000000 --- a/src/Gaufrette/Adapter/LazyRackspaceCloudfiles.php +++ /dev/null @@ -1,129 +0,0 @@ - - * @author Leszek Prabucki - */ -class LazyRackspaceCloudfiles extends RackspaceCloudfiles -{ - /** - * @var RackspaceCloudfiles\ConnectionFactoryInterface $authentication - */ - protected $connectionFactory; - - /** - * @var string $containerName - */ - protected $containerName; - - /** - * @var bool $createContainer - */ - protected $createContainer; - - /** - * @var bool $initialized - */ - protected $initialized = false; - - /** - * Constructor. - * Creates a new Rackspace adapter starting from a rackspace connection factory - * - * @param RackspaceCloudfiles\ConnectionFactoryInterface $connectionFactory - * @param string $containerName - * @param bool $createContainer if true will try to create the container if not - * existent. Default false - */ - public function __construct(RackspaceCloudfiles\ConnectionFactoryInterface $connectionFactory, $containerName, $createContainer = false) - { - $this->connectionFactory = $connectionFactory; - $this->containerName = $containerName; - $this->createContainer = $createContainer; - } - - /** - * {@inheritDoc} - */ - public function read($key) - { - $this->initialize(); - - return parent::read($key); - } - - /** - * {@inheritDoc} - */ - public function write($key, $content, array $metadata = null) - { - $this->initialize(); - - return parent::write($key, $content, $metadata); - } - - /** - * {@inheritDoc} - */ - public function exists($key) - { - $this->initialize(); - - return parent::exists($key); - } - - /** - * {@inheritDoc} - */ - public function keys() - { - $this->initialize(); - - return parent::keys(); - } - - /** - * {@inheritDoc} - */ - public function checksum($key) - { - $this->initialize(); - - return parent::checksum($key); - } - - /** - * {@inheritDoc} - */ - public function delete($key) - { - $this->initialize(); - - return parent::delete($key); - } - - /** - * Initializes the container - */ - protected function initialize() - { - if (!$this->initialized) { - $conn = $this->connectionFactory->create(); - - if ($this->createContainer) { - $this->container = $conn->create_container($this->containerName); - } else { - $this->container = $conn->get_container($this->containerName); - } - - $this->initialized = true; - } - } -} diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/BaseOpenStackAuthenticationFactory.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/BaseOpenStackAuthenticationFactory.php deleted file mode 100644 index 846db776e..000000000 --- a/src/Gaufrette/Adapter/OpenStackCloudFiles/BaseOpenStackAuthenticationFactory.php +++ /dev/null @@ -1,58 +0,0 @@ - - * @deprecated in favor of ObjectStoreFactory - */ -abstract class BaseOpenStackAuthenticationFactory implements ConnectionFactoryInterface -{ - - /** - * @var null|OpenStack|Rackspace - */ - protected $authenciationService = null; - /** - * @var string - */ - protected $url; - /** - * @var string - */ - protected $username; - /** - * @var null|string - */ - protected $tenant; - /** - * @var string - */ - protected $apikey; - /** - * @var string - */ - protected $region; - - /** - * @param string $url - * @param string $apikey - * @param string $username - * @param string $region - * @param null|string $tenant - */ - function __construct($url, $apikey, $username, $region, $tenant = null) - { - $this->apikey = $apikey; - $this->region = $region; - $this->tenant = $tenant; - $this->url = $url; - $this->username = $username; - } -} diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/ConnectionFactoryInterface.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/ConnectionFactoryInterface.php deleted file mode 100644 index f1b5d6f3b..000000000 --- a/src/Gaufrette/Adapter/OpenStackCloudFiles/ConnectionFactoryInterface.php +++ /dev/null @@ -1,15 +0,0 @@ - - * @deprecated in favor of ObjectStoreFactory - */ -interface ConnectionFactoryInterface -{ - public function create(); -} diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/OpenStackAuthenticationFactory.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/OpenStackAuthenticationFactory.php deleted file mode 100644 index 04745cbf5..000000000 --- a/src/Gaufrette/Adapter/OpenStackCloudFiles/OpenStackAuthenticationFactory.php +++ /dev/null @@ -1,28 +0,0 @@ - - * @deprecated in favor of ObjectStoreFactory - */ -class OpenStackAuthenticationFactory extends BaseOpenStackAuthenticationFactory implements ConnectionFactoryInterface -{ - /** - * @return OpenStack - */ - public function create() - { - if (!$this->authenciationService) { - $this->authenciationService = new OpenStack($this->url, array ($this->username, $this->apikey)); - $this->authenciationService->getUser()->setDefaultRegion($this->region); - } - - return $this->authenciationService; - } -} diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationFactory.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationFactory.php deleted file mode 100644 index 3328c9d81..000000000 --- a/src/Gaufrette/Adapter/OpenStackCloudFiles/RackspaceAuthenticationFactory.php +++ /dev/null @@ -1,30 +0,0 @@ - - * @deprecated in favor of OpenStackObjectStoreFactory - */ -class RackspaceAuthenticationFactory extends BaseOpenStackAuthenticationFactory implements ConnectionFactoryInterface -{ - - /** - * @return Rackspace - */ - public function create() - { - if (!$this->authenciationService) { - $this->authenciationService = new Rackspace($this->url, array ($this->username, $this->apikey)); - $this->authenciationService->getUser()->setDefaultRegion($this->region); - } - - return $this->authenciationService; - } -} diff --git a/src/Gaufrette/Adapter/RackspaceCloudfiles.php b/src/Gaufrette/Adapter/RackspaceCloudfiles.php deleted file mode 100644 index bd36a65a1..000000000 --- a/src/Gaufrette/Adapter/RackspaceCloudfiles.php +++ /dev/null @@ -1,151 +0,0 @@ - - * @deprecated php-cloudfiles is deprecated and will be unavailable after August 1, 2013 - */ -class RackspaceCloudfiles implements Adapter, - ChecksumCalculator -{ - protected $container; - - /** - * Constructor - * - * @param RackspaceContainer $container A CF_Container instance - */ - public function __construct(RackspaceContainer $container) - { - $this->container = $container; - } - - /** - * {@inheritDoc} - * - * @throws \InvalidResponseException - */ - public function read($key) - { - $object = $this->container->get_object($key); - - return $object->read(); - } - - /** - * {@inheritDoc} - */ - public function rename($key, $new) - { - $this->write($new, $this->read($key)); - $this->delete($key); - - return true; - } - - /** - * {@inheritDoc} - */ - public function write($key, $content, array $metadata = null) - { - $object = $this->tryGetObject($key); - if (false === $object) { - // the object does not exist, so we create it - $object = $this->container->create_object($key); - } - - if (!$object->write($content)) { - return false; - } - - return Util\Size::fromContent($content); - } - - /** - * {@inheritDoc} - */ - public function exists($key) - { - return false !== $this->tryGetObject($key); - } - - /** - * {@inheritDoc} - */ - public function keys() - { - $keys = $this->container->list_objects(0, null, null); - sort($keys); - - return $keys; - } - - /** - * {@inheritDoc} - */ - public function mtime($key) - { - return false; - } - - /** - * {@inheritDoc} - */ - public function checksum($key) - { - $object = $this->container->get_object($key); - - return $object->getETag(); - } - - /** - * {@inheritDoc} - * - * @throws InvalidResponseException - * @throws SyntaxException - */ - public function delete($key) - { - try { - $this->container->delete_object($key); - } catch (\NoSuchObjectException $e) { - return false; - } - - return true; - } - - /** - * {@inheritDoc} - */ - public function isDirectory($key) - { - return false; - } - - /** - * Tries to get the object for the specified key or return false - * - * @param string $key The key of the object - * - * @return CF_Object or FALSE if the object does not exist - */ - protected function tryGetObject($key) - { - try { - return $this->container->get_object($key); - } catch (\NoSuchObjectException $e) { - // the NoSuchObjectException is thrown by the CF_Object during it's - // creation if the object doesn't exist - return false; - } - } -} diff --git a/src/Gaufrette/Adapter/RackspaceCloudfiles/AuthenticationConnectionFactory.php b/src/Gaufrette/Adapter/RackspaceCloudfiles/AuthenticationConnectionFactory.php deleted file mode 100644 index 6d2b796c3..000000000 --- a/src/Gaufrette/Adapter/RackspaceCloudfiles/AuthenticationConnectionFactory.php +++ /dev/null @@ -1,28 +0,0 @@ -authentication = $authentication; - } - - /** - * {@inheritDoc} - */ - public function create() - { - if (!$this->authentication->authenticated()) { - $this->authentication->authenticate(); - } - - return new CF_Connection($this->authentication); - } -} diff --git a/src/Gaufrette/Adapter/RackspaceCloudfiles/ConnectionFactoryInterface.php b/src/Gaufrette/Adapter/RackspaceCloudfiles/ConnectionFactoryInterface.php deleted file mode 100644 index ef09e6348..000000000 --- a/src/Gaufrette/Adapter/RackspaceCloudfiles/ConnectionFactoryInterface.php +++ /dev/null @@ -1,11 +0,0 @@ - Date: Tue, 4 Mar 2014 12:56:43 +0100 Subject: [PATCH 092/426] Allows to creates file even when subdirectory does not exists. Add this behavior to LocalStream cause InMemoryBuffer working like this before. --- src/Gaufrette/Stream/Local.php | 4 ++++ .../Functional/FileStream/FunctionalTestCase.php | 13 +++++++++++++ 2 files changed, 17 insertions(+) diff --git a/src/Gaufrette/Stream/Local.php b/src/Gaufrette/Stream/Local.php index 45177fba8..6ed17833b 100644 --- a/src/Gaufrette/Stream/Local.php +++ b/src/Gaufrette/Stream/Local.php @@ -31,6 +31,10 @@ public function __construct($path) */ public function open(StreamMode $mode) { + $baseDirPath = dirname($this->path); + if ($mode->allowsWrite() && !is_dir($baseDirPath)) { + @mkdir($baseDirPath, 0755, true); + } try { $fileHandle = @fopen($this->path, $mode->getMode()); } catch (\Exception $e) { diff --git a/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php b/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php index 4c1fc5e20..8c8005ade 100644 --- a/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php +++ b/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php @@ -150,6 +150,19 @@ public function shouldNotSeekWhenWhenceParameterIsInvalid() $this->assertEquals(-1, fseek($fileHandler, 1, 666)); } + /** + * @test + */ + public function shouldHandlesSubDir() + { + file_put_contents('gaufrette://filestream/subdir/test.txt', 'test content'); + + $this->assertTrue(is_file('gaufrette://filestream/subdir/test.txt')); + + $this->filesystem->delete('subdir/test.txt'); + $this->assertFalse(is_file('gaufrette://filestream/subdir/test.txt')); + } + /** * @test */ From 139b2ca0a37d70ddbed3f205972a190fa1410de9 Mon Sep 17 00:00:00 2001 From: Julius Beckmann Date: Tue, 4 Mar 2014 15:38:44 +0100 Subject: [PATCH 093/426] Added PHP 5.5 and 5.6 to travis. --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index 9f3951441..9648013c0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,6 +3,8 @@ language: php php: - 5.3 - 5.4 + - 5.5 + - 5.6 before_script: - ./bin/configure_test_env.sh From a6b77e22809ca0d4ec5a47c718c38dfab4b8aff3 Mon Sep 17 00:00:00 2001 From: Richtermeister Date: Wed, 5 Mar 2014 18:20:30 -0800 Subject: [PATCH 094/426] Update RS-related readme for new library version. --- README.markdown | 27 ++++++++++----------------- 1 file changed, 10 insertions(+), 17 deletions(-) diff --git a/README.markdown b/README.markdown index a357d603d..372748934 100644 --- a/README.markdown +++ b/README.markdown @@ -150,7 +150,7 @@ $connection = new OpenCloud\OpenStack( 'tenantName' => 'your tenant (project) name' )); -$objectStore = $connection->ObjectStore('cloudFiles', 'LON', 'publicURL'); +$objectStore = $connection->objectStoreService('cloudFiles', 'LON', 'publicURL'); $adapter = new Gaufrette\Adapter\OpenCloud( $objectStore, @@ -177,24 +177,17 @@ $connection = new OpenCloud\Rackspace( ### LazyOpenCloud -```php -$factory = new Gaufrette\Adapter\OpenStackAuthenticationFactory( - $url, // connection URL from cloud Vendor - $apikey, // password - $username, //username - $region, // region - $tenant) // tenant -$adapter = new Gaufrette\Adapter\LazyOpenStackCloudFiles($factory, 'container-name'); -``` +Instantiating the OpenCloud object store service has some overhead because it issues an authentication request, +even if you end up not using the filesystem. For better performance you can use a lazy-loading adapter which only authenticates when needed. -### LazyOpenCloud on Rackspace ```php -$factory = new Gaufrette\Adapter\RackspaceAuthenticationFactory( - $url, // connection URL from cloud Vendor - $apikey, // password - $username, //username - $region, // region - $tenant) // tenant +// ... $connection from previous step, either OpenCloud\OpenStack or OpenCloud\Rackspace instance + +$factory = new Gaufrette\Adapter\OpenStackCloudFiles\ObjectStoreFactory($connection); +$adapter = new Gaufrette\Adapter\LazyOpenCloud($factory, 'container-name'); + +$filesystem = new Filesystem($adapter); + ``` Using AzureBlobStorage From 4b52d8675fa607dc7f88d7f97b84b11c8ed047f2 Mon Sep 17 00:00:00 2001 From: scasei Date: Mon, 17 Mar 2014 16:48:59 +0100 Subject: [PATCH 095/426] fix / windows path hassle, \\ -> / --- src/Gaufrette/Adapter/Ftp.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index d23d597ac..172537205 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -77,7 +77,7 @@ public function write($key, $content) $this->ensureDirectoryExists($this->directory, $this->create); $path = $this->computePath($key); - $directory = dirname($path); + $directory = str_replace('\\', '/', dirname($path)); $this->ensureDirectoryExists($directory, true); @@ -106,7 +106,7 @@ public function rename($sourceKey, $targetKey) $sourcePath = $this->computePath($sourceKey); $targetPath = $this->computePath($targetKey); - $this->ensureDirectoryExists(dirname($targetPath), true); + $this->ensureDirectoryExists(str_replace('\\', '/', dirname($targetPath)), true); return ftp_rename($this->getConnection(), $sourcePath, $targetPath); } @@ -119,7 +119,7 @@ public function exists($key) $this->ensureDirectoryExists($this->directory, $this->create); $file = $this->computePath($key); - $lines = ftp_rawlist($this->getConnection(), '-al ' . dirname($file)); + $lines = ftp_rawlist($this->getConnection(), '-al ' . str_replace('\\', '/', dirname($file))); if (false === $lines) { return false; @@ -274,7 +274,7 @@ public function createFile($key, Filesystem $filesystem) $file = new File($key, $filesystem); if (!array_key_exists($key, $this->fileData)) { - $directory = dirname($key) == '.' ? '' : dirname($key); + $directory = dirname($key) == '.' ? '' : str_replace('\\', '/', dirname($key)); $this->listDirectory($directory); } @@ -320,7 +320,7 @@ protected function ensureDirectoryExists($directory, $create = false) protected function createDirectory($directory) { // create parent directory if needed - $parent = dirname($directory); + $parent = str_replace('\\', '/', dirname($directory)); if (!$this->isDir($parent)) { $this->createDirectory($parent); } From 386372edf5fbd4d5fb9328aa0916b453443cb9ef Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Fri, 2 May 2014 18:16:32 +0200 Subject: [PATCH 096/426] Fixed the return value of fetchKeys in case of errors --- src/Gaufrette/Adapter/Ftp.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index d23d597ac..88b9c7e37 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -359,7 +359,7 @@ private function fetchKeys($directory = '', $onlyKeys = true) $lines = ftp_rawlist($this->getConnection(), '-alR '. $this->directory . $directory); if (false === $lines) { - return array(); + return array('keys' => array(), 'dirs' => array()); } $regexDir = '/'.preg_quote($this->directory . $directory, '/').'\/?(.+):$/u'; From 0984932c36485b0d1fa8814d8c1aa21a094e776e Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Tue, 6 May 2014 01:10:35 +0200 Subject: [PATCH 097/426] Fixed the error handling for invalid ftp login The ftp_login function throws a PHP warning on failure as well as returning false --- src/Gaufrette/Adapter/Ftp.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index d23d597ac..6a933be8d 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -511,7 +511,7 @@ private function connect() $password = $this->password ? : ''; // login ftp user - if (!ftp_login($this->connection, $username, $password)) { + if (!@ftp_login($this->connection, $username, $password)) { $this->close(); throw new \RuntimeException(sprintf('Could not login as %s.', $username)); } From b0fb65f0a584291287261de4dd77822ea5bb520d Mon Sep 17 00:00:00 2001 From: Christophe Coevoet Date: Thu, 15 May 2014 16:46:05 +0200 Subject: [PATCH 098/426] Use the precompiled mongo extension on Travis --- bin/configure_test_env.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/configure_test_env.sh b/bin/configure_test_env.sh index d199aef11..03e46b72a 100755 --- a/bin/configure_test_env.sh +++ b/bin/configure_test_env.sh @@ -2,7 +2,7 @@ sudo apt-get update -qq sudo apt-get install -qq libssh2-1-dev libssh2-php -pecl install -f mongo +echo "extension = mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini touch .interactive (pecl install -f ssh2 < .interactive) From 64ac9aefe0811ebcc90d3545c01113889c65c98b Mon Sep 17 00:00:00 2001 From: Leszek Prabucki Date: Tue, 3 Jun 2014 10:00:52 +0200 Subject: [PATCH 099/426] Update composer.json Ref #276 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 05a5a4aae..bb778e6d6 100644 --- a/composer.json +++ b/composer.json @@ -43,7 +43,7 @@ "dropbox-php/dropbox-php": "to use the Dropbox adapter", "rackspace/php-opencloud" : "to use Opencloud adapter", "herzult/php-ssh": "to use SFtp adapter", - "phpseclib/phpseclib": "to user PhpseclibSftp adapter", + "phpseclib/phpseclib": "to use PhpseclibSftp adapter", "aws/aws-sdk-php": "to use the Amazon S3 adapter", "amazonwebservices/aws-sdk-for-php": "to use the legacy Amazon S3 adapters", "doctrine/dbal": "to use the Doctrine DBAL adapter", From 87ac6fb066703b3d7cb2b2e683fdc16cda654f58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gildas=20Qu=C3=A9m=C3=A9ner?= Date: Fri, 23 May 2014 11:26:34 +0200 Subject: [PATCH 100/426] Provide mime type through the local adapter --- spec/Gaufrette/Adapter/LocalSpec.php | 14 +++++++++++++ spec/Gaufrette/FilesystemSpec.php | 23 +++++++++++++++++++++- src/Gaufrette/Adapter/Local.php | 19 +++++++++++++++++- src/Gaufrette/Adapter/MimeTypeProvider.php | 20 +++++++++++++++++++ src/Gaufrette/Filesystem.php | 21 ++++++++++++++++++++ 5 files changed, 95 insertions(+), 2 deletions(-) create mode 100644 src/Gaufrette/Adapter/MimeTypeProvider.php diff --git a/spec/Gaufrette/Adapter/LocalSpec.php b/spec/Gaufrette/Adapter/LocalSpec.php index bd786a6c5..77bc3705e 100644 --- a/spec/Gaufrette/Adapter/LocalSpec.php +++ b/spec/Gaufrette/Adapter/LocalSpec.php @@ -24,6 +24,16 @@ function it_is_checksum_calculator() $this->shouldHaveType('Gaufrette\Adapter\ChecksumCalculator'); } + function it_is_a_mime_type_provider() + { + $this->shouldHaveType('Gaufrette\Adapter\MimeTypeProvider'); + } + + function it_gets_the_file_mime_type() + { + $this->mimeType('filename')->shouldReturn('text/plain'); + } + function it_is_stream_factory() { $this->shouldHaveType('Gaufrette\Adapter\StreamFactory'); @@ -136,6 +146,10 @@ function it_fails_when_directory_does_not_exists() ->shouldThrow(new \RuntimeException(sprintf('The directory "%s" does not exist.', vfsStream::url('other')))) ->duringChecksum('filename') ; + $this + ->shouldThrow(new \RuntimeException(sprintf('The directory "%s" does not exist.', vfsStream::url('other')))) + ->duringMimeType('filename') + ; } function it_creates_directory_when_does_not_exists() diff --git a/spec/Gaufrette/FilesystemSpec.php b/spec/Gaufrette/FilesystemSpec.php index 61d07b946..8751eed13 100644 --- a/spec/Gaufrette/FilesystemSpec.php +++ b/spec/Gaufrette/FilesystemSpec.php @@ -400,11 +400,32 @@ function it_delegates_checksum_calculation_to_adapter_when_adapter_is_checksum_c $this->checksum('filename')->shouldReturn(12); } + + /** + * @param \spec\Gaufrette\Adapter $extendedAdapter + */ + function it_delegates_mime_type_resolution_to_adapter_when_adapter_is_mime_type_provider($extendedAdapter) + { + $this->beConstructedWith($extendedAdapter); + $extendedAdapter->exists('filename')->willReturn(true); + $extendedAdapter->mimeType('filename')->willReturn('text/plain'); + + $this->mimeType('filename')->shouldReturn('text/plain'); + } + + function it_cannot_resolve_mime_type_if_the_adapter_cannot_provide_it($adapter) + { + $adapter->exists('filename')->willReturn(true); + $this + ->shouldThrow(new \LogicException(sprintf('Adapter "%s" cannot provide MIME type', get_class($adapter->getWrappedObject())))) + ->duringMimeType('filename'); + } } interface Adapter extends \Gaufrette\Adapter, \Gaufrette\Adapter\FileFactory, \Gaufrette\Adapter\StreamFactory, \Gaufrette\Adapter\ChecksumCalculator, - \Gaufrette\Adapter\MetadataSupporter + \Gaufrette\Adapter\MetadataSupporter, + \Gaufrette\Adapter\MimeTypeProvider {} diff --git a/src/Gaufrette/Adapter/Local.php b/src/Gaufrette/Adapter/Local.php index e171576a3..3210f1b04 100644 --- a/src/Gaufrette/Adapter/Local.php +++ b/src/Gaufrette/Adapter/Local.php @@ -17,7 +17,8 @@ class Local implements Adapter, StreamFactory, ChecksumCalculator, - SizeCalculator + SizeCalculator, + MimeTypeProvider { protected $directory; private $create; @@ -151,16 +152,32 @@ public function createStream($key) return new Stream\Local($this->computePath($key)); } + /** + * {@inheritdoc} + */ public function checksum($key) { return Util\Checksum::fromFile($this->computePath($key)); } + /** + * {@inheritdoc} + */ public function size($key) { return Util\Size::fromFile($this->computePath($key)); } + /** + * {@inheritdoc} + */ + public function mimeType($key) + { + $fileInfo = new \finfo(FILEINFO_MIME_TYPE); + + return $fileInfo->file($this->computePath($key)); + } + /** * Computes the key from the specified path * diff --git a/src/Gaufrette/Adapter/MimeTypeProvider.php b/src/Gaufrette/Adapter/MimeTypeProvider.php new file mode 100644 index 000000000..1fe1aed56 --- /dev/null +++ b/src/Gaufrette/Adapter/MimeTypeProvider.php @@ -0,0 +1,20 @@ + + */ +interface MimeTypeProvider +{ + /** + * Returns the mime type of the specified key + * + * @param string $key + * + * @return string + */ + public function mimeType($key); +} diff --git a/src/Gaufrette/Filesystem.php b/src/Gaufrette/Filesystem.php index 8559f2257..9466542f2 100644 --- a/src/Gaufrette/Filesystem.php +++ b/src/Gaufrette/Filesystem.php @@ -276,6 +276,27 @@ public function createFile($key) return new File($key, $this); } + /** + * Get the mime type of the provided key + * + * @param string $key + * + * @return string + */ + public function mimeType($key) + { + $this->assertHasFile($key); + + if ($this->adapter instanceof Adapter\MimeTypeProvider) { + return $this->adapter->mimeType($key); + } + + throw new \LogicException(sprintf( + 'Adapter "%s" cannot provide MIME type', + get_class($this->adapter) + )); + } + /** * Checks if matching file by given key exists in the filesystem * From 897925b6b95725cae614e2a1fa2e0c734f546601 Mon Sep 17 00:00:00 2001 From: Nikolai Zujev Date: Sat, 7 Jun 2014 00:45:18 +0200 Subject: [PATCH 101/426] check if source is written/renamed correctly --- spec/Gaufrette/Adapter/CacheSpec.php | 4 ++-- src/Gaufrette/Adapter/Cache.php | 18 +++++++++++++----- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/spec/Gaufrette/Adapter/CacheSpec.php b/spec/Gaufrette/Adapter/CacheSpec.php index 01873696c..cda8c1a17 100644 --- a/spec/Gaufrette/Adapter/CacheSpec.php +++ b/spec/Gaufrette/Adapter/CacheSpec.php @@ -87,7 +87,7 @@ function it_update_cache_adapter_when_source_file_is_modified($source, $cache) */ function it_rename_file_in_source_and_cache($source, $cache) { - $source->rename('filename', 'filename2')->shouldBeCalled(); + $source->rename('filename', 'filename2')->shouldBeCalled()->willReturn(true); $cache->rename('filename', 'filename2')->shouldBeCalled()->willReturn(true); $this->rename('filename', 'filename2')->shouldReturn(true); @@ -99,7 +99,7 @@ function it_rename_file_in_source_and_cache($source, $cache) */ function it_writes_file_to_source_and_cache($source, $cache) { - $source->write('filename', 'some content')->shouldBeCalled(); + $source->write('filename', 'some content')->shouldBeCalled()->willReturn(12); $cache->write('filename', 'some content')->shouldBeCalled()->willReturn(12); $this->write('filename', 'some content')->shouldReturn(12); diff --git a/src/Gaufrette/Adapter/Cache.php b/src/Gaufrette/Adapter/Cache.php index 2a3412bcc..428f23a19 100644 --- a/src/Gaufrette/Adapter/Cache.php +++ b/src/Gaufrette/Adapter/Cache.php @@ -95,9 +95,7 @@ public function read($key) */ public function rename($key, $new) { - $this->source->rename($key, $new); - - return $this->cache->rename($key, $new); + return $this->source->rename($key, $new) && $this->cache->rename($key, $new); } /** @@ -105,9 +103,19 @@ public function rename($key, $new) */ public function write($key, $content, array $metadata = null) { - $this->source->write($key, $content); + $bytesSource = $this->source->write($key, $content); + + if (false === $bytesSource) { + return false; + } + + $bytesCache = $this->cache->write($key, $content); + + if ($bytesSource !== $bytesCache) { + return false; + } - return $this->cache->write($key, $content); + return $bytesSource; } /** From c03f8d130696ff40272a6d6a8496f6a9b97bb71b Mon Sep 17 00:00:00 2001 From: "Making GitHub Delicious." Date: Tue, 24 Jun 2014 02:31:24 -0600 Subject: [PATCH 102/426] add waffle.io badge --- README.markdown | 1 + 1 file changed, 1 insertion(+) diff --git a/README.markdown b/README.markdown index 372748934..12190930b 100644 --- a/README.markdown +++ b/README.markdown @@ -1,3 +1,4 @@ +[![Stories in Ready](https://badge.waffle.io/knplabs/gaufrette.png?label=ready&title=Ready)](https://waffle.io/knplabs/gaufrette) Gaufrette ========= From 05f0497ac33681f1d7af0630f23aff2a421c5fd6 Mon Sep 17 00:00:00 2001 From: choojoy-work Date: Tue, 1 Jul 2014 21:44:27 +0400 Subject: [PATCH 103/426] Cache::exists performance fix --- src/Gaufrette/Adapter/Cache.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/Cache.php b/src/Gaufrette/Adapter/Cache.php index 2a3412bcc..fc196efbd 100644 --- a/src/Gaufrette/Adapter/Cache.php +++ b/src/Gaufrette/Adapter/Cache.php @@ -115,7 +115,11 @@ public function write($key, $content, array $metadata = null) */ public function exists($key) { - return $this->source->exists($key); + if ($this->needsReload($key)) { + return $this->source->exists($key); + } else { + return $this->cache->exists($key); + } } /** From b5d9ebc6fbfd3e0288a6e67901243f4231575d78 Mon Sep 17 00:00:00 2001 From: choojoy-work Date: Wed, 2 Jul 2014 17:32:41 +0400 Subject: [PATCH 104/426] Code formatting --- src/Gaufrette/Adapter/Cache.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Gaufrette/Adapter/Cache.php b/src/Gaufrette/Adapter/Cache.php index fc196efbd..57cc2f0bd 100644 --- a/src/Gaufrette/Adapter/Cache.php +++ b/src/Gaufrette/Adapter/Cache.php @@ -117,9 +117,8 @@ public function exists($key) { if ($this->needsReload($key)) { return $this->source->exists($key); - } else { - return $this->cache->exists($key); } + return $this->cache->exists($key); } /** From c21a62816f2d654b42f36daca12a2070e944729b Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Wed, 9 Jul 2014 12:06:33 +0200 Subject: [PATCH 105/426] Added HHVM to travis.yml --- .travis.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.travis.yml b/.travis.yml index 9648013c0..6c2c72640 100644 --- a/.travis.yml +++ b/.travis.yml @@ -5,6 +5,11 @@ php: - 5.4 - 5.5 - 5.6 + - hhvm + +matrix: + allow_failures: + - php: hhvm before_script: - ./bin/configure_test_env.sh From 7b96cd78f54bf2a0dc90e56d60bc2e39b8cfec67 Mon Sep 17 00:00:00 2001 From: Thomas Tourlourat Date: Fri, 18 Jul 2014 12:23:04 +0200 Subject: [PATCH 106/426] Be more realistic Not really needed, but it's more about philosophic ;) --- spec/Gaufrette/Adapter/functions.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spec/Gaufrette/Adapter/functions.php b/spec/Gaufrette/Adapter/functions.php index 166205fcc..2aee4371e 100644 --- a/spec/Gaufrette/Adapter/functions.php +++ b/spec/Gaufrette/Adapter/functions.php @@ -76,7 +76,7 @@ function ftp_mkdir($connection, $dirname) return false; } -function ftp_connect($host, $password) +function ftp_connect($host, $port = 21, $timeout = 90) { if ('localhost' !== $host) { return false; From 3715933116ce1ecb53e7f817dd6e33c8f2fdf310 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Tue, 23 Sep 2014 11:35:33 +0200 Subject: [PATCH 107/426] unified spelling --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 12190930b..3fd54cc31 100644 --- a/README.markdown +++ b/README.markdown @@ -251,7 +251,7 @@ In your Symfony2 project, add to ``deps``: # if you want to use Amazon S3 [aws-sdk] - git=https://github.com/aws/aws-sdk-php + git=https://github.com/aws/aws-sdk-php.git ``` And then, you can simply add them as services of your dependency injection container. From 3f8734f126231969efe536d359d6f859bfe22d24 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Tue, 23 Sep 2014 11:36:03 +0200 Subject: [PATCH 108/426] emphasize cli commands --- README.markdown | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.markdown b/README.markdown index 3fd54cc31..8c597c2ec 100644 --- a/README.markdown +++ b/README.markdown @@ -316,9 +316,11 @@ It will avoid skip a lot of tests. In the Gaufrette root directory: To check if classes specification pass: + $ php bin/phpspec run To check basic functionality of the adapters (adapters should be configured you will see many skipped tests): + $ phpunit Is it green? From 95db7b20e3a89c04f97a20c7a69783a6fd0d285c Mon Sep 17 00:00:00 2001 From: Stan Lemon Date: Sat, 25 Oct 2014 01:01:45 -0400 Subject: [PATCH 109/426] Don't override the region by default, it's already sent on the client coming in - only set it when explicitly specifying 'region' setting. --- spec/Gaufrette/Adapter/AmazonS3Spec.php | 28 ++----------------------- src/Gaufrette/Adapter/AmazonS3.php | 2 +- 2 files changed, 3 insertions(+), 27 deletions(-) diff --git a/spec/Gaufrette/Adapter/AmazonS3Spec.php b/spec/Gaufrette/Adapter/AmazonS3Spec.php index ef108c38b..d81a5c754 100644 --- a/spec/Gaufrette/Adapter/AmazonS3Spec.php +++ b/spec/Gaufrette/Adapter/AmazonS3Spec.php @@ -51,7 +51,6 @@ function it_reads_file($service) ->shouldBeCalled() ->willReturn(new \CFResponse('header', 'some content', 200)) ; - $service->set_region(Argument::any())->shouldBeCalled(); $this->setMetadata('filename', $options); $this->read('filename')->shouldReturn('some content'); @@ -76,7 +75,6 @@ function it_returns_false_when_cannot_read($service) ->shouldBeCalled() ->willReturn(new \CFResponse('header', 'some content', 500)) ; - $service->set_region(Argument::any())->shouldBeCalled(); $this->read('filename')->shouldReturn(false); } @@ -99,7 +97,6 @@ function it_is_verbose_and_throws_exceptions_when_read($service) ) ->willThrow(new \RuntimeException('read')) ; - $service->set_region(Argument::any())->shouldBeCalled(); $this->shouldThrow(new \RuntimeException('read'))->duringRead('filename'); } @@ -114,7 +111,6 @@ function it_rename_file($service) ->shouldBeCalled() ->willReturn(true) ; - $service->set_region(Argument::any())->shouldBeCalled(); $service ->copy_object( array( @@ -149,7 +145,6 @@ function it_rename_file($service) */ function it_is_verbose_and_throws_exceptions_when_rename($service) { - $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() @@ -168,7 +163,6 @@ function it_is_verbose_and_throws_exceptions_when_rename($service) */ function it_returns_false_when_cannot_rename($service) { - $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() @@ -198,7 +192,6 @@ function it_returns_false_when_cannot_rename($service) */ function it_should_write_file($service) { - $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() @@ -226,7 +219,6 @@ function it_should_write_file($service) */ function it_returns_false_when_cannot_write($service) { - $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() @@ -253,7 +245,6 @@ function it_returns_false_when_cannot_write($service) */ function it_is_verbose_and_throws_exceptions_when_write($service) { - $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() @@ -272,7 +263,6 @@ function it_is_verbose_and_throws_exceptions_when_write($service) */ function it_should_check_if_file_exists($service) { - $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() @@ -291,7 +281,6 @@ function it_should_check_if_file_exists($service) */ function it_is_verbose_and_throws_exceptions_when_file_exists($service) { - $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() @@ -310,7 +299,6 @@ function it_is_verbose_and_throws_exceptions_when_file_exists($service) */ function it_should_get_file_mtime($service) { - $service->set_region(Argument::any())->shouldBeCalled(); $metadata = array('acl' => \AmazonS3::ACL_PUBLIC); $service ->if_bucket_exists('bucketName') @@ -337,7 +325,6 @@ function it_should_get_file_mtime($service) */ function it_returns_false_when_cannot_fetch_mtime($service) { - $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() @@ -362,7 +349,6 @@ function it_returns_false_when_cannot_fetch_mtime($service) */ function it_is_verbose_and_throws_exceptions_when_fetch_mtime($service) { - $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() @@ -383,7 +369,6 @@ function it_should_delete_file($service) { $metadata = array('acl' => \AmazonS3::ACL_PRIVATE); - $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() @@ -407,7 +392,6 @@ function it_should_delete_file($service) */ function it_is_verbose_and_throws_exceptions_when_fetch_delete($service) { - $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->willReturn(true) @@ -429,7 +413,6 @@ function it_is_verbose_and_throws_exceptions_when_fetch_delete($service) */ function it_returns_false_when_cannot_delete($service) { - $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() @@ -452,7 +435,6 @@ function it_returns_false_when_cannot_delete($service) */ function it_should_get_keys($service) { - $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() @@ -472,7 +454,6 @@ function it_should_get_keys($service) */ function it_is_verbose_and_throws_exceptions_when_fetch_keys($service) { - $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->willReturn(true) @@ -490,7 +471,6 @@ function it_is_verbose_and_throws_exceptions_when_fetch_keys($service) */ function it_should_handle_dirs($service) { - $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->willReturn(true) @@ -518,7 +498,6 @@ function it_should_handle_dirs($service) */ function it_should_fail_when_bucket_does_not_exist($service) { - $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->willReturn(false) @@ -563,6 +542,7 @@ function it_creates_bucket_if_create_mode_is_enabled($service) ->if_bucket_exists('bucketName') ->willReturn(false) ; + $service->hostname = \AmazonS3::REGION_US_E1; $service ->create_bucket('bucketName', \AmazonS3::REGION_US_E1) ->shouldBeCalled() @@ -582,13 +562,12 @@ function it_creates_bucket_if_create_mode_is_enabled($service) */ function it_fails_when_cannot_create_bucket($service) { - $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->willReturn(false) ; $service - ->create_bucket('bucketName', \AmazonS3::REGION_US_E1) + ->create_bucket('bucketName', Argument::any()) ->shouldBeCalled() ->willReturn(new \CFResponse(array(), 'created', 500)) ; @@ -605,7 +584,6 @@ function it_fails_when_cannot_create_bucket($service) */ function it_allows_to_configure_reqion($service) { - $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->willReturn(true) @@ -653,7 +631,6 @@ function it_allows_to_configure_region_for_bucket($service) function it_allows_to_configure_acl($service) { $this->setAcl('123abc'); - $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() @@ -682,7 +659,6 @@ function it_allows_to_configure_acl($service) function its_file_metadata_acl_are_more_important_than_global_acl_config($service) { $this->setAcl('123abc'); - $service->set_region(Argument::any())->shouldBeCalled(); $service ->if_bucket_exists('bucketName') ->shouldBeCalled() diff --git a/src/Gaufrette/Adapter/AmazonS3.php b/src/Gaufrette/Adapter/AmazonS3.php index 2543cc01b..8843b08bb 100644 --- a/src/Gaufrette/Adapter/AmazonS3.php +++ b/src/Gaufrette/Adapter/AmazonS3.php @@ -28,7 +28,7 @@ public function __construct(AmazonClient $service, $bucket, $options = array()) $this->service = $service; $this->bucket = $bucket; $this->options = array_replace_recursive( - array('directory' => '', 'create' => false, 'region' => AmazonClient::REGION_US_E1, 'acl' => AmazonClient::ACL_PUBLIC), + array('directory' => '', 'create' => false, 'region' => $service->hostname, 'acl' => AmazonClient::ACL_PUBLIC), $options ); } From bce2ed7100554da3c8dd6dd92e5f6ecfbb8a31b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Blaznik?= Date: Mon, 27 Oct 2014 19:06:23 +0100 Subject: [PATCH 110/426] Wrong class parameter Gaufrette\Adapter\AmazonS3 constructor does not support S3Client but AwsS3 does. --- README.markdown | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.markdown b/README.markdown index 12190930b..0d2913f56 100644 --- a/README.markdown +++ b/README.markdown @@ -268,7 +268,7 @@ As an example, here is services declaration to use Amazon S3: - + %acme.s3.bucket_name% From c3fda1c6d453007bcc2b41e6d52e1613f3061e2e Mon Sep 17 00:00:00 2001 From: Patrik Karisch Date: Sat, 1 Nov 2014 00:35:53 +0100 Subject: [PATCH 111/426] Add support for Google Cloud Storage --- composer.json | 4 +- composer.lock | 48 ++- .../Adapter/GoogleCloudStorageSpec.php | 28 ++ src/Gaufrette/Adapter/GoogleCloudStorage.php | 354 ++++++++++++++++++ .../Adapter/GoogleCloudStorageTest.php | 53 +++ .../adapters/GoogleCloudStorage.php.dist | 23 ++ 6 files changed, 507 insertions(+), 3 deletions(-) create mode 100644 spec/Gaufrette/Adapter/GoogleCloudStorageSpec.php create mode 100644 src/Gaufrette/Adapter/GoogleCloudStorage.php create mode 100644 tests/Gaufrette/Functional/Adapter/GoogleCloudStorageTest.php create mode 100644 tests/Gaufrette/Functional/adapters/GoogleCloudStorage.php.dist diff --git a/composer.json b/composer.json index bb778e6d6..96f0ccd57 100644 --- a/composer.json +++ b/composer.json @@ -29,6 +29,7 @@ "aws/aws-sdk-php": "~2", "amazonwebservices/aws-sdk-for-php": "1.5.*", "rackspace/php-opencloud" : "1.9.*", + "google/apiclient": "~1.1", "phpspec/phpspec": "2.0.*", "phpseclib/phpseclib": "dev-master", "doctrine/dbal": ">=2.3", @@ -42,6 +43,7 @@ "knplabs/knp-gaufrette-bundle": "to use with Symfony2", "dropbox-php/dropbox-php": "to use the Dropbox adapter", "rackspace/php-opencloud" : "to use Opencloud adapter", + "google/apiclient": "to use GoogleCloudStorage adapter", "herzult/php-ssh": "to use SFtp adapter", "phpseclib/phpseclib": "to use PhpseclibSftp adapter", "aws/aws-sdk-php": "to use the Amazon S3 adapter", @@ -53,7 +55,7 @@ "ext-curl": "*", "ext-mbstring": "*", "ext-mongo": "*", - "ext-fileinfo": "This extension is used to automatically detect the content-type of a file in the AwsS3, OpenCloud, and AzureBlogStorage adapters" + "ext-fileinfo": "This extension is used to automatically detect the content-type of a file in the AwsS3, OpenCloud, AzureBlogStorage and GoogleCloudStorage adapters" }, "autoload": { "psr-0": { "Gaufrette": "src/" } diff --git a/composer.lock b/composer.lock index 2251b0964..bd57d21d8 100644 --- a/composer.lock +++ b/composer.lock @@ -1,9 +1,10 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" + "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", + "This file is @generated automatically" ], - "hash": "0ee4a7fa8b599a2c767f6bc4c38392d3", + "hash": "a1b734d6a5d534d72d1036a8eda13c01", "packages": [ ], @@ -672,6 +673,48 @@ ], "time": "2013-03-29 22:25:23" }, + { + "name": "google/apiclient", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/google/google-api-php-client.git", + "reference": "4793f71912b5ef6cdbc8dfc24ff27c1ae39b3ccf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/google/google-api-php-client/zipball/4793f71912b5ef6cdbc8dfc24ff27c1ae39b3ccf", + "reference": "4793f71912b5ef6cdbc8dfc24ff27c1ae39b3ccf", + "shasum": "" + }, + "require": { + "php": ">=5.2.1" + }, + "require-dev": { + "phpunit/phpunit": "3.7.*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "description": "Client library for Google APIs", + "homepage": "http://developers.google.com/api-client-library/php", + "keywords": [ + "google" + ], + "time": "2014-10-29 09:47:08" + }, { "name": "guzzle/guzzle", "version": "dev-master", @@ -2092,6 +2135,7 @@ "phpseclib/phpseclib": 20, "microsoft/windowsazure": 20 }, + "prefer-stable": false, "platform": { "php": ">=5.3.2" }, diff --git a/spec/Gaufrette/Adapter/GoogleCloudStorageSpec.php b/spec/Gaufrette/Adapter/GoogleCloudStorageSpec.php new file mode 100644 index 000000000..bb109e13a --- /dev/null +++ b/spec/Gaufrette/Adapter/GoogleCloudStorageSpec.php @@ -0,0 +1,28 @@ +beConstructedWith($service, 'bucketName'); + } + + public function it_is_adapter() + { + $this->shouldHaveType('Gaufrette\Adapter'); + } + + public function it_supports_metadata() + { + $this->shouldHaveType('Gaufrette\Adapter\MetadataSupporter'); + } + + public function it_is_list_keys_aware() + { + $this->shouldHaveType('Gaufrette\Adapter\ListKeysAware'); + } +} diff --git a/src/Gaufrette/Adapter/GoogleCloudStorage.php b/src/Gaufrette/Adapter/GoogleCloudStorage.php new file mode 100644 index 000000000..18ec758c1 --- /dev/null +++ b/src/Gaufrette/Adapter/GoogleCloudStorage.php @@ -0,0 +1,354 @@ + + */ +class GoogleCloudStorage implements Adapter, + MetadataSupporter, + ListKeysAware +{ + protected $service; + protected $bucket; + protected $options; + protected $bucketExists; + protected $metadata = array(); + protected $detectContentType; + + /** + * @param \Google_Service_Storage $service The storage service class with authenticated + * client and full access scope + * @param string $bucket The bucket name + * @param array $options Options can be directory and acl + * @param bool $detectContentType Whether to detect the content type or not + */ + public function __construct( + \Google_Service_Storage $service, + $bucket, + array $options = array(), + $detectContentType = false + ) { + $this->service = $service; + $this->bucket = $bucket; + $this->options = array_replace( + array( + 'directory' => '', + 'acl' => 'private', + ), + $options + ); + + $this->detectContentType = $detectContentType; + } + + /** + * @return array The actual options + */ + public function getOptions() + { + return $this->options; + } + + /** + * @param array $options The new options + */ + public function setOptions($options) + { + $this->options = array_replace($this->options, $options); + } + + /** + * @return string The current bucket name + */ + public function getBucket() + { + return $this->bucket; + } + + /** + * Sets a new bucket name. + * + * @param string $bucket The new bucket name + */ + public function setBucket($bucket) + { + $this->bucket = $bucket; + } + + /** + * {@inheritdoc} + */ + public function read($key) + { + $this->ensureBucketExists(); + $path = $this->computePath($key); + + $object = $this->getObjectData($path); + if ($object === false) { + return false; + } + + $request = new \Google_Http_Request($object->getMediaLink()); + $this->service->getClient()->getAuth()->sign($request); + + $response = $this->service->getClient()->getIo()->executeRequest($request); + + if ($response[2] == 200) { + $this->setMetadata($key, $object->getMetadata()); + + return $response[0]; + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function write($key, $content) + { + $this->ensureBucketExists(); + $path = $this->computePath($key); + + $metadata = $this->getMetadata($key); + $options = array( + 'uploadType' => 'multipart', + 'data' => $content + ); + + /** + * If the ContentType was not already set in the metadata, then we autodetect + * it to prevent everything being served up as application/octet-stream. + */ + if (!isset($metadata['ContentType']) && $this->detectContentType) { + $finfo = new \finfo(FILEINFO_MIME_TYPE); + $options['mimeType'] = $finfo->buffer($content); + unset($metadata['ContentType']); + } elseif (isset($metadata['ContentType'])) { + $options['mimeType'] = $metadata['ContentType']; + unset($metadata['ContentType']); + } + + $object = new \Google_Service_Storage_StorageObject(); + $object->name = $path; + $object->setMetadata($metadata); + + try { + $object = $this->service->objects->insert($this->bucket, $object, $options); + + if ($this->options['acl'] == 'public') { + $acl = new \Google_Service_Storage_ObjectAccessControl(); + $acl->setEntity("allUsers"); + $acl->setRole("READER"); + + $this->service->objectAccessControls->insert($this->bucket, $path, $acl); + } + + return $object->getSize(); + } catch (\Google_Service_Exception $e) { + return false; + } + } + + /** + * {@inheritdoc} + */ + public function exists($key) + { + $this->ensureBucketExists(); + $path = $this->computePath($key); + + try { + $this->service->objects->get($this->bucket, $path); + } catch (\Google_Service_Exception $e) { + return false; + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function keys() + { + return $this->listKeys(); + } + + /** + * {@inheritdoc} + */ + public function mtime($key) + { + $this->ensureBucketExists(); + $path = $this->computePath($key); + + $object = $this->getObjectData($path); + + return $object ? strtotime($object->getUpdated()) : false; + } + + /** + * {@inheritdoc} + */ + public function delete($key) + { + $this->ensureBucketExists(); + $path = $this->computePath($key); + + try { + $this->service->objects->delete($this->bucket, $path); + } catch (\Google_Service_Exception $e) { + return false; + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function rename($sourceKey, $targetKey) + { + $this->ensureBucketExists(); + $sourcePath = $this->computePath($sourceKey); + $targetPath = $this->computePath($targetKey); + + $object = $this->getObjectData($sourcePath); + if ($object === false) { + return false; + } + + try { + $this->service->objects->copy($this->bucket, $sourcePath, $this->bucket, $targetPath, $object); + $this->delete($sourcePath); + } catch (\Google_Service_Exception $e) { + return false; + } + + return true; + } + + /** + * {@inheritdoc} + */ + public function isDirectory($key) + { + if ($this->exists($key . '/')) { + return true; + } + + return false; + } + + /** + * {@inheritdoc} + */ + public function listKeys($prefix = '') + { + $this->ensureBucketExists(); + + $options = array(); + if ((string)$prefix != '') { + $options['prefix'] = $this->computePath($prefix); + } elseif (!empty($this->options['directory'])) { + $options['prefix'] = $this->options['directory']; + } + + $list = $this->service->objects->listObjects($this->bucket, $options); + $keys = array(); + + // FIXME: Temporary workaround for google/google-api-php-client#375 + $reflectionClass = new \ReflectionClass('Google_Service_Storage_Objects'); + $reflectionProperty = $reflectionClass->getProperty('collection_key'); + $reflectionProperty->setAccessible(true); + $reflectionProperty->setValue($list, 'items'); + + /** @var \Google_Service_Storage_StorageObject $object */ + foreach ($list as $object) { + $keys[] = $object->name; + } + + sort($keys); + + return $keys; + } + + /** + * {@inheritdoc} + */ + public function setMetadata($key, $content) + { + $path = $this->computePath($key); + + $this->metadata[$path] = $content; + } + + /** + * {@inheritdoc} + */ + public function getMetadata($key) + { + $path = $this->computePath($key); + + return isset($this->metadata[$path]) ? $this->metadata[$path] : array(); + } + + /** + * Ensures the specified bucket exists. + * + * @throws \RuntimeException if the bucket does not exists + */ + protected function ensureBucketExists() + { + if ($this->bucketExists) { + return; + } + + try { + $this->service->buckets->get($this->bucket); + $this->bucketExists = true; + + return; + } catch (\Google_Service_Exception $e) { + $this->bucketExists = false; + + throw new \RuntimeException( + sprintf( + 'The configured bucket "%s" does not exist.', + $this->bucket + ) + ); + } + } + + protected function computePath($key) + { + if (empty($this->options['directory'])) { + return $key; + } + + return sprintf('%s/%s', $this->options['directory'], $key); + } + + /** + * @param string $path + * @param array $options + * + * @return bool|\Google_Service_Storage_StorageObject + */ + private function getObjectData($path, $options = array()) + { + try { + return $this->service->objects->get($this->bucket, $path, $options); + } catch (\Google_Service_Exception $e) { + return false; + } + } +} diff --git a/tests/Gaufrette/Functional/Adapter/GoogleCloudStorageTest.php b/tests/Gaufrette/Functional/Adapter/GoogleCloudStorageTest.php new file mode 100644 index 000000000..ca7fa13f3 --- /dev/null +++ b/tests/Gaufrette/Functional/Adapter/GoogleCloudStorageTest.php @@ -0,0 +1,53 @@ + + */ +class GoogleCloudStorageTest extends FunctionalTestCase +{ + /** + * @test + * @group functional + * + * @expectedException \RuntimeException + */ + public function shouldThrowExceptionIfBucketMissing() + { + /** @var \Gaufrette\Adapter\GoogleCloudStorage $adapter */ + $adapter = $this->filesystem->getAdapter(); + $oldBucket = $adapter->getOptions(); + $adapter->setBucket('Gaufrette-' . mt_rand()); + + $adapter->read('foo'); + $adapter->setBucket($oldBucket); + } + + /** + * @test + * @group functional + */ + public function shouldWriteAndReadWithDirectory() + { + /** @var \Gaufrette\Adapter\GoogleCloudStorage $adapter */ + $adapter = $this->filesystem->getAdapter(); + $oldOptions = $adapter->getOptions(); + $adapter->setOptions(array('directory' => 'Gaufrette')); + + $this->assertEquals(12, $this->filesystem->write('foo', 'Some content')); + $this->assertEquals(13, $this->filesystem->write('test/subdir/foo', 'Some content1', true)); + + $this->assertEquals('Some content', $this->filesystem->read('foo')); + $this->assertEquals('Some content1', $this->filesystem->read('test/subdir/foo')); + + $this->filesystem->delete('foo'); + $this->filesystem->delete('test/subdir/foo'); + $adapter->setOptions($oldOptions); + } +} diff --git a/tests/Gaufrette/Functional/adapters/GoogleCloudStorage.php.dist b/tests/Gaufrette/Functional/adapters/GoogleCloudStorage.php.dist new file mode 100644 index 000000000..d24db34e0 --- /dev/null +++ b/tests/Gaufrette/Functional/adapters/GoogleCloudStorage.php.dist @@ -0,0 +1,23 @@ +setApplicationName('Gaufrette'); + +$key = file_get_contents($key_file_location); +$cred = new \Google_Auth_AssertionCredentials( + $service_account_name, + array(\Google_Service_Storage::DEVSTORAGE_FULL_CONTROL), + $key +); +$client->setAssertionCredentials($cred); +if ($client->getAuth()->isAccessTokenExpired()) { + $client->getAuth()->refreshTokenWithAssertion($cred); +} + +$service = new \Google_Service_Storage($client); +return new Gaufrette\Adapter\GoogleCloudStorage($service, $bucket_name, array(), true); From ba86a5b6cc91ec2b3c8a7894fd65ad96e8b94c0e Mon Sep 17 00:00:00 2001 From: Patrik Karisch Date: Sat, 1 Nov 2014 00:47:46 +0100 Subject: [PATCH 112/426] Add documentation --- README.markdown | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/README.markdown b/README.markdown index 12190930b..dfc6566e7 100644 --- a/README.markdown +++ b/README.markdown @@ -217,6 +217,45 @@ $adapter = new Gaufrette\Adapter\AzureBlobStorage($factory, 'my-container'); $filesystem = new Gaufrette\Filesystem($adapter); ``` +Using GoogleCloudStorage +------------------------ + +To use the GoogleCloudStorage adapter you will need to create a connection using the [Google APIs Client Library for PHP] +(https://github.com/google/google-api-php-client) and create a Client ID/Service Account in your [Developers Console] +(https://console.developers.google.com/). You can then create the `\Google_Service_Storage` which is required for the +GoogleCloudStorage adapter. + +```php +setApplicationName('Gaufrette'); + +$key = file_get_contents($key_file_location); +$cred = new \Google_Auth_AssertionCredentials( + $service_account_name, + array(\Google_Service_Storage::DEVSTORAGE_FULL_CONTROL), + $key +); +$client->setAssertionCredentials($cred); +if ($client->getAuth()->isAccessTokenExpired()) { + $client->getAuth()->refreshTokenWithAssertion($cred); +} + +$service = new \Google_Service_Storage($client); +$adapter = new GoogleCloudStorage($service, $bucket_name, array(), true); + +$filesystem = new Filesystem($adapter); +``` + Using FTP adapters --------------- From dd2f1237bf43d86ff886e3135ffd0e87ff5a72b1 Mon Sep 17 00:00:00 2001 From: lapause Date: Thu, 27 Nov 2014 01:17:33 +0100 Subject: [PATCH 113/426] rename() function in AwsS3 adapter was just copying the file instead of renaming it. Added deletion after copy. --- src/Gaufrette/Adapter/AwsS3.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index 5605de7f4..7d0710f60 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -111,7 +111,7 @@ public function rename($sourceKey, $targetKey) try { $this->service->copyObject($options); - return true; + return $this->delete($sourceKey); } catch (\Exception $e) { return false; } From 06519fe970143b83e91a6e53f9211757110e465b Mon Sep 17 00:00:00 2001 From: lapause Date: Thu, 27 Nov 2014 16:54:37 +0100 Subject: [PATCH 114/426] Add support for target metadata in rename process, to allow user to set ACL of the destination file (by default copied objects are private, see https://github.com/aws/aws-sdk-php/issues/367) --- src/Gaufrette/Adapter/AwsS3.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index 7d0710f60..555fc3cf5 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -110,7 +110,7 @@ public function rename($sourceKey, $targetKey) ); try { - $this->service->copyObject($options); + $this->service->copyObject(array_merge($options, $this->getMetadata($targetKey))); return $this->delete($sourceKey); } catch (\Exception $e) { return false; From 170489e7f24354483b3dde9ed6be88c033545b8c Mon Sep 17 00:00:00 2001 From: mgdigital Date: Thu, 22 Jan 2015 15:35:27 +0000 Subject: [PATCH 115/426] Fixed an undefined index. The FTP raw directory listing begins with a total line, which is causing an undefined index in the FTP adapter. --- src/Gaufrette/Adapter/Ftp.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index 03a60eb0e..8e1e0905b 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -436,7 +436,7 @@ private function parseRawlist(array $rawlist) 'name' => $infos[8] ); } - } else { + } elseif (count($infos) >= 4) { $isDir = (boolean) ('' === $infos[2]); $parsed[] = array( 'perms' => $isDir ? 'd' : '-', From ef5f085262e8a911083544d350df738dfdabf194 Mon Sep 17 00:00:00 2001 From: MAXakaWIZARD Date: Mon, 23 Mar 2015 19:39:24 +0200 Subject: [PATCH 116/426] Add methods for removing file from filesystem's registry and clearing the registry --- src/Gaufrette/Filesystem.php | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/Gaufrette/Filesystem.php b/src/Gaufrette/Filesystem.php index 5c6d26f62..d258b8877 100644 --- a/src/Gaufrette/Filesystem.php +++ b/src/Gaufrette/Filesystem.php @@ -344,4 +344,24 @@ private function isFileInRegister($key) { return array_key_exists($key, $this->fileRegister); } + + /** + * Clear files register + */ + public function clearCache() + { + $this->fileRegister = array(); + } + + /** + * Removes File object from register + * + * @param string $key + */ + public function detach($key) + { + if ($this->isFileInRegister($key)) { + unset($this->fileRegister[$key]); + } + } } From 4206d94d5cd5b7ea9295863d5d5556dbeab6a7c5 Mon Sep 17 00:00:00 2001 From: Alex Romanenko Date: Fri, 27 Mar 2015 10:30:52 +0300 Subject: [PATCH 117/426] Suppress warning if directory exists (e.g. was created between check and create calls when upload multiple files simultaneously) --- src/Gaufrette/Adapter/Local.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/Local.php b/src/Gaufrette/Adapter/Local.php index 3210f1b04..f4de26da6 100644 --- a/src/Gaufrette/Adapter/Local.php +++ b/src/Gaufrette/Adapter/Local.php @@ -259,7 +259,7 @@ protected function ensureDirectoryExists($directory, $create = false) */ protected function createDirectory($directory) { - $created = mkdir($directory, $this->mode, true); + $created = @mkdir($directory, $this->mode, true); if (!$created) { if (!is_dir($directory)) { From 406bf2f9baac8812f49bdd0ca5d3c6e3c0e4d995 Mon Sep 17 00:00:00 2001 From: MAXakaWIZARD Date: Fri, 27 Mar 2015 16:06:38 +0200 Subject: [PATCH 118/426] Refactor delete method in Filesystem class --- src/Gaufrette/Filesystem.php | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Gaufrette/Filesystem.php b/src/Gaufrette/Filesystem.php index d258b8877..fe41f9cc8 100644 --- a/src/Gaufrette/Filesystem.php +++ b/src/Gaufrette/Filesystem.php @@ -163,9 +163,7 @@ public function delete($key) $this->assertHasFile($key); if ($this->adapter->delete($key)) { - if($this->isFileInRegister($key)) { - unset($this->fileRegister[$key]); - } + $this->detach($key); return true; } From f25bff4e4f98a3168063176b3ba2374307163039 Mon Sep 17 00:00:00 2001 From: MAXakaWIZARD Date: Fri, 27 Mar 2015 16:07:38 +0200 Subject: [PATCH 119/426] Fix docblock --- src/Gaufrette/Filesystem.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gaufrette/Filesystem.php b/src/Gaufrette/Filesystem.php index fe41f9cc8..d28d4845a 100644 --- a/src/Gaufrette/Filesystem.php +++ b/src/Gaufrette/Filesystem.php @@ -90,7 +90,7 @@ public function rename($sourceKey, $targetKey) * @param string $key Key of the file * @param boolean $create Whether to create the file if it does not exist * - * @throws Gaufrette\Exception\FileNotFound + * @throws Exception\FileNotFound * @return File */ public function get($key, $create = false) From f10302292a381333e84fcc9e60ed3fe16cf17f40 Mon Sep 17 00:00:00 2001 From: MAXakaWIZARD Date: Fri, 27 Mar 2015 16:08:13 +0200 Subject: [PATCH 120/426] Add tests for Filesystem's clearCache and detach methods --- .../Functional/Adapter/LocalTest.php | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/Gaufrette/Functional/Adapter/LocalTest.php b/tests/Gaufrette/Functional/Adapter/LocalTest.php index c2833b8e8..03f669cfa 100644 --- a/tests/Gaufrette/Functional/Adapter/LocalTest.php +++ b/tests/Gaufrette/Functional/Adapter/LocalTest.php @@ -103,4 +103,44 @@ public function shouldListingOnlyGivenDirectory() @unlink($this->directory.DIRECTORY_SEPARATOR.'aaa.txt'); @rmdir($dirname); } + + /** + * @test + * @group functional + */ + public function shouldBeAbleToClearCache() + { + $dirname = sprintf( + '%s/adapters/bbb', + dirname(__DIR__) + ); + + @mkdir($dirname); + + $this->filesystem = new Filesystem(new Local($dirname)); + + $this->filesystem->get('test.txt', true); + $this->filesystem->write('test.txt', '123', true); + + $this->filesystem->get('test2.txt', true); + $this->filesystem->write('test2.txt', '123', true); + + $fsReflection = new \ReflectionClass($this->filesystem); + + $fsIsFileInRegister = $fsReflection->getMethod('isFileInRegister'); + $fsIsFileInRegister->setAccessible(true); + + $this->assertTrue($fsIsFileInRegister->invoke($this->filesystem, 'test.txt')); + $this->filesystem->detach('test.txt'); + $this->assertFalse($fsIsFileInRegister->invoke($this->filesystem, 'test.txt')); + + $this->filesystem->clearCache(); + $fsRegister = $fsReflection->getProperty('fileRegister'); + $fsRegister->setAccessible(true); + $this->assertEquals(0, count($fsRegister->getValue($this->filesystem))); + + $this->filesystem->delete('test.txt'); + $this->filesystem->delete('test2.txt'); + @rmdir($dirname); + } } From 73b5141471e81d3de2e945c7b86b300a6f5046d0 Mon Sep 17 00:00:00 2001 From: MAXakaWIZARD Date: Fri, 27 Mar 2015 17:43:35 +0200 Subject: [PATCH 121/426] Rename Filesystem's clearCache and detach methods --- src/Gaufrette/Filesystem.php | 6 +++--- tests/Gaufrette/Functional/Adapter/LocalTest.php | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Gaufrette/Filesystem.php b/src/Gaufrette/Filesystem.php index d28d4845a..f2cea6c69 100644 --- a/src/Gaufrette/Filesystem.php +++ b/src/Gaufrette/Filesystem.php @@ -163,7 +163,7 @@ public function delete($key) $this->assertHasFile($key); if ($this->adapter->delete($key)) { - $this->detach($key); + $this->removeFromRegister($key); return true; } @@ -346,7 +346,7 @@ private function isFileInRegister($key) /** * Clear files register */ - public function clearCache() + public function clearFileRegister() { $this->fileRegister = array(); } @@ -356,7 +356,7 @@ public function clearCache() * * @param string $key */ - public function detach($key) + public function removeFromRegister($key) { if ($this->isFileInRegister($key)) { unset($this->fileRegister[$key]); diff --git a/tests/Gaufrette/Functional/Adapter/LocalTest.php b/tests/Gaufrette/Functional/Adapter/LocalTest.php index 03f669cfa..acc95af9b 100644 --- a/tests/Gaufrette/Functional/Adapter/LocalTest.php +++ b/tests/Gaufrette/Functional/Adapter/LocalTest.php @@ -131,10 +131,10 @@ public function shouldBeAbleToClearCache() $fsIsFileInRegister->setAccessible(true); $this->assertTrue($fsIsFileInRegister->invoke($this->filesystem, 'test.txt')); - $this->filesystem->detach('test.txt'); + $this->filesystem->removeFromRegister('test.txt'); $this->assertFalse($fsIsFileInRegister->invoke($this->filesystem, 'test.txt')); - $this->filesystem->clearCache(); + $this->filesystem->clearFileRegister(); $fsRegister = $fsReflection->getProperty('fileRegister'); $fsRegister->setAccessible(true); $this->assertEquals(0, count($fsRegister->getValue($this->filesystem))); From f2e4947285dc6f2d8f9823db8aad00e5a6176df2 Mon Sep 17 00:00:00 2001 From: Barry Date: Mon, 11 May 2015 17:00:21 +0100 Subject: [PATCH 122/426] Check for type value in PhpseclibSftp Adapter --- src/Gaufrette/Adapter/PhpseclibSftp.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/PhpseclibSftp.php b/src/Gaufrette/Adapter/PhpseclibSftp.php index 95c67b080..76dc565af 100644 --- a/src/Gaufrette/Adapter/PhpseclibSftp.php +++ b/src/Gaufrette/Adapter/PhpseclibSftp.php @@ -215,7 +215,7 @@ protected function fetchKeys($directory = '', $onlyKeys = true) } $path = ltrim($directory . '/' . $filename, '/'); - if ($stat['type'] === NET_SFTP_TYPE_DIRECTORY) { + if (isset($stat['type']) && $stat['type'] === NET_SFTP_TYPE_DIRECTORY) { $keys['dirs'][] = $path; } else { $keys['keys'][] = $path; From 99676abe2bbc50fa6dca893ce5afd637a531491d Mon Sep 17 00:00:00 2001 From: Ian Jenkins Date: Fri, 31 Jul 2015 10:09:01 +0100 Subject: [PATCH 123/426] Use getPartialObject for exists check. Means we simply get the head of the object to check it exists rather than read the entire object which can take a long time if particularly large file. --- spec/Gaufrette/Adapter/LazyOpenCloudSpec.php | 2 +- spec/Gaufrette/Adapter/OpenCloudSpec.php | 6 +++--- src/Gaufrette/Adapter/OpenCloud.php | 8 +++++++- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/spec/Gaufrette/Adapter/LazyOpenCloudSpec.php b/spec/Gaufrette/Adapter/LazyOpenCloudSpec.php index b2543f22c..6b0aeb1b4 100644 --- a/spec/Gaufrette/Adapter/LazyOpenCloudSpec.php +++ b/spec/Gaufrette/Adapter/LazyOpenCloudSpec.php @@ -37,6 +37,6 @@ function it_initializes_object_store($objectStoreFactory, $objectStore, $contain $objectStore->getContainer("test-container-name")->shouldBeCalled()->willReturn($container); $container->getObject("test-file-name")->willThrow(new ObjectNotFoundException()); - $this->exists("test-file-name"); + $this->read("test-file-name"); } } diff --git a/spec/Gaufrette/Adapter/OpenCloudSpec.php b/spec/Gaufrette/Adapter/OpenCloudSpec.php index 9c4c1744c..dbbb2c405 100644 --- a/spec/Gaufrette/Adapter/OpenCloudSpec.php +++ b/spec/Gaufrette/Adapter/OpenCloudSpec.php @@ -87,7 +87,7 @@ function it_writes_file_and_write_fails_returns_false($container) */ function it_returns_true_if_key_exists($container, $object) { - $container->getObject('test')->willReturn($object); + $container->getPartialObject('test')->willReturn($object); $this->exists('test')->shouldReturn(true); } @@ -97,7 +97,7 @@ function it_returns_true_if_key_exists($container, $object) */ function it_returns_false_if_key_does_not_exist($container) { - $container->getObject('test')->willThrow(new ObjectNotFoundException()); + $container->getPartialObject('test')->willThrow(new BadResponseException()); $this->exists('test')->shouldReturn(false); } @@ -215,7 +215,7 @@ function it_creates_container($objectStore, $container) $objectStore->getContainer($containerName)->willThrow(new BadResponseException()); $objectStore->createContainer($containerName)->willReturn($container); - $container->getObject($filename)->willThrow(new ObjectNotFoundException()); + $container->getPartialObject($filename)->willThrow(new BadResponseException()); $this->beConstructedWith($objectStore, $containerName, true); diff --git a/src/Gaufrette/Adapter/OpenCloud.php b/src/Gaufrette/Adapter/OpenCloud.php index 8f24c53ba..cd6206dd6 100644 --- a/src/Gaufrette/Adapter/OpenCloud.php +++ b/src/Gaufrette/Adapter/OpenCloud.php @@ -126,7 +126,13 @@ public function write($key, $content) */ public function exists($key) { - return $this->tryGetObject($key) !== false; + try { + $exists = $this->getContainer()->getPartialObject($key) !== false; + } catch (BadResponseException $objFetchError) { + return false; + } + + return $exists; } /** From 4db00a7096a956d0287239c4fb26205ac2ad4132 Mon Sep 17 00:00:00 2001 From: Ana Cicconi Date: Thu, 10 Sep 2015 14:25:10 +0200 Subject: [PATCH 124/426] List keys without directory on AwsS3 adapter --- src/Gaufrette/Adapter/AwsS3.php | 14 +++++++++++++- tests/Gaufrette/Functional/Adapter/AwsS3Test.php | 14 ++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index 5605de7f4..51903ab03 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -188,7 +188,7 @@ public function listKeys($prefix = '') $keys = array(); $iter = $this->service->getIterator('ListObjects', $options); foreach ($iter as $file) { - $keys[] = $file['Key']; + $keys[] = $this->computeKey($file['Key']); } return $keys; @@ -281,4 +281,16 @@ protected function computePath($key) return sprintf('%s/%s', $this->options['directory'], $key); } + + /** + * Computes the key from the specified path + * + * @param string $path + * + * return string + */ + protected function computeKey($path) + { + return ltrim(substr($path, strlen($this->options['directory']), '/'); + } } diff --git a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php index 5e23be512..fe5cd8dc2 100644 --- a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php +++ b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php @@ -105,4 +105,18 @@ public function testGetsObjectUrlsWithDirectory() $adapter = new AwsS3($client, 'bucket', array('directory' => 'bar')); $this->assertEquals('https://bucket.s3.amazonaws.com/bar/foo', $adapter->getUrl('foo')); } + + /** + * @test + * @covers Gaufrette\Adapter\AwsS3 + */ + public function shouldListKeysWithoutDirectory() + { + $client = $this->getClient(); + $adapter = new AwsS3($client, 'bucket', array('directory' => 'bar')); + $adapter->write('test.txt', 'some content'); + $keys = $adapter->listKeys(); + + $this->assertEquals('test.txt', $keys['key']); + } } From 6e517cb971372c548975cd3f9e1b0fc6424656ed Mon Sep 17 00:00:00 2001 From: Ana Cicconi Date: Thu, 10 Sep 2015 14:41:37 +0200 Subject: [PATCH 125/426] syntax correction --- src/Gaufrette/Adapter/AwsS3.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index 51903ab03..5f8ed43bc 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -291,6 +291,6 @@ protected function computePath($key) */ protected function computeKey($path) { - return ltrim(substr($path, strlen($this->options['directory']), '/'); + return ltrim(substr($path, strlen($this->options['directory']), '/')); } } From b4dcd2f90fa43a12268ef08c19a6a38676e6a3d8 Mon Sep 17 00:00:00 2001 From: Ana Cicconi Date: Thu, 10 Sep 2015 14:47:40 +0200 Subject: [PATCH 126/426] correction --- tests/Gaufrette/Functional/Adapter/AwsS3Test.php | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php index fe5cd8dc2..6d01ab373 100644 --- a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php +++ b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php @@ -106,17 +106,12 @@ public function testGetsObjectUrlsWithDirectory() $this->assertEquals('https://bucket.s3.amazonaws.com/bar/foo', $adapter->getUrl('foo')); } - /** - * @test - * @covers Gaufrette\Adapter\AwsS3 - */ public function shouldListKeysWithoutDirectory() { $client = $this->getClient(); $adapter = new AwsS3($client, 'bucket', array('directory' => 'bar')); $adapter->write('test.txt', 'some content'); $keys = $adapter->listKeys(); - $this->assertEquals('test.txt', $keys['key']); } } From 66cda23c1b2939a8aa9913adafefe7b8ada31269 Mon Sep 17 00:00:00 2001 From: Ana Cicconi Date: Thu, 10 Sep 2015 19:02:27 +0200 Subject: [PATCH 127/426] syntax correction --- src/Gaufrette/Adapter/AwsS3.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index 5f8ed43bc..f0ca6e951 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -291,6 +291,6 @@ protected function computePath($key) */ protected function computeKey($path) { - return ltrim(substr($path, strlen($this->options['directory']), '/')); + return ltrim(substr($path, strlen($this->options['directory'])), '/'); } } From 70e9793ac4f1879efdf133e75851e5ce49204954 Mon Sep 17 00:00:00 2001 From: Andy Waterman Date: Tue, 29 Sep 2015 12:00:44 +0100 Subject: [PATCH 128/426] Correctly set cache control and other permitted metadata headers within Google Cloud Storage --- composer.json | 2 +- composer.lock | 93 +++++++++---------- src/Gaufrette/Adapter/GoogleCloudStorage.php | 21 +++++ .../Adapter/GoogleCloudStorageTest.php | 35 +++++++ 4 files changed, 102 insertions(+), 49 deletions(-) diff --git a/composer.json b/composer.json index 96f0ccd57..e18dc8e95 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "aws/aws-sdk-php": "~2", "amazonwebservices/aws-sdk-for-php": "1.5.*", "rackspace/php-opencloud" : "1.9.*", - "google/apiclient": "~1.1", + "google/apiclient": "~1.1.3", "phpspec/phpspec": "2.0.*", "phpseclib/phpseclib": "dev-master", "doctrine/dbal": ">=2.3", diff --git a/composer.lock b/composer.lock index bd57d21d8..7ae99ee91 100644 --- a/composer.lock +++ b/composer.lock @@ -1,13 +1,12 @@ { "_readme": [ "This file locks the dependencies of your project to a known state", - "Read more about it at http://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#composer-lock-the-lock-file", "This file is @generated automatically" ], - "hash": "a1b734d6a5d534d72d1036a8eda13c01", - "packages": [ - - ], + "hash": "af28c7783567f619fce38698e40145eb", + "content-hash": "0d811df7595ebba4b57ef1deeebb94f8", + "packages": [], "packages-dev": [ { "name": "amazonwebservices/aws-sdk-for-php", @@ -137,7 +136,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/088ea3b5dfbe52adf7b12623caa897e9ee1d795e", + "url": "https://api.github.com/repos/doctrine/annotations/zipball/f25c8aab83e0c3e976fd7d19875f198ccf2f7535", "reference": "088ea3b5dfbe52adf7b12623caa897e9ee1d795e", "shasum": "" }, @@ -209,7 +208,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/36c4eee5051629524389da376ba270f15765e49f", + "url": "https://api.github.com/repos/doctrine/cache/zipball/dd47003641aa5425820c0ec8a6f4a85e7412ffcd", "reference": "36c4eee5051629524389da376ba270f15765e49f", "shasum": "" }, @@ -283,7 +282,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/b99c5c46c87126201899afe88ec490a25eedd6a2", + "url": "https://api.github.com/repos/doctrine/collections/zipball/866e100a425b8b73d15393fd081c6bf067f05bf9", "reference": "b99c5c46c87126201899afe88ec490a25eedd6a2", "shasum": "" }, @@ -351,7 +350,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/common/zipball/9a7e20e779360f3b8a02c27a89d47d5a6fdce8d1", + "url": "https://api.github.com/repos/doctrine/common/zipball/1b0f957ae7facde8a48500894f5c20bce352b9f3", "reference": "9a7e20e779360f3b8a02c27a89d47d5a6fdce8d1", "shasum": "" }, @@ -426,7 +425,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/646edacd87d5b9d201bf19aaf55ee0e4d5e470c2", + "url": "https://api.github.com/repos/doctrine/dbal/zipball/f44782e91c8c910736272986f543989d214daf1c", "reference": "646edacd87d5b9d201bf19aaf55ee0e4d5e470c2", "shasum": "" }, @@ -497,7 +496,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/a81c334f2764b09e2f13a55cfd8fe3233946f728", + "url": "https://api.github.com/repos/doctrine/inflector/zipball/3a422c73f7bc556d39571f436b61fd58ccae0eb0", "reference": "a81c334f2764b09e2f13a55cfd8fe3233946f728", "shasum": "" }, @@ -566,7 +565,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/f12a5f74e5f4a9e3f558f3288504e121edfad891", + "url": "https://api.github.com/repos/doctrine/lexer/zipball/83893c552fd2045dd78aef794c31e694c37c0b8c", "reference": "f12a5f74e5f4a9e3f558f3288504e121edfad891", "shasum": "" }, @@ -623,7 +622,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Dropbox-PHP/dropbox-php/zipball/5d05e32f825fa98182c98a93f46a176868e314c6", + "url": "https://api.github.com/repos/Dropbox-PHP/dropbox-php/zipball/de6495d56d91e96f4ff9ff61302f3cca3cefa7b6", "reference": "5d05e32f825fa98182c98a93f46a176868e314c6", "shasum": "" }, @@ -679,19 +678,20 @@ "source": { "type": "git", "url": "https://github.com/google/google-api-php-client.git", - "reference": "4793f71912b5ef6cdbc8dfc24ff27c1ae39b3ccf" + "reference": "2bc00ec4e8fa1a5eec793d800fac339f30b730db" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/google/google-api-php-client/zipball/4793f71912b5ef6cdbc8dfc24ff27c1ae39b3ccf", - "reference": "4793f71912b5ef6cdbc8dfc24ff27c1ae39b3ccf", + "url": "https://api.github.com/repos/google/google-api-php-client/zipball/2bc00ec4e8fa1a5eec793d800fac339f30b730db", + "reference": "2bc00ec4e8fa1a5eec793d800fac339f30b730db", "shasum": "" }, "require": { "php": ">=5.2.1" }, "require-dev": { - "phpunit/phpunit": "3.7.*" + "phpunit/phpunit": "3.7.*", + "squizlabs/php_codesniffer": "~2.3" }, "type": "library", "extra": { @@ -713,19 +713,19 @@ "keywords": [ "google" ], - "time": "2014-10-29 09:47:08" + "time": "2015-09-23 04:19:33" }, { "name": "guzzle/guzzle", "version": "dev-master", "source": { "type": "git", - "url": "https://github.com/guzzle/guzzle.git", - "reference": "8d39ffa70ac1aecd297e93d4185092f51877edb4" + "url": "https://github.com/guzzle/guzzle3.git", + "reference": "b3f5050cb6270c7a728a0b74ac2de50a262b3e02" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle/zipball/8d39ffa70ac1aecd297e93d4185092f51877edb4", + "url": "https://api.github.com/repos/guzzle/guzzle3/zipball/b3f5050cb6270c7a728a0b74ac2de50a262b3e02", "reference": "8d39ffa70ac1aecd297e93d4185092f51877edb4", "shasum": "" }, @@ -817,7 +817,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Herzult/php-ssh/zipball/bea40b828ef2de2f9161255ce42256f26cd8a5b8", + "url": "https://api.github.com/repos/Herzult/php-ssh/zipball/d569d15483a5f9218700313cd21c50090aa93351", "reference": "bea40b828ef2de2f9161255ce42256f26cd8a5b8", "shasum": "" }, @@ -861,12 +861,12 @@ "version": "dev-master", "source": { "type": "git", - "url": "https://github.com/WindowsAzure/azure-sdk-for-php.git", - "reference": "61d4e12495358491f62cc5bd8716335b0b313488" + "url": "https://github.com/Azure/azure-sdk-for-php.git", + "reference": "d91dd566c6f78f68850a8545826bf98d015718f1" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WindowsAzure/azure-sdk-for-php/zipball/61d4e12495358491f62cc5bd8716335b0b313488", + "url": "https://api.github.com/repos/Azure/azure-sdk-for-php/zipball/d91dd566c6f78f68850a8545826bf98d015718f1", "reference": "61d4e12495358491f62cc5bd8716335b0b313488", "shasum": "" }, @@ -897,7 +897,7 @@ "php", "sdk" ], - "time": "2014-01-23 05:45:10" + "time": "2015-09-22 21:34:15" }, { "name": "mikey179/vfsStream", @@ -1236,7 +1236,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/76c9692908a6c25601df2db373043fcbb335332e", + "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/70f55bd1565402e5605ee5c6cde7dda0fd3aba45", "reference": "76c9692908a6c25601df2db373043fcbb335332e", "shasum": "" }, @@ -1435,7 +1435,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/6bd3659919ae6472454aea07482200077293e75f", + "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4f9b1eaf0a7da77c362f8d91cbc68ab1f4718d62", "reference": "6bd3659919ae6472454aea07482200077293e75f", "shasum": "" }, @@ -1493,7 +1493,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/69e55e68481cf708a6db43aff0b504e31402fe27", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/fe2466802556d3fe4e4d1d58ffd3ccfd0a19be0b", "reference": "69e55e68481cf708a6db43aff0b504e31402fe27", "shasum": "" }, @@ -1554,7 +1554,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/acd690379117b042d1c8af1fafd61bde001bf6bb", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6150bf2c35d3fc379e50c7602b75caceaa39dbf0", "reference": "acd690379117b042d1c8af1fafd61bde001bf6bb", "shasum": "" }, @@ -1599,7 +1599,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/206dfefc0ffe9cebf65c413e3d0e809c82fbf00a", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/f87a4b06e9124e7ad58572ff5f481d79423d51e0", "reference": "206dfefc0ffe9cebf65c413e3d0e809c82fbf00a", "shasum": "" }, @@ -1643,7 +1643,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/19689d4354b295ee3d8c54b4f42c3efb69cbc17c", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3e82f4e9fc92665fafd9157568e4dcb01d014e5b", "reference": "19689d4354b295ee3d8c54b4f42c3efb69cbc17c", "shasum": "" }, @@ -1687,7 +1687,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/292f4d5772dad5a12775be69f4a8dd663b20f103", + "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/cab6c6fefee93d7b7c3a01292a0fe0884ea66644", "reference": "292f4d5772dad5a12775be69f4a8dd663b20f103", "shasum": "" }, @@ -1737,7 +1737,7 @@ }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/2752cbb9ea5bd84c2811b34b6953f76965ec7a2f", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/38709dc22d519a3d1be46849868aa2ddf822bcf6", "reference": "2752cbb9ea5bd84c2811b34b6953f76965ec7a2f", "shasum": "" }, @@ -1976,12 +1976,12 @@ "target-dir": "Symfony/Component/EventDispatcher", "source": { "type": "git", - "url": "https://github.com/symfony/EventDispatcher.git", - "reference": "8b345992d9e850798f46a6c68804ac9e4965e91a" + "url": "https://github.com/symfony/event-dispatcher.git", + "reference": "a593aeccba98b024e3480fea5a30b900542784dd" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/8b345992d9e850798f46a6c68804ac9e4965e91a", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/a593aeccba98b024e3480fea5a30b900542784dd", "reference": "8b345992d9e850798f46a6c68804ac9e4965e91a", "shasum": "" }, @@ -2026,7 +2026,7 @@ ], "description": "Symfony EventDispatcher Component", "homepage": "http://symfony.com", - "time": "2014-02-11 13:52:21" + "time": "2015-09-22 14:11:08" }, { "name": "symfony/finder", @@ -2083,12 +2083,12 @@ "target-dir": "Symfony/Component/Yaml", "source": { "type": "git", - "url": "https://github.com/symfony/Yaml.git", - "reference": "e49a47d60348665261f6e279ba383241deb73cab" + "url": "https://github.com/symfony/yaml.git", + "reference": "6fa3d4a79022c9a02de17f210683516e92024f76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/Yaml/zipball/e49a47d60348665261f6e279ba383241deb73cab", + "url": "https://api.github.com/repos/symfony/yaml/zipball/6fa3d4a79022c9a02de17f210683516e92024f76", "reference": "e49a47d60348665261f6e279ba383241deb73cab", "shasum": "" }, @@ -2124,22 +2124,19 @@ ], "description": "Symfony Yaml Component", "homepage": "http://symfony.com", - "time": "2014-02-24 16:21:51" + "time": "2015-09-28 12:06:46" } ], - "aliases": [ - - ], + "aliases": [], "minimum-stability": "dev", "stability-flags": { "phpseclib/phpseclib": 20, "microsoft/windowsazure": 20 }, "prefer-stable": false, + "prefer-lowest": false, "platform": { "php": ">=5.3.2" }, - "platform-dev": [ - - ] + "platform-dev": [] } diff --git a/src/Gaufrette/Adapter/GoogleCloudStorage.php b/src/Gaufrette/Adapter/GoogleCloudStorage.php index 18ec758c1..d580db628 100644 --- a/src/Gaufrette/Adapter/GoogleCloudStorage.php +++ b/src/Gaufrette/Adapter/GoogleCloudStorage.php @@ -137,6 +137,27 @@ public function write($key, $content) $object = new \Google_Service_Storage_StorageObject(); $object->name = $path; + + if (isset($metadata['ContentDisposition'])) { + $object->setContentDisposition($metadata['ContentDisposition']); + unset($metadata['ContentDisposition']); + } + + if (isset($metadata['CacheControl'])) { + $object->setCacheControl($metadata['CacheControl']); + unset($metadata['CacheControl']); + } + + if (isset($metadata['ContentLanguage'])) { + $object->setContentLanguage($metadata['ContentLanguage']); + unset($metadata['ContentLanguage']); + } + + if (isset($metadata['ContentEncoding'])) { + $object->setContentEncoding($metadata['ContentEncoding']); + unset($metadata['ContentEncoding']); + } + $object->setMetadata($metadata); try { diff --git a/tests/Gaufrette/Functional/Adapter/GoogleCloudStorageTest.php b/tests/Gaufrette/Functional/Adapter/GoogleCloudStorageTest.php index ca7fa13f3..7f8d9d4f2 100644 --- a/tests/Gaufrette/Functional/Adapter/GoogleCloudStorageTest.php +++ b/tests/Gaufrette/Functional/Adapter/GoogleCloudStorageTest.php @@ -50,4 +50,39 @@ public function shouldWriteAndReadWithDirectory() $this->filesystem->delete('test/subdir/foo'); $adapter->setOptions($oldOptions); } + + /** + * @test + * @group functional + */ + public function shouldSetMetadataCorrectly() + { + /** @var \Gaufrette\Adapter\GoogleCloudStorage $adapter */ + $adapter = $this->filesystem->getAdapter(); + + $adapter->setMetadata('metadata.txt', array( + 'CacheControl' => 'public, maxage=7200', + 'ContentDisposition' => 'attachment; filename="test.txt"', + 'ContentEncoding' => 'identity', + 'ContentLanguage' => 'en', + 'Colour' => 'Yellow', + )); + + $this->assertEquals(12, $this->filesystem->write('metadata.txt', 'Some content', true)); + + $reflectionObject = new \ReflectionObject($adapter); + $reflectionMethod = $reflectionObject->getMethod('getObjectData'); + $reflectionMethod->setAccessible(true); + $metadata = $reflectionMethod->invoke($adapter, array('metadata.txt')); + + $this->assertEquals('public, maxage=7200', $metadata->cacheControl); + $this->assertEquals('attachment; filename="test.txt"', $metadata->contentDisposition); + $this->assertEquals('identity', $metadata->contentEncoding); + $this->assertEquals('en', $metadata->contentLanguage); + $this->assertEquals(array( + 'Colour' => 'Yellow', + ), $metadata->metadata); + + $this->filesystem->delete('metadata.txt'); + } } From f1dfd92a12f15c64c625fa89dae3f5739d68b262 Mon Sep 17 00:00:00 2001 From: Kai Date: Sun, 11 Oct 2015 15:20:20 +0200 Subject: [PATCH 129/426] makes AwsS3 adapter implement the SizeCalculator interface --- spec/Gaufrette/Adapter/AwsS3Spec.php | 5 +++++ src/Gaufrette/Adapter/AwsS3.php | 16 +++++++++++++++- 2 files changed, 20 insertions(+), 1 deletion(-) diff --git a/spec/Gaufrette/Adapter/AwsS3Spec.php b/spec/Gaufrette/Adapter/AwsS3Spec.php index 5a6775bd6..b88fa92bc 100644 --- a/spec/Gaufrette/Adapter/AwsS3Spec.php +++ b/spec/Gaufrette/Adapter/AwsS3Spec.php @@ -29,4 +29,9 @@ function it_supports_metadata() { $this->shouldHaveType('Gaufrette\Adapter\MetadataSupporter'); } + + function it_supports_sizecalculator() + { + $this->shouldHaveType('Gaufrette\Adapter\SizeCalculator'); + } } diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index 5605de7f4..0c3ff5507 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -13,7 +13,8 @@ */ class AwsS3 implements Adapter, MetadataSupporter, - ListKeysAware + ListKeysAware, + SizeCalculator { protected $service; protected $bucket; @@ -165,6 +166,19 @@ public function mtime($key) } } + /** + * {@inheritDoc} + */ + public function size($key) + { + try { + $result = $this->service->headObject($this->getOptions($key)); + return $result['ContentLength']; + } catch (\Exception $e) { + return false; + } + } + /** * {@inheritDoc} */ From 2718f0cebba789cae14b86cc4a9a88dd658209dc Mon Sep 17 00:00:00 2001 From: Sergii Mikhalchuk Date: Wed, 11 Nov 2015 22:10:39 +0200 Subject: [PATCH 130/426] fix duplicates file content when read file using MogileFS adapter --- src/Gaufrette/Adapter/MogileFS.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Gaufrette/Adapter/MogileFS.php b/src/Gaufrette/Adapter/MogileFS.php index 9ca2994d1..61da741de 100644 --- a/src/Gaufrette/Adapter/MogileFS.php +++ b/src/Gaufrette/Adapter/MogileFS.php @@ -52,6 +52,7 @@ public function read($key) $data = ''; if ($paths) { + shuffle($paths); foreach ($paths as $path) { $fh = fopen($path, 'r'); @@ -64,6 +65,7 @@ public function read($key) } fclose($fh); + break; } } From a0322e8122c6da6cd56765c0ecc11f4280af399c Mon Sep 17 00:00:00 2001 From: andrey1s <“andrey@4devs.pro”> Date: Mon, 4 Jan 2016 13:48:22 +0300 Subject: [PATCH 131/426] fix directory list local adapter --- spec/Gaufrette/Adapter/SafeLocalSpec.php | 2 +- src/Gaufrette/Adapter/Local.php | 8 ++--- .../Functional/Adapter/LocalTest.php | 34 ++++++++++++++++++- .../Functional/Adapter/SafeLocalTest.php | 3 +- 4 files changed, 39 insertions(+), 8 deletions(-) diff --git a/spec/Gaufrette/Adapter/SafeLocalSpec.php b/spec/Gaufrette/Adapter/SafeLocalSpec.php index 2aa076fb7..b938b9881 100644 --- a/spec/Gaufrette/Adapter/SafeLocalSpec.php +++ b/spec/Gaufrette/Adapter/SafeLocalSpec.php @@ -27,6 +27,6 @@ function it_computes_path_using_base64() function it_computes_key_back_using_base64() { - $this->keys()->shouldReturn(array(base64_decode('dir/file'), base64_decode('filename'))); + $this->keys()->shouldReturn(array(base64_decode('dir'), base64_decode('dir/file'), base64_decode('filename'))); } } diff --git a/src/Gaufrette/Adapter/Local.php b/src/Gaufrette/Adapter/Local.php index 3210f1b04..1cb93fcc6 100644 --- a/src/Gaufrette/Adapter/Local.php +++ b/src/Gaufrette/Adapter/Local.php @@ -97,7 +97,8 @@ public function keys() new \RecursiveDirectoryIterator( $this->directory, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS - ) + ), + \RecursiveIteratorIterator::CHILD_FIRST ); } catch (\Exception $e) { $files = new \EmptyIterator; @@ -105,10 +106,7 @@ public function keys() $keys = array(); foreach ($files as $file) { - $keys[] = $key = $this->computeKey($file); - if ('.' !== dirname($key)) { - $keys[] = dirname($key); - } + $keys[] = $this->computeKey($file); } sort($keys); diff --git a/tests/Gaufrette/Functional/Adapter/LocalTest.php b/tests/Gaufrette/Functional/Adapter/LocalTest.php index acc95af9b..619eb4a8e 100644 --- a/tests/Gaufrette/Functional/Adapter/LocalTest.php +++ b/tests/Gaufrette/Functional/Adapter/LocalTest.php @@ -29,7 +29,8 @@ public function tearDown() new \RecursiveDirectoryIterator( $this->directory, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS - ) + ), + \RecursiveIteratorIterator::CHILD_FIRST ); foreach ($iterator as $item) { @@ -104,6 +105,37 @@ public function shouldListingOnlyGivenDirectory() @rmdir($dirname); } + /** + * @test + * @covers Gaufrette\Adapter\Local + */ + public function shouldListingAllKeys() + { + $dirname = sprintf( + '%s/localDir', + $this->directory + ); + @mkdir($dirname); + + $this->filesystem = new Filesystem(new Local($this->directory)); + $this->filesystem->write('aaa.txt', 'some content'); + $this->filesystem->write('localDir/dir1/dir2/dir3/test.txt', 'some content'); + + $keys = $this->filesystem->keys(); + $dirs = $this->filesystem->listKeys(); + $this->assertCount(6, $keys); + $this->assertCount(4, $dirs['dirs']); + $this->assertEquals('localDir/dir1/dir2/dir3/test.txt', $dirs['keys'][1]); + + foreach ($dirs['keys'] as $item) { + @unlink($item); + } + $reversed = array_reverse($dirs['dirs']); + foreach ($reversed as $item) { + @rmdir($item); + } + } + /** * @test * @group functional diff --git a/tests/Gaufrette/Functional/Adapter/SafeLocalTest.php b/tests/Gaufrette/Functional/Adapter/SafeLocalTest.php index be216eb03..cd233ab90 100644 --- a/tests/Gaufrette/Functional/Adapter/SafeLocalTest.php +++ b/tests/Gaufrette/Functional/Adapter/SafeLocalTest.php @@ -25,7 +25,8 @@ public function tearDown() new \RecursiveDirectoryIterator( $this->getDirectory(), \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS - ) + ), + \RecursiveIteratorIterator::CHILD_FIRST ); foreach ($iterator as $item) { From e0dde6bcfdf2d17a2495b2f45787fca52dc5b796 Mon Sep 17 00:00:00 2001 From: andrey1s <“andrey@4devs.pro”> Date: Fri, 15 Jan 2016 16:41:44 +0300 Subject: [PATCH 132/426] add test group functional --- tests/Gaufrette/Functional/Adapter/LocalTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Gaufrette/Functional/Adapter/LocalTest.php b/tests/Gaufrette/Functional/Adapter/LocalTest.php index 619eb4a8e..a5732cb7c 100644 --- a/tests/Gaufrette/Functional/Adapter/LocalTest.php +++ b/tests/Gaufrette/Functional/Adapter/LocalTest.php @@ -74,6 +74,7 @@ public function shouldWorkWithSyslink() /** * @test * @covers Gaufrette\Adapter\Local + * @group functional */ public function shouldListingOnlyGivenDirectory() { @@ -108,6 +109,7 @@ public function shouldListingOnlyGivenDirectory() /** * @test * @covers Gaufrette\Adapter\Local + * @group functional */ public function shouldListingAllKeys() { From ed44678ce8e2fcf0ca590ffe4f2330505b62e3c5 Mon Sep 17 00:00:00 2001 From: Paul Bizouard Date: Fri, 15 Jan 2016 15:56:37 +0100 Subject: [PATCH 133/426] Add object store type parameter to the constructor of the adapter Openstack ObjectStoreFactory --- .../OpenStackCloudFiles/ObjectStoreFactory.php | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactory.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactory.php index 6f802c908..16ef879d0 100644 --- a/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactory.php +++ b/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactory.php @@ -26,18 +26,25 @@ class ObjectStoreFactory implements ObjectStoreFactoryInterface */ protected $urlType; + /** + * @var string + */ + protected $objectStoreType; + /** * Constructor * * @param OpenStack $connection * @param string $region * @param string $urlType + * @param string $objectStoreType */ - public function __construct(OpenStack $connection, $region, $urlType) + public function __construct(OpenStack $connection, $region, $urlType, $objectStoreType = 'cloudFiles') { $this->connection = $connection; $this->region = $region; $this->urlType = $urlType; + $this->objectStoreType = $objectStoreType; } /** @@ -45,6 +52,6 @@ public function __construct(OpenStack $connection, $region, $urlType) */ public function getObjectStore() { - return $this->connection->objectStoreService('cloudFiles', $this->region, $this->urlType); + return $this->connection->objectStoreService($this->objectStoreType, $this->region, $this->urlType); } } From 440001c0ac7609dc9b89662fa2f7c17a84d00042 Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Sat, 16 Jan 2016 12:25:27 +0200 Subject: [PATCH 134/426] Install ssh2 ext manually on travis in case if it was enabled. --- bin/configure_test_env.sh | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/bin/configure_test_env.sh b/bin/configure_test_env.sh index 03e46b72a..5fb5feef0 100755 --- a/bin/configure_test_env.sh +++ b/bin/configure_test_env.sh @@ -6,4 +6,12 @@ echo "extension = mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php touch .interactive (pecl install -f ssh2 < .interactive) +if grep -Fxq "ssh2" ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini +then + echo "SSH2 extension was already installed. "; +else + echo "SSH2 was not enabled during installation. Enabling it manually"; + echo "extension = ssh2.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini +fi + cp tests/Gaufrette/Functional/adapters/DoctrineDbal.php.dist tests/Gaufrette/Functional/adapters/DoctrineDbal.php -f From ce0ff4d2d6a0591a45fde0c4172ce7406164e591 Mon Sep 17 00:00:00 2001 From: Mathias Strasser Date: Wed, 29 Jul 2015 11:28:14 +0200 Subject: [PATCH 135/426] Rename README.markdown to README.md --- README.markdown => README.md | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename README.markdown => README.md (100%) diff --git a/README.markdown b/README.md similarity index 100% rename from README.markdown rename to README.md From 9ee7878c4a296c63adde935f3c07dd3405addb06 Mon Sep 17 00:00:00 2001 From: Mathias Strasser Date: Wed, 29 Jul 2015 11:32:02 +0200 Subject: [PATCH 136/426] static factory() method is deprecated --- README.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index c011c88d2..1295e682c 100644 --- a/README.md +++ b/README.md @@ -130,7 +130,9 @@ AWS SDK for PHP. If you use the newer adapter ``\AwsS3`` you will need to use the S3Client factory method, and the plug that into the Adapter. ```php -$service = S3Client::factory(array('key' => 'your_key_here', 'secret' => 'your_secret' )); +use Aws\S3\S3Client; + +$service = new S3Client(array('key' => 'your_key_here', 'secret' => 'your_secret')); $client = new AwsS3($service,'your-bucket-name'); ``` From 6126644b56b55470e8dd0bb32abe1d1e5dab985d Mon Sep 17 00:00:00 2001 From: Mathias Strasser Date: Wed, 29 Jul 2015 11:36:27 +0200 Subject: [PATCH 137/426] version and region are mandatories --- README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1295e682c..46cdc3bc4 100644 --- a/README.md +++ b/README.md @@ -132,7 +132,12 @@ If you use the newer adapter ``\AwsS3`` you will need to use the S3Client factor ```php use Aws\S3\S3Client; -$service = new S3Client(array('key' => 'your_key_here', 'secret' => 'your_secret')); +$service = new S3Client(array( + 'key' => 'your_key_here', + 'secret' => 'your_secret', + 'version' => 'latest', + 'region' => 'eu-west-1', +)); $client = new AwsS3($service,'your-bucket-name'); ``` From e64f145b88c63057be7e95f1517230d0596acb32 Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Sat, 16 Jan 2016 19:27:09 +0200 Subject: [PATCH 138/426] Change branch-alias for 0.3.x-dev --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index e18dc8e95..40437d8a1 100644 --- a/composer.json +++ b/composer.json @@ -62,7 +62,7 @@ }, "extra": { "branch-alias": { - "dev-master": "0.2.x-dev" + "dev-master": "0.3.x-dev" } }, "config": { From 89aa8cf9d1f68efd8b4809267a8e0c791dd01c37 Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Sat, 16 Jan 2016 19:28:17 +0200 Subject: [PATCH 139/426] Fix versions in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 46cdc3bc4..a43911f9e 100644 --- a/README.md +++ b/README.md @@ -31,12 +31,12 @@ Try it! Development version: ```bash -php composer.phar require knplabs/gaufrette:0.2.*@dev +php composer.phar require knplabs/gaufrette:0.3.*@dev ``` Stable version: ```bash -php composer.phar require knplabs/gaufrette:0.1.* +php composer.phar require knplabs/gaufrette:~0.2 ``` ### Setup your filesystem From 95ad05b3990bdc3f1b9efe18368ae9d7950a444a Mon Sep 17 00:00:00 2001 From: Albin Kerouaton Date: Sun, 17 Jan 2016 13:16:52 +0100 Subject: [PATCH 140/426] Add functional test case for file copy using streams --- .../Functional/FileStream/FunctionalTestCase.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php b/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php index 8c8005ade..1eba3c991 100644 --- a/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php +++ b/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php @@ -174,6 +174,22 @@ public function shouldUnlinkFile() $this->assertFalse($this->filesystem->has('test.txt')); } + /** + * @test + */ + public function shouldCopyFile() + { + file_put_contents('gaufrette://filestream/copy1.txt', 'test content'); + + $this->assertTrue(is_file('gaufrette://filestream/copy1.txt')); + $this->assertFalse(is_file('gaufrette://filestream/copy2.txt')); + + copy('gaufrette://filestream/copy1.txt', 'gaufrette://filestream/copy2.txt'); + + $this->assertTrue(is_file('gaufrette://filestream/copy1.txt')); + $this->assertTrue(is_file('gaufrette://filestream/copy2.txt')); + } + /** * @test * @dataProvider modesProvider From 043f9b9cd23e98f5efccc19d7e46cbee33eab802 Mon Sep 17 00:00:00 2001 From: Albin Kerouaton Date: Sat, 16 Jan 2016 18:38:53 +0100 Subject: [PATCH 141/426] Split README into multiple files (one per adapter) --- .editorconfig | 12 + README.md | 323 ++--------------------- composer.json | 2 +- doc/adapters/amazonS3.md | 15 ++ doc/adapters/apc.md | 21 ++ doc/adapters/awsS3.md | 27 ++ doc/adapters/azureBlobStorage.md | 29 ++ doc/adapters/doctrineDbal.md | 4 + doc/adapters/dropbox.md | 4 + doc/adapters/ftp.md | 48 ++++ doc/adapters/googleCloudStorage.md | 37 +++ doc/adapters/gridFs.md | 4 + doc/adapters/inMemory.md | 19 ++ doc/adapters/local.md | 17 ++ doc/adapters/mogileFS.md | 4 + doc/adapters/openCloud.md | 75 ++++++ doc/adapters/phpseclibSftp.md | 23 ++ doc/adapters/sftp.md | 26 ++ doc/adapters/zip.md | 17 ++ doc/index.md | 77 ++++++ src/Gaufrette/Adapter/Ftp.php | 4 + src/Gaufrette/Adapter/SizeCalculator.php | 2 +- 22 files changed, 485 insertions(+), 305 deletions(-) create mode 100644 .editorconfig create mode 100644 doc/adapters/amazonS3.md create mode 100644 doc/adapters/apc.md create mode 100644 doc/adapters/awsS3.md create mode 100644 doc/adapters/azureBlobStorage.md create mode 100644 doc/adapters/doctrineDbal.md create mode 100644 doc/adapters/dropbox.md create mode 100644 doc/adapters/ftp.md create mode 100644 doc/adapters/googleCloudStorage.md create mode 100644 doc/adapters/gridFs.md create mode 100644 doc/adapters/inMemory.md create mode 100644 doc/adapters/local.md create mode 100644 doc/adapters/mogileFS.md create mode 100644 doc/adapters/openCloud.md create mode 100644 doc/adapters/phpseclibSftp.md create mode 100644 doc/adapters/sftp.md create mode 100644 doc/adapters/zip.md create mode 100644 doc/index.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..779f99a12 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = space +indent_size = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false diff --git a/README.md b/README.md index a43911f9e..518ac349a 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,17 @@ -[![Stories in Ready](https://badge.waffle.io/knplabs/gaufrette.png?label=ready&title=Ready)](https://waffle.io/knplabs/gaufrette) Gaufrette ========= Gaufrette is a PHP5 library that provides a filesystem abstraction layer. -This project is under intensive development but we do not want to break BC. +This project does not have any stable release yet but we do not want to break BC now. [![Build Status](https://secure.travis-ci.org/KnpLabs/Gaufrette.png)](http://travis-ci.org/KnpLabs/Gaufrette) +[![Join the chat at https://gitter.im/KnpLabs/Gaufrette](https://badges.gitter.im/KnpLabs/Gaufrette.svg)](https://gitter.im/KnpLabs/Gaufrette) +[![Stories in Ready](https://badge.waffle.io/knplabs/gaufrette.png?label=ready&title=Ready)](https://waffle.io/knplabs/gaufrette) + +Symfony integration is available here: [KnpLabs/KnpGaufretteBundle](https://github.com/KnpLabs/KnpGaufretteBundle). + +Documentation is available [here](doc). Why use Gaufrette? ------------------ @@ -26,22 +31,13 @@ solution. Try it! ------- -### Installation - -Development version: ```bash -php composer.phar require knplabs/gaufrette:0.3.*@dev +php composer.phar require knplabs/gaufrette:~0.2 # Stable version +php composer.phar require knplabs/gaufrette:0.4.*@dev # Development version ``` -Stable version: - -```bash -php composer.phar require knplabs/gaufrette:~0.2 -``` - -### Setup your filesystem -Following an example with the local filesystem adapter. To setup other adapters, look up the [testcases](https://github.com/KnpLabs/Gaufrette/tree/master/tests/Gaufrette/Functional/adapters). +Following an example with the local filesystem adapter. To setup other adapters, look up [their respective documentation](https://github.com/KnpLabs/Gaufrette/tree/master/doc/#adapters). ```php read('myFile'); - -$content = 'Hello I am the new content'; - -$filesystem->write('myFile', $content); -``` - -### Use file objects - -Gaufrette also provide a File class that is a representation of files in a filesystem - -```php -setContent('Hello World'); - -echo $file->getContent(); // Hello World -``` - -### Cache a slow filesystem - -If you have to deal with a slow filesystem, it is out of question to use it directly. -So, you need a cache! Happily, Gaufrette offers a cache system ready for use. -It consist of an adapter itself composed of two adapters: - - * The *source* adapter that should be cached - * The *cache* adapter that is used to cache - -Here is an example of how to cache an ftp filesystem: - -```php - 'your_key_here', - 'secret' => 'your_secret', - 'version' => 'latest', - 'region' => 'eu-west-1', -)); -$client = new AwsS3($service,'your-bucket-name'); -``` - - -Using OpenCloud ---------------- -To use the OpenCloud adapter you will need to create a connection using the [OpenCloud SDK](https://github.com/rackspace/php-opencloud). -You can then fetch the ObjectStore which is required for the OpenCloud adapter. - -### OpenCloud - -```php -$connection = new OpenCloud\OpenStack( - 'https://example.com/v2/identity', - array( - 'username' => 'your username', - 'password' => 'your Keystone password', - 'tenantName' => 'your tenant (project) name' - )); - -$objectStore = $connection->objectStoreService('cloudFiles', 'LON', 'publicURL'); - -$adapter = new Gaufrette\Adapter\OpenCloud( - $objectStore, - 'container-name' -); - -$filesystem = new Filesystem($adapter); - -``` - -### Rackspace - -Rackspace uses a difference connection class - -```php -$connection = new OpenCloud\Rackspace( - 'https://identity.api.rackspacecloud.com/v2.0/', - array( - 'username' => 'rackspace-user', - 'apiKey' => '0900af093093788912388fc09dde090ffee09' - )); - -``` - -### LazyOpenCloud - -Instantiating the OpenCloud object store service has some overhead because it issues an authentication request, -even if you end up not using the filesystem. For better performance you can use a lazy-loading adapter which only authenticates when needed. - -```php -// ... $connection from previous step, either OpenCloud\OpenStack or OpenCloud\Rackspace instance - -$factory = new Gaufrette\Adapter\OpenStackCloudFiles\ObjectStoreFactory($connection); -$adapter = new Gaufrette\Adapter\LazyOpenCloud($factory, 'container-name'); - -$filesystem = new Filesystem($adapter); - -``` - -Using AzureBlobStorage ----------------------- -Azure Blob Storage is the storage service provided by Microsoft Windows Azure cloud environment. To use this adapter -you need to install the [Azure SDK for php](http://www.windowsazure.com/en-us/develop/php/common-tasks/download-php-sdk/) -into your project. - -To instantiate the `AzureBlobStorage` adapter you need a `BlobProxyFactoryInterface` instance (you can use the default -`BlobProxyFactory` class) and a connection string. The connection string should follow this prototype: - - BlobEndpoint=https://XXXXXXXXXX.blob.core.windows.net/;AccountName=XXXXXXXX;AccountKey=XXXXXXXXXXXXXXXXXXXX - -You should be able to find your **endpoint**, **account name** and **account key** in your -[Windows Azure management console](https://manage.windowsazure.com). - -Thanks to the blob proxy factory, the adapter lazy loads the connection to the endpoint, so it will not create any -connection until it's really needed (eg. when a read or write operation is issued). - -Follows a simple example on how to build the adapter: - -```php -$connectionString = '...'; -$factory = new Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactory($connectionString); -$adapter = new Gaufrette\Adapter\AzureBlobStorage($factory, 'my-container'); -$filesystem = new Gaufrette\Filesystem($adapter); -``` - -Using GoogleCloudStorage ------------------------- - -To use the GoogleCloudStorage adapter you will need to create a connection using the [Google APIs Client Library for PHP] -(https://github.com/google/google-api-php-client) and create a Client ID/Service Account in your [Developers Console] -(https://console.developers.google.com/). You can then create the `\Google_Service_Storage` which is required for the -GoogleCloudStorage adapter. - -```php -setApplicationName('Gaufrette'); - -$key = file_get_contents($key_file_location); -$cred = new \Google_Auth_AssertionCredentials( - $service_account_name, - array(\Google_Service_Storage::DEVSTORAGE_FULL_CONTROL), - $key -); -$client->setAssertionCredentials($cred); -if ($client->getAuth()->isAccessTokenExpired()) { - $client->getAuth()->refreshTokenWithAssertion($cred); -} - -$service = new \Google_Service_Storage($client); -$adapter = new GoogleCloudStorage($service, $bucket_name, array(), true); - -$filesystem = new Filesystem($adapter); -``` - -Using FTP adapters ---------------- - -Some FTP servers need valid configuration so Gaufrette can work with them as expected. - -### Pure Ftpd - -To handle hidden files we need to configure it by: - -```bash -echo "yes" > /etc/pure-ftpd/conf/DisplayDotFiles -``` - -### Proftpd - -To handle hidden files we need to change `ListOptions` in proftpd configuration (at debian system `/etc/proftpd/proftpd.conf` probably) to: - -```bash -ListOptions "-la" -``` - -Using Gaufrette in a Symfony2 project -------------------------------------- - -As you can see, Gaufrette provides an elegant way to declare your filesystems. - -In your Symfony2 project, add to ``deps``: - -```ini -[gaufrette] - git=https://github.com/KnpLabs/Gaufrette.git - -# if you want to use Amazon S3 -[aws-sdk] - git=https://github.com/aws/aws-sdk-php.git -``` - -And then, you can simply add them as services of your dependency injection container. -As an example, here is services declaration to use Amazon S3: - -```xml - - - %acme.aws_key% - %acme.aws_secret_key% - - - - - - %acme.s3.bucket_name% - - - - - -``` - -Streaming Files ---------------- - -Sometimes, you don't have the choice, you must get a streamable file URL (i.e -to transform an image). Let's take a look at the following example: - -```php -$adapter = new InMemoryAdapter(array('hello.txt' => 'Hello World!')); -$filesystem = new Filesystem($adapter); - -$map = StreamWrapper::getFilesystemMap(); -$map->set('foo', $filesystem); -StreamWrapper::register(); +// Then, you can access your filesystem directly +var_dump($filesystem->read('myFile')); // bool(false) +$filesystem->write('myFile', 'Hello world!'); -echo file_get_contents('gaufrette://foo/hello.txt'); // Says "Hello World!" +// Or use File objects +$file = $filesystem->get('myFile'); +// Will print something like: "myFile (modified 17/01/2016 18:40:36): Hello world!" +echo sprintf('%s (modified %s): %s', $file->getKey(), date('d/m/Y, H:i:s', $file->getMtime()), $file->getContent()); ``` Running the Tests @@ -352,7 +69,7 @@ The tests use phpspec2 and PHPUnit. As some filesystem adapters use vendor libraries, you should install the vendors: $ cd gaufrette - $ php composer.phar install --dev + $ php composer.phar install $ sh bin/configure_test_env.sh It will avoid skip a lot of tests. @@ -367,6 +84,6 @@ To check if classes specification pass: To check basic functionality of the adapters (adapters should be configured you will see many skipped tests): - $ phpunit + $ bin/phpunit Is it green? diff --git a/composer.json b/composer.json index 40437d8a1..44dd83a2f 100644 --- a/composer.json +++ b/composer.json @@ -62,7 +62,7 @@ }, "extra": { "branch-alias": { - "dev-master": "0.3.x-dev" + "dev-master": "0.4.x-dev" } }, "config": { diff --git a/doc/adapters/amazonS3.md b/doc/adapters/amazonS3.md new file mode 100644 index 000000000..7319139be --- /dev/null +++ b/doc/adapters/amazonS3.md @@ -0,0 +1,15 @@ +Amazon S3 & AclAwareAmazonS3 +============================ + +When using the legacy Amazon S3 adapters (aws-sdk-php < 2), you will need to specify a CA +certificate to be able to talk to Amazon servers in https. You can use +the one which is shipped with the SDK by defining before creating the +``\AmazonS3`` object: + +```php +define("AWS_CERTIFICATE_AUTHORITY", true); +``` + +Specifying a custom CA certificate is not required when using the +`Gaufrette\Adapter\AmazonS3` adapter because it uses the newest version of the +AWS SDK for PHP. diff --git a/doc/adapters/apc.md b/doc/adapters/apc.md new file mode 100644 index 000000000..9b4d484dd --- /dev/null +++ b/doc/adapters/apc.md @@ -0,0 +1,21 @@ +APC +=== + +`apc` extension should be enabled in order to use this adapter. + +Example +------- + +`Apc` adpater takes only two arguments : + * the first, mandatory, is a prefix to avoid conflicts between filesystems + * the second, not mandatory, is the ttl for each file stored + +```php + 'your_key_here', + 'secret' => 'your_secret', + 'version' => 'latest', + 'region' => 'eu-west-1', +)); +$adapter = new AwsS3Adapter($s3client,'your-bucket-name'); +$filesystem = new Filesystem($adapter); +``` diff --git a/doc/adapters/azureBlobStorage.md b/doc/adapters/azureBlobStorage.md new file mode 100644 index 000000000..f07cfe9f5 --- /dev/null +++ b/doc/adapters/azureBlobStorage.md @@ -0,0 +1,29 @@ +AzureBlobStorage +================== + +Azure Blob Storage is the storage service provided by Microsoft Windows Azure cloud environment. To use this adapter +you need to install the [Azure SDK for php](http://www.windowsazure.com/en-us/develop/php/common-tasks/download-php-sdk/) +into your project. + +To instantiate the `AzureBlobStorage` adapter you need a `BlobProxyFactoryInterface` instance (you can use the default +`BlobProxyFactory` class) and a connection string. The connection string should follow this prototype: + + BlobEndpoint=https://XXXXXXXXXX.blob.core.windows.net/;AccountName=XXXXXXXX;AccountKey=XXXXXXXXXXXXXXXXXXXX + +You should be able to find your **endpoint**, **account name** and **account key** in your +[Windows Azure management console](https://manage.windowsazure.com). + +Thanks to the blob proxy factory, the adapter lazy loads the connection to the endpoint, so it will not create any +connection until it's really needed (eg. when a read or write operation is issued). + +Example +------- + +```php + /etc/pure-ftpd/conf/DisplayDotFiles +``` + +### Proftpd + +We need to change `ListOptions` in proftpd configuration (at debian system `/etc/proftpd/proftpd.conf`) to: + +```bash +ListOptions "-la" +``` + +Example +------- + +The third argument of the `Ftp` adapter is not mandatory, however you can use it to pass configuration options +(port, username, password, etc.): + +```php + 21, + 'username' => 'my_username', + 'password' => 'my_password', + 'passive' => true, + 'create' => true, // Whether to create the remote directory if it does not exist + 'mode' => FTP_BINARY, // Or FTP_TEXT + 'ssl' => true, +)); +$filesystem = new Filesystem($adapter); +``` diff --git a/doc/adapters/googleCloudStorage.md b/doc/adapters/googleCloudStorage.md new file mode 100644 index 000000000..41de1c461 --- /dev/null +++ b/doc/adapters/googleCloudStorage.md @@ -0,0 +1,37 @@ +GoogleCloudStorage +================== + +To use the GoogleCloudStorage adapter you will need to create a connection using the [Google APIs Client Library for PHP] +(https://github.com/google/google-api-php-client) and create a Client ID/Service Account in your [Developers Console] +(https://console.developers.google.com/). You can then create the `\Google_Service_Storage` which is required for the +GoogleCloudStorage adapter. + +Example +------- + +```php +setClientId('xxxxxxxxxxxxxxx.apps.googleusercontent.com'); +$client->setApplicationName('Gaufrette'); + +$cred = new \Google_Auth_AssertionCredentials( + 'xxxxxxxxxxxxxxx@developer.gserviceaccount.com', + array(\Google_Service_Storage::DEVSTORAGE_FULL_CONTROL), + file_get_contents('key.p12') +); +$client->setAssertionCredentials($cred); +if ($client->getAuth()->isAccessTokenExpired()) { + $client->getAuth()->refreshTokenWithAssertion($cred); +} + +$service = new \Google_Service_Storage($client); +$adapter = new Gaufrette\Adapter\GoogleCloudStorage($service, $config['gcsBucket'], array( + 'acl' => 'public', +), true); +$filesystem = new Gaufrette\Filesystem($adapter); +``` diff --git a/doc/adapters/gridFs.md b/doc/adapters/gridFs.md new file mode 100644 index 000000000..edbccdbd0 --- /dev/null +++ b/doc/adapters/gridFs.md @@ -0,0 +1,4 @@ +GridFS +====== + +TBW diff --git a/doc/adapters/inMemory.md b/doc/adapters/inMemory.md new file mode 100644 index 000000000..18e50c372 --- /dev/null +++ b/doc/adapters/inMemory.md @@ -0,0 +1,19 @@ +InMemory +======== + +This adapter is useful in test environments where you don't want to depend on external filesystems. + +Example +------- + +`InMemory` adapter only takes an array of available files as argument. + +``` + 'its content')); +$filesystem = new Filesystem($adapter); +``` diff --git a/doc/adapters/local.md b/doc/adapters/local.md new file mode 100644 index 000000000..fb82d7dcb --- /dev/null +++ b/doc/adapters/local.md @@ -0,0 +1,17 @@ +Local & SafeLocal +================= + +Those two adapters aims to use local filesystem. The second one will encode in base64 the filename before storing/retrieving. + +Example +------- + +```php + 'your username', + 'password' => 'your Keystone password', + 'tenantName' => 'your tenant (project) name' + ) +); +$objectStore = $connection->objectStoreService('cloudFiles', 'LON', 'publicURL'); + +$adapter = new OpenCloudAdapter( + $objectStore, + 'container-name' +); +$filesystem = new Filesystem($adapter); +``` + +### Rackspace + +Rackspace uses a difference connection class + +```php + 'rackspace-user', + 'apiKey' => '0900af093093788912388fc09dde090ffee09' + ) +); + +$objectStore = $connection->objectStoreService('cloudFiles', 'LON', 'publicURL'); + +$adapter = new OpenCloudAdapter( + $objectStore, + 'container-name' +); +$filesystem = new Filesystem($adapter); +``` + +LazyOpenCloud Example +--------------------- + +Instantiating the OpenCloud object store service has some overhead because it issues an authentication request, +even if you end up not using the filesystem. For better performance you can use a lazy-loading adapter which only authenticates when needed. + +```php + 'Hello World!')); +$filesystem = new Filesystem($adapter); + +$map = StreamWrapper::getFilesystemMap(); +$map->set('foo', $filesystem); + +StreamWrapper::register(); + +copy('gaufrette://foo/hello.txt', 'gaufrette://foo/world.txt'); +unlink('gaufrette://foo/hello.txt'); + +echo file_get_contents('gaufrette://foo/world.txt'); // Says "Hello World!" +``` diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index 03a60eb0e..6e1e93b57 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -38,6 +38,10 @@ class Ftp implements Adapter, */ public function __construct($directory, $host, $options = array()) { + if (!extension_loaded('ftp')) { + throw new \RuntimeException('Unable to use Gaufrette\Adapter\Ftp as the FTP extension is not available.'); + } + $this->directory = (string) $directory; $this->host = $host; $this->port = isset($options['port']) ? $options['port'] : 21; diff --git a/src/Gaufrette/Adapter/SizeCalculator.php b/src/Gaufrette/Adapter/SizeCalculator.php index 02e7d29af..6fd483043 100644 --- a/src/Gaufrette/Adapter/SizeCalculator.php +++ b/src/Gaufrette/Adapter/SizeCalculator.php @@ -14,7 +14,7 @@ interface SizeCalculator * * @param string $key * - * @return string + * @return int */ public function size($key); } From 23daafbda02f989b57869766f3055b07d336b1e2 Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Mon, 18 Jan 2016 09:03:52 +0200 Subject: [PATCH 142/426] Small docblocks correction, fixes #342 --- src/Gaufrette/Filesystem.php | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Gaufrette/Filesystem.php b/src/Gaufrette/Filesystem.php index f2cea6c69..059d00283 100644 --- a/src/Gaufrette/Filesystem.php +++ b/src/Gaufrette/Filesystem.php @@ -13,11 +13,11 @@ class Filesystem { protected $adapter; - + /** * Contains File objects created with $this->createFile() method - * - * @var array + * + * @var array */ protected $fileRegister = array(); @@ -75,7 +75,7 @@ public function rename($sourceKey, $targetKey) if (! $this->adapter->rename($sourceKey, $targetKey)) { throw new \RuntimeException(sprintf('Could not rename the "%s" key to "%s".', $sourceKey, $targetKey)); } - + if($this->isFileInRegister($sourceKey)) { $this->fileRegister[$targetKey] = $this->fileRegister[$sourceKey]; unset($this->fileRegister[$sourceKey]); @@ -266,7 +266,10 @@ public function size($key) } /** - * {@inheritDoc} + * Gets a new stream instance of the specified file. + * + * @param $key + * @return Stream|Stream\InMemoryBuffer */ public function createStream($key) { @@ -278,7 +281,10 @@ public function createStream($key) } /** - * {@inheritDoc} + * Creates a new file in a filesystem. + * + * @param $key + * @return File */ public function createFile($key) { @@ -330,12 +336,12 @@ private function assertHasFile($key) throw new Exception\FileNotFound($key); } } - + /** * Checks if matching File object by given key exists in the fileRegister - * + * * @param string $key - * + * * @return bool */ private function isFileInRegister($key) From 3521f16b60d971788c47a476f35404c7f1802d4e Mon Sep 17 00:00:00 2001 From: Albin Kerouaton Date: Sat, 16 Jan 2016 20:55:31 +0100 Subject: [PATCH 143/426] Improve directory handling in StreamWrapper It is now possible to call `file_exists` & `is_dir` on stream URI pointing to a directory, whether it is hosted locally or remotely. --- spec/Gaufrette/StreamWrapperSpec.php | 6 ++++-- src/Gaufrette/Adapter/StreamFactory.php | 2 +- src/Gaufrette/Filesystem.php | 10 ++++++++++ src/Gaufrette/Stream/InMemoryBuffer.php | 7 ++++--- src/Gaufrette/Stream/Local.php | 2 ++ src/Gaufrette/StreamWrapper.php | 3 ++- tests/Gaufrette/Functional/FileStream/LocalTest.php | 12 ++++++++++-- 7 files changed, 33 insertions(+), 9 deletions(-) diff --git a/spec/Gaufrette/StreamWrapperSpec.php b/spec/Gaufrette/StreamWrapperSpec.php index 9dd1131b8..72dafabea 100644 --- a/spec/Gaufrette/StreamWrapperSpec.php +++ b/spec/Gaufrette/StreamWrapperSpec.php @@ -248,10 +248,12 @@ function it_should_stat_from_url($stream) /** * @param \Gaufrette\Stream $stream */ - function it_does_not_stat_when_cannot_open($stream) + function it_stats_even_if_it_cannot_be_open($filesystem, $stream) { + $filesystem->createStream('dir/')->willReturn($stream); $stream->open(Argument::any())->willThrow(new \RuntimeException); - $this->url_stat('gaufrette://some/filename', STREAM_URL_STAT_LINK)->shouldReturn(false); + $stream->stat(Argument::any())->willReturn(array('mode' => 16893)); + $this->url_stat('gaufrette://some/dir/', STREAM_URL_STAT_LINK)->shouldReturn(array('mode' => 16893)); } /** diff --git a/src/Gaufrette/Adapter/StreamFactory.php b/src/Gaufrette/Adapter/StreamFactory.php index cec1d90d6..4f4f42135 100644 --- a/src/Gaufrette/Adapter/StreamFactory.php +++ b/src/Gaufrette/Adapter/StreamFactory.php @@ -14,7 +14,7 @@ interface StreamFactory * * @param string $key * - * @return Gaufrette\Stream + * @return \Gaufrette\Stream */ public function createStream($key); } diff --git a/src/Gaufrette/Filesystem.php b/src/Gaufrette/Filesystem.php index f2cea6c69..8469297f7 100644 --- a/src/Gaufrette/Filesystem.php +++ b/src/Gaufrette/Filesystem.php @@ -362,4 +362,14 @@ public function removeFromRegister($key) unset($this->fileRegister[$key]); } } + + /** + * @param string $key + * + * @return bool + */ + public function isDirectory($key) + { + return $this->adapter->isDirectory($key); + } } diff --git a/src/Gaufrette/Stream/InMemoryBuffer.php b/src/Gaufrette/Stream/InMemoryBuffer.php index 57e9fe555..09d527ca4 100644 --- a/src/Gaufrette/Stream/InMemoryBuffer.php +++ b/src/Gaufrette/Stream/InMemoryBuffer.php @@ -158,17 +158,18 @@ public function eof() public function stat() { if ($this->filesystem->has($this->key)) { - $time = $this->filesystem->mtime($this->key); + $isDirectory = $this->filesystem->isDirectory($this->key); + $time = $this->filesystem->mtime($this->key); $stats = array( 'dev' => 1, 'ino' => 0, - 'mode' => 33204, + 'mode' => $isDirectory ? 16893 : 33204, 'nlink' => 1, 'uid' => 0, 'gid' => 0, 'rdev' => 0, - 'size' => Util\Size::fromContent($this->content), + 'size' => $isDirectory ? 0 : Util\Size::fromContent($this->content), 'atime' => $time, 'mtime' => $time, 'ctime' => $time, diff --git a/src/Gaufrette/Stream/Local.php b/src/Gaufrette/Stream/Local.php index 6ed17833b..897831907 100644 --- a/src/Gaufrette/Stream/Local.php +++ b/src/Gaufrette/Stream/Local.php @@ -157,6 +157,8 @@ public function stat() { if ($this->fileHandle) { return fstat($this->fileHandle); + } elseif (!is_resource($this->fileHandle) && is_dir($this->path)) { + return stat($this->path); } return false; diff --git a/src/Gaufrette/StreamWrapper.php b/src/Gaufrette/StreamWrapper.php index b2956742c..7c94e0716 100644 --- a/src/Gaufrette/StreamWrapper.php +++ b/src/Gaufrette/StreamWrapper.php @@ -77,6 +77,8 @@ protected static function streamWrapperUnregister($scheme) /** * @param string $scheme - protocol scheme * @param string $className + * + * @return bool */ protected static function streamWrapperRegister($scheme, $className) { @@ -198,7 +200,6 @@ public function url_stat($path, $flags) try { $stream->open($this->createStreamMode('r+')); } catch (\RuntimeException $e) { - return false; } return $stream->stat(); diff --git a/tests/Gaufrette/Functional/FileStream/LocalTest.php b/tests/Gaufrette/Functional/FileStream/LocalTest.php index 60467cb54..73734d75b 100644 --- a/tests/Gaufrette/Functional/FileStream/LocalTest.php +++ b/tests/Gaufrette/Functional/FileStream/LocalTest.php @@ -12,8 +12,7 @@ class LocalTest extends FunctionalTestCase public function setUp() { $this->directory = __DIR__.DIRECTORY_SEPARATOR.'filesystem'; - @mkdir($this->directory); - @chmod($this->directory, 0777); + @mkdir($this->directory.DIRECTORY_SEPARATOR.'subdir', 0777, true); $this->filesystem = new Filesystem(new LocalAdapter($this->directory, true)); $this->registerLocalFilesystemInStream(); @@ -28,4 +27,13 @@ public function tearDown() @rmdir($this->directory); } } + + /** + * @test + */ + public function shouldSupportsDirectory() + { + $this->assertTrue(file_exists('gaufrette://filestream/subdir')); + $this->assertTrue(is_dir('gaufrette://filestream/subdir')); + } } From 2bcd01b1e8e5fc1425e40e5fe7352630553ea358 Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Thu, 4 Feb 2016 16:42:50 +0200 Subject: [PATCH 144/426] Delete composer.lock --- composer.lock | 2142 ------------------------------------------------- 1 file changed, 2142 deletions(-) delete mode 100644 composer.lock diff --git a/composer.lock b/composer.lock deleted file mode 100644 index 7ae99ee91..000000000 --- a/composer.lock +++ /dev/null @@ -1,2142 +0,0 @@ -{ - "_readme": [ - "This file locks the dependencies of your project to a known state", - "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", - "This file is @generated automatically" - ], - "hash": "af28c7783567f619fce38698e40145eb", - "content-hash": "0d811df7595ebba4b57ef1deeebb94f8", - "packages": [], - "packages-dev": [ - { - "name": "amazonwebservices/aws-sdk-for-php", - "version": "1.5.17.1", - "source": { - "type": "git", - "url": "https://github.com/amazonwebservices/aws-sdk-for-php.git", - "reference": "42d86270e9458acffff721a661f62d818ca7282f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/amazonwebservices/aws-sdk-for-php/zipball/42d86270e9458acffff721a661f62d818ca7282f", - "reference": "42d86270e9458acffff721a661f62d818ca7282f", - "shasum": "" - }, - "require": { - "php": ">=5.2.0" - }, - "type": "library", - "autoload": { - "classmap": [ - "authentication/", - "extensions/", - "lib/", - "services/", - "utilities/", - "sdk.class.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "Amazon Web Services", - "homepage": "http://aws.amazon.com" - } - ], - "description": "AWS SDK for PHP", - "homepage": "http://aws.amazon.com/sdkforphp/", - "keywords": [ - "amazon", - "aws", - "dynamodb", - "ec2", - "s3", - "sdk" - ], - "time": "2012-11-26 23:59:55" - }, - { - "name": "aws/aws-sdk-php", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/aws/aws-sdk-php.git", - "reference": "74896cc3dab67035052b5c5e4d7e80c7a58889fe" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/aws/aws-sdk-php/zipball/74896cc3dab67035052b5c5e4d7e80c7a58889fe", - "reference": "74896cc3dab67035052b5c5e4d7e80c7a58889fe", - "shasum": "" - }, - "require": { - "guzzle/guzzle": ">=3.7.0,<3.9.0", - "php": ">=5.3.3" - }, - "require-dev": { - "doctrine/cache": "~1.0", - "ext-openssl": "*", - "monolog/monolog": "1.4.*", - "phpunit/phpunit": "3.7.*", - "symfony/class-loader": "2.*", - "symfony/yaml": "2.*" - }, - "suggest": { - "doctrine/cache": "Adds support for caching of credentials and responses", - "ext-apc": "Allows service description opcode caching, request and response caching, and credentials caching", - "ext-openssl": "Allows working with CloudFront private distributions and verifying received SNS messages", - "monolog/monolog": "Adds support for logging HTTP requests and responses", - "symfony/yaml": "Eases the ability to write manifests for creating jobs in AWS Import/Export" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.5.x-dev" - } - }, - "autoload": { - "psr-0": { - "Aws": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "Amazon Web Services", - "homepage": "http://aws.amazon.com" - } - ], - "description": "AWS SDK for PHP - Use Amazon Web Services in your PHP project", - "homepage": "http://aws.amazon.com/sdkforphp", - "keywords": [ - "amazon", - "aws", - "cloud", - "dynamodb", - "ec2", - "glacier", - "s3", - "sdk" - ], - "time": "2014-02-19 17:59:07" - }, - { - "name": "doctrine/annotations", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/doctrine/annotations.git", - "reference": "088ea3b5dfbe52adf7b12623caa897e9ee1d795e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/annotations/zipball/f25c8aab83e0c3e976fd7d19875f198ccf2f7535", - "reference": "088ea3b5dfbe52adf7b12623caa897e9ee1d795e", - "shasum": "" - }, - "require": { - "doctrine/lexer": "1.*", - "php": ">=5.3.2" - }, - "require-dev": { - "doctrine/cache": "1.*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, - "autoload": { - "psr-0": { - "Doctrine\\Common\\Annotations\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com", - "homepage": "http://www.jwage.com/", - "role": "Creator" - }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com", - "homepage": "http://www.instaclick.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com", - "homepage": "http://jmsyst.com", - "role": "Developer of wrapped JMSSerializerBundle" - } - ], - "description": "Docblock Annotations Parser", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "annotations", - "docblock", - "parser" - ], - "time": "2014-02-23 21:16:16" - }, - { - "name": "doctrine/cache", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/doctrine/cache.git", - "reference": "36c4eee5051629524389da376ba270f15765e49f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/cache/zipball/dd47003641aa5425820c0ec8a6f4a85e7412ffcd", - "reference": "36c4eee5051629524389da376ba270f15765e49f", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "conflict": { - "doctrine/common": ">2.2,<2.4" - }, - "require-dev": { - "phpunit/phpunit": ">=3.7", - "satooshi/php-coveralls": "~0.6" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.4.x-dev" - } - }, - "autoload": { - "psr-0": { - "Doctrine\\Common\\Cache\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com", - "homepage": "http://www.jwage.com/", - "role": "Creator" - }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com", - "homepage": "http://www.instaclick.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com", - "homepage": "http://jmsyst.com", - "role": "Developer of wrapped JMSSerializerBundle" - } - ], - "description": "Caching library offering an object-oriented API for many cache backends", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "cache", - "caching" - ], - "time": "2013-12-18 17:21:03" - }, - { - "name": "doctrine/collections", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/doctrine/collections.git", - "reference": "b99c5c46c87126201899afe88ec490a25eedd6a2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/collections/zipball/866e100a425b8b73d15393fd081c6bf067f05bf9", - "reference": "b99c5c46c87126201899afe88ec490a25eedd6a2", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, - "autoload": { - "psr-0": { - "Doctrine\\Common\\Collections\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com", - "homepage": "http://www.jwage.com/", - "role": "Creator" - }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com", - "homepage": "http://www.instaclick.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com", - "homepage": "http://jmsyst.com", - "role": "Developer of wrapped JMSSerializerBundle" - } - ], - "description": "Collections Abstraction library", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "array", - "collections", - "iterator" - ], - "time": "2014-02-03 23:07:43" - }, - { - "name": "doctrine/common", - "version": "2.4.x-dev", - "source": { - "type": "git", - "url": "https://github.com/doctrine/common.git", - "reference": "9a7e20e779360f3b8a02c27a89d47d5a6fdce8d1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/common/zipball/1b0f957ae7facde8a48500894f5c20bce352b9f3", - "reference": "9a7e20e779360f3b8a02c27a89d47d5a6fdce8d1", - "shasum": "" - }, - "require": { - "doctrine/annotations": "1.*", - "doctrine/cache": "1.*", - "doctrine/collections": "1.*", - "doctrine/inflector": "1.*", - "doctrine/lexer": "1.*", - "php": ">=5.3.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.4.x-dev" - } - }, - "autoload": { - "psr-0": { - "Doctrine\\Common\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com", - "homepage": "http://www.jwage.com/", - "role": "Creator" - }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com", - "homepage": "http://www.instaclick.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com", - "homepage": "http://jmsyst.com", - "role": "Developer of wrapped JMSSerializerBundle" - } - ], - "description": "Common Library for Doctrine projects", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "annotations", - "collections", - "eventmanager", - "persistence", - "spl" - ], - "time": "2014-02-03 23:31:47" - }, - { - "name": "doctrine/dbal", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/doctrine/dbal.git", - "reference": "646edacd87d5b9d201bf19aaf55ee0e4d5e470c2" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/f44782e91c8c910736272986f543989d214daf1c", - "reference": "646edacd87d5b9d201bf19aaf55ee0e4d5e470c2", - "shasum": "" - }, - "require": { - "doctrine/common": "2.4.*", - "php": ">=5.3.2" - }, - "require-dev": { - "phpunit/phpunit": "3.7.*", - "symfony/console": "2.*" - }, - "suggest": { - "symfony/console": "Allows use of the command line interface" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.5.x-dev" - } - }, - "autoload": { - "psr-0": { - "Doctrine\\DBAL\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com", - "homepage": "http://www.jwage.com/", - "role": "Creator" - }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com", - "homepage": "http://www.instaclick.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - } - ], - "description": "Database Abstraction Layer", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "database", - "dbal", - "persistence", - "queryobject" - ], - "time": "2014-02-23 12:02:55" - }, - { - "name": "doctrine/inflector", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/doctrine/inflector.git", - "reference": "a81c334f2764b09e2f13a55cfd8fe3233946f728" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/inflector/zipball/3a422c73f7bc556d39571f436b61fd58ccae0eb0", - "reference": "a81c334f2764b09e2f13a55cfd8fe3233946f728", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-0": { - "Doctrine\\Common\\Inflector\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jonathan Wage", - "email": "jonwage@gmail.com", - "homepage": "http://www.jwage.com/", - "role": "Creator" - }, - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com", - "homepage": "http://www.instaclick.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Benjamin Eberlei", - "email": "kontakt@beberlei.de" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com", - "homepage": "http://jmsyst.com", - "role": "Developer of wrapped JMSSerializerBundle" - } - ], - "description": "Common String Manipulations with regard to casing and singular/plural rules.", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "inflection", - "pluralize", - "singularize", - "string" - ], - "time": "2013-12-21 19:19:50" - }, - { - "name": "doctrine/lexer", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/doctrine/lexer.git", - "reference": "f12a5f74e5f4a9e3f558f3288504e121edfad891" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/lexer/zipball/83893c552fd2045dd78aef794c31e694c37c0b8c", - "reference": "f12a5f74e5f4a9e3f558f3288504e121edfad891", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.0.x-dev" - } - }, - "autoload": { - "psr-0": { - "Doctrine\\Common\\Lexer\\": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Guilherme Blanco", - "email": "guilhermeblanco@gmail.com", - "homepage": "http://www.instaclick.com" - }, - { - "name": "Roman Borschel", - "email": "roman@code-factory.org" - }, - { - "name": "Johannes Schmitt", - "email": "schmittjoh@gmail.com", - "homepage": "http://jmsyst.com", - "role": "Developer of wrapped JMSSerializerBundle" - } - ], - "description": "Base library for a lexer that can be used in Top-Down, Recursive Descent Parsers.", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "lexer", - "parser" - ], - "time": "2013-12-20 21:39:00" - }, - { - "name": "dropbox-php/dropbox-php", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/Dropbox-PHP/dropbox-php.git", - "reference": "5d05e32f825fa98182c98a93f46a176868e314c6" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Dropbox-PHP/dropbox-php/zipball/de6495d56d91e96f4ff9ff61302f3cca3cefa7b6", - "reference": "5d05e32f825fa98182c98a93f46a176868e314c6", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "suggest": { - "http_oauth": ">=1.1.18", - "oauth": "*" - }, - "type": "library", - "autoload": { - "psr-0": { - "Dropbox": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Evert Pot", - "email": "evert@rooftopsolutions.nl", - "homepage": "http://www.rooftopsolutions.nl/", - "role": "Developer" - }, - { - "name": "Joe Constant", - "email": "lazyguru@jofee.com", - "homepage": "http://www.joeconstant.me/", - "role": "Developer" - }, - { - "name": "Evan Kaufman", - "role": "Developer" - }, - { - "name": "Naoki Sawada", - "role": "Developer" - } - ], - "description": "Dropbox PHP library", - "homepage": "http://dropbox-php.com/", - "keywords": [ - "dropbox" - ], - "time": "2013-03-29 22:25:23" - }, - { - "name": "google/apiclient", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/google/google-api-php-client.git", - "reference": "2bc00ec4e8fa1a5eec793d800fac339f30b730db" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/google/google-api-php-client/zipball/2bc00ec4e8fa1a5eec793d800fac339f30b730db", - "reference": "2bc00ec4e8fa1a5eec793d800fac339f30b730db", - "shasum": "" - }, - "require": { - "php": ">=5.2.1" - }, - "require-dev": { - "phpunit/phpunit": "3.7.*", - "squizlabs/php_codesniffer": "~2.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1.x-dev" - } - }, - "autoload": { - "classmap": [ - "src/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "description": "Client library for Google APIs", - "homepage": "http://developers.google.com/api-client-library/php", - "keywords": [ - "google" - ], - "time": "2015-09-23 04:19:33" - }, - { - "name": "guzzle/guzzle", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/guzzle/guzzle3.git", - "reference": "b3f5050cb6270c7a728a0b74ac2de50a262b3e02" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/guzzle/guzzle3/zipball/b3f5050cb6270c7a728a0b74ac2de50a262b3e02", - "reference": "8d39ffa70ac1aecd297e93d4185092f51877edb4", - "shasum": "" - }, - "require": { - "ext-curl": "*", - "php": ">=5.3.3", - "symfony/event-dispatcher": ">=2.1" - }, - "replace": { - "guzzle/batch": "self.version", - "guzzle/cache": "self.version", - "guzzle/common": "self.version", - "guzzle/http": "self.version", - "guzzle/inflection": "self.version", - "guzzle/iterator": "self.version", - "guzzle/log": "self.version", - "guzzle/parser": "self.version", - "guzzle/plugin": "self.version", - "guzzle/plugin-async": "self.version", - "guzzle/plugin-backoff": "self.version", - "guzzle/plugin-cache": "self.version", - "guzzle/plugin-cookie": "self.version", - "guzzle/plugin-curlauth": "self.version", - "guzzle/plugin-error-response": "self.version", - "guzzle/plugin-history": "self.version", - "guzzle/plugin-log": "self.version", - "guzzle/plugin-md5": "self.version", - "guzzle/plugin-mock": "self.version", - "guzzle/plugin-oauth": "self.version", - "guzzle/service": "self.version", - "guzzle/stream": "self.version" - }, - "require-dev": { - "doctrine/cache": "*", - "monolog/monolog": "1.*", - "phpunit/phpunit": "3.7.*", - "psr/log": "1.0.*", - "symfony/class-loader": "*", - "zendframework/zend-cache": "<2.3", - "zendframework/zend-log": "<2.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.8-dev" - } - }, - "autoload": { - "psr-0": { - "Guzzle": "src/", - "Guzzle\\Tests": "tests/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Michael Dowling", - "email": "mtdowling@gmail.com", - "homepage": "https://github.com/mtdowling" - }, - { - "name": "Guzzle Community", - "homepage": "https://github.com/guzzle/guzzle/contributors" - } - ], - "description": "Guzzle is a PHP HTTP client library and framework for building RESTful web service clients", - "homepage": "http://guzzlephp.org/", - "keywords": [ - "client", - "curl", - "framework", - "http", - "http client", - "rest", - "web service" - ], - "time": "2014-02-20 17:01:46" - }, - { - "name": "herzult/php-ssh", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/Herzult/php-ssh.git", - "reference": "bea40b828ef2de2f9161255ce42256f26cd8a5b8" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Herzult/php-ssh/zipball/d569d15483a5f9218700313cd21c50090aa93351", - "reference": "bea40b828ef2de2f9161255ce42256f26cd8a5b8", - "shasum": "" - }, - "require": { - "ext-ssh2": "*", - "php": ">=5.3.2" - }, - "require-dev": { - "phpunit/phpunit": "3.7.*" - }, - "type": "library", - "autoload": { - "psr-0": { - "Ssh": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Antoine Hérault", - "email": "antoine.herault@gmail.com", - "homepage": "https://github.com/Herzult" - }, - { - "name": "The contributors", - "homepage": "http://github.com/Herzult/php-ssh/contributors" - } - ], - "description": "Provides an object-oriented wrapper for the php ssh2 extension.", - "keywords": [ - "ssh", - "ssh2" - ], - "time": "2013-11-21 17:54:47" - }, - { - "name": "microsoft/windowsazure", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/Azure/azure-sdk-for-php.git", - "reference": "d91dd566c6f78f68850a8545826bf98d015718f1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Azure/azure-sdk-for-php/zipball/d91dd566c6f78f68850a8545826bf98d015718f1", - "reference": "61d4e12495358491f62cc5bd8716335b0b313488", - "shasum": "" - }, - "require": { - "pear-pear/http_request2": "*", - "pear-pear/mail_mime": "*", - "pear-pear/mail_mimedecode": "*" - }, - "type": "library", - "autoload": { - "psr-0": { - "WindowsAzure\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "Apache-2.0" - ], - "authors": [ - { - "name": "Azure PHP SDK", - "email": "azurephpsdk@microsoft.com" - } - ], - "description": "This project provides a set of PHP client libraries that make it easy to access Windows Azure tables, blobs, queues, service runtime and service management APIs.", - "keywords": [ - "azure", - "php", - "sdk" - ], - "time": "2015-09-22 21:34:15" - }, - { - "name": "mikey179/vfsStream", - "version": "v1.2.0", - "source": { - "type": "git", - "url": "https://github.com/mikey179/vfsStream.git", - "reference": "063fb10633f10c5ccbcac26227e94f46d9336f90" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/mikey179/vfsStream/zipball/063fb10633f10c5ccbcac26227e94f46d9336f90", - "reference": "063fb10633f10c5ccbcac26227e94f46d9336f90", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "type": "library", - "autoload": { - "psr-0": { - "org\\bovigo\\vfs\\": "src/main/php" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD" - ], - "homepage": "http://vfs.bovigo.org/", - "time": "2013-04-01 10:41:02" - }, - { - "name": "pear-pear.php.net/Archive_Tar", - "version": "1.3.11", - "dist": { - "type": "file", - "url": "http://pear.php.net/get/Archive_Tar-1.3.11.tgz", - "reference": null, - "shasum": null - }, - "require": { - "php": ">=4.3.0.0" - }, - "replace": { - "pear-pear/archive_tar": "== 1.3.11.0" - }, - "type": "pear-library", - "autoload": { - "classmap": [ - "" - ] - }, - "include-path": [ - "/" - ], - "description": "This class provides handling of tar files in PHP.\nIt supports creating, listing, extracting and adding to tar files.\nGzip support is available if PHP has the zlib extension built-in or\nloaded. Bz2 compression is also supported with the bz2 extension loaded." - }, - { - "name": "pear-pear.php.net/Console_Getopt", - "version": "1.3.1", - "dist": { - "type": "file", - "url": "http://pear.php.net/get/Console_Getopt-1.3.1.tgz", - "reference": null, - "shasum": null - }, - "require": { - "php": ">=4.3.0.0" - }, - "replace": { - "pear-pear/console_getopt": "== 1.3.1.0" - }, - "type": "pear-library", - "autoload": { - "classmap": [ - "" - ] - }, - "include-path": [ - "/" - ], - "description": "This is a PHP implementation of "getopt" supporting both\nshort and long options." - }, - { - "name": "pear-pear.php.net/HTTP_Request2", - "version": "2.2.1", - "dist": { - "type": "file", - "url": "http://pear.php.net/get/HTTP_Request2-2.2.1.tgz", - "reference": null, - "shasum": null - }, - "require": { - "pear-pear.php.net/net_url2": ">=2.0.0.0", - "pear-pear.php.net/pear": ">=1.9.2.0", - "php": ">=5.2.0.0" - }, - "replace": { - "pear-pear/http_request2": "== 2.2.1.0" - }, - "type": "pear-library", - "autoload": { - "classmap": [ - "" - ] - }, - "include-path": [ - "/" - ], - "description": "PHP5 rewrite of HTTP_Request package (with parts of HTTP_Client). Provides\ncleaner API and pluggable Adapters:\n * Socket adapter, based on old HTTP_Request code,\n * Curl adapter, wraps around PHP's cURL extension,\n * Mock adapter, to use for testing packages dependent on HTTP_Request2.\nSupports POST requests with data and file uploads, basic and digest\nauthentication, cookies, managing cookies across requests, proxies, gzip and\ndeflate encodings, redirects, monitoring the request progress with Observers..." - }, - { - "name": "pear-pear.php.net/Mail_Mime", - "version": "1.8.8", - "dist": { - "type": "file", - "url": "http://pear.php.net/get/Mail_Mime-1.8.8.tgz", - "reference": null, - "shasum": null - }, - "require": { - "php": ">=4.3.0.0" - }, - "replace": { - "pear-pear/mail_mime": "== 1.8.8.0" - }, - "type": "pear-library", - "autoload": { - "classmap": [ - "" - ] - }, - "include-path": [ - "/" - ], - "description": "Mail_Mime provides classes to deal with the creation and manipulation of MIME messages.\nIt allows people to create e-mail messages consisting of:\n* Text Parts\n* HTML Parts\n* Inline HTML Images\n* Attachments\n* Attached messages\n\nIt supports big messages, base64 and quoted-printable encodings and\nnon-ASCII characters in filenames, subjects, recipients, etc. encoded\nusing RFC2047 and/or RFC2231." - }, - { - "name": "pear-pear.php.net/Mail_mimeDecode", - "version": "1.5.5", - "dist": { - "type": "file", - "url": "http://pear.php.net/get/Mail_mimeDecode-1.5.5.tgz", - "reference": null, - "shasum": null - }, - "require": { - "pear-pear.php.net/mail_mime": ">1.4.0.0", - "php": ">=4.3.0.0" - }, - "replace": { - "pear-pear/mail_mimedecode": "== 1.5.5.0" - }, - "type": "pear-library", - "autoload": { - "classmap": [ - "" - ] - }, - "include-path": [ - "/" - ], - "description": "Provides a class to deal with the decoding and interpreting of mime messages.\n This package used to be part of the Mail_Mime package, but has been split off." - }, - { - "name": "pear-pear.php.net/Net_URL2", - "version": "2.0.5", - "dist": { - "type": "file", - "url": "http://pear.php.net/get/Net_URL2-2.0.5.tgz", - "reference": null, - "shasum": null - }, - "require": { - "php": ">=5.1.4.0" - }, - "replace": { - "pear-pear/net_url2": "== 2.0.5.0" - }, - "type": "pear-library", - "autoload": { - "classmap": [ - "" - ] - }, - "include-path": [ - "/" - ], - "description": "Provides parsing of URLs into their constituent parts (scheme, host, path etc.), URL generation, and resolving of\n relative URLs." - }, - { - "name": "pear-pear.php.net/PEAR", - "version": "1.9.4", - "dist": { - "type": "file", - "url": "http://pear.php.net/get/PEAR-1.9.4.tgz", - "reference": null, - "shasum": null - }, - "require": { - "ext-pcre": "*", - "ext-xml": "*", - "pear-pear.php.net/archive_tar": ">=1.3.7.0", - "pear-pear.php.net/console_getopt": ">=1.2.0.0", - "pear-pear.php.net/structures_graph": ">=1.0.2.0", - "pear-pear.php.net/xml_util": ">=1.2.0.0", - "php": ">=4.4.0.0,!=5.0.0.0,!=5.1.0.0,!=5.1.1.0,!=5.1.2.0,!=5.1.3.0,!=5.1.4.0,!=5.1.5.0" - }, - "conflict": { - "pear-pear.php.net/pear_frontend_gtk": "<0.4.0.0", - "pear-pear.php.net/pear_frontend_web": "<=0.4.0.0" - }, - "replace": { - "pear-pear/pear": "== 1.9.4.0" - }, - "type": "pear-library", - "autoload": { - "classmap": [ - "" - ] - }, - "include-path": [ - "/" - ], - "description": "The PEAR package contains:\n * the PEAR installer, for creating, distributing\n and installing packages\n * the PEAR_Exception PHP5 error handling mechanism\n * the PEAR_ErrorStack advanced error handling mechanism\n * the PEAR_Error error handling mechanism\n * the OS_Guess class for retrieving info about the OS\n where PHP is running on\n * the System class for quick handling of common operations\n with files and directories\n * the PEAR base class\n Features in a nutshell:\n * full support for channels\n * pre-download dependency validation\n * new package.xml 2.0 format allows tremendous flexibility while maintaining BC\n * support for optional dependency groups and limited support for sub-packaging\n * robust dependency support\n * full dependency validation on uninstall\n * remote install for hosts with only ftp access - no more problems with\n restricted host installation\n * full support for mirroring\n * support for bundling several packages into a single tarball\n * support for static dependencies on a url-based package\n * support for custom file roles and installation tasks" - }, - { - "name": "pear-pear.php.net/Structures_Graph", - "version": "1.0.4", - "dist": { - "type": "file", - "url": "http://pear.php.net/get/Structures_Graph-1.0.4.tgz", - "reference": null, - "shasum": null - }, - "require": { - "php": ">=4.2.0.0" - }, - "replace": { - "pear-pear/structures_graph": "== 1.0.4.0" - }, - "type": "pear-library", - "autoload": { - "classmap": [ - "" - ] - }, - "include-path": [ - "/" - ], - "description": "Structures_Graph is a package for creating and manipulating graph datastructures. It allows building of directed\nand undirected graphs, with data and metadata stored in nodes. The library provides functions for graph traversing\nas well as for characteristic extraction from the graph topology." - }, - { - "name": "pear-pear.php.net/XML_Util", - "version": "1.2.1", - "dist": { - "type": "file", - "url": "http://pear.php.net/get/XML_Util-1.2.1.tgz", - "reference": null, - "shasum": null - }, - "require": { - "ext-pcre": "*", - "php": ">=4.3.0.0" - }, - "replace": { - "pear-pear/xml_util": "== 1.2.1.0" - }, - "type": "pear-library", - "autoload": { - "classmap": [ - "" - ] - }, - "include-path": [ - "/" - ], - "description": "Selection of methods that are often needed when working with XML documents. Functionality includes creating of attribute lists from arrays, creation of tags, validation of XML names and more." - }, - { - "name": "phpdocumentor/reflection-docblock", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", - "reference": "cfb3ebea556b24df8f8f3745d61478293cb5a166" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/cfb3ebea556b24df8f8f3745d61478293cb5a166", - "reference": "cfb3ebea556b24df8f8f3745d61478293cb5a166", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "phpunit/phpunit": "3.7.*@stable" - }, - "suggest": { - "dflydev/markdown": "1.0.*", - "erusev/parsedown": "~0.7" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-0": { - "phpDocumentor": [ - "src/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Mike van Riel", - "email": "mike.vanriel@naenius.com" - } - ], - "time": "2013-12-05 08:16:55" - }, - { - "name": "phpseclib/phpseclib", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/phpseclib/phpseclib.git", - "reference": "76c9692908a6c25601df2db373043fcbb335332e" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpseclib/phpseclib/zipball/70f55bd1565402e5605ee5c6cde7dda0fd3aba45", - "reference": "76c9692908a6c25601df2db373043fcbb335332e", - "shasum": "" - }, - "require": { - "php": ">=5.0.0" - }, - "require-dev": { - "squizlabs/php_codesniffer": "1.*" - }, - "suggest": { - "ext-gmp": "Install the GMP (GNU Multiple Precision) extension in order to speed up arbitrary precision integer arithmetic operations.", - "ext-mcrypt": "Install the Mcrypt extension in order to speed up a wide variety of cryptographic operations.", - "pear-pear/PHP_Compat": "Install PHP_Compat to get phpseclib working on PHP < 4.3.3." - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "0.3-dev" - } - }, - "autoload": { - "psr-0": { - "Crypt": "phpseclib/", - "File": "phpseclib/", - "Math": "phpseclib/", - "Net": "phpseclib/" - }, - "files": [ - "phpseclib/Crypt/Random.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "include-path": [ - "phpseclib/" - ], - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Jim Wigginton", - "email": "terrafrost@php.net", - "role": "Lead Developer" - }, - { - "name": "Patrick Monnerat", - "email": "pm@datasphere.ch", - "role": "Developer" - }, - { - "name": "Andreas Fischer", - "email": "bantu@phpbb.com", - "role": "Developer" - }, - { - "name": "Hans-Jürgen Petrich", - "email": "petrich@tronic-media.com", - "role": "Developer" - } - ], - "description": "PHP Secure Communications Library - Pure-PHP implementations of RSA, AES, SSH2, SFTP, X.509 etc.", - "homepage": "http://phpseclib.sourceforge.net", - "keywords": [ - "BigInteger", - "aes", - "asn.1", - "asn1", - "blowfish", - "crypto", - "cryptography", - "encryption", - "rsa", - "security", - "sftp", - "signature", - "signing", - "ssh", - "twofish", - "x.509", - "x509" - ], - "time": "2014-02-23 01:03:22" - }, - { - "name": "phpspec/php-diff", - "version": "v1.0.2", - "source": { - "type": "git", - "url": "https://github.com/phpspec/php-diff.git", - "reference": "30e103d19519fe678ae64a60d77884ef3d71b28a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/php-diff/zipball/30e103d19519fe678ae64a60d77884ef3d71b28a", - "reference": "30e103d19519fe678ae64a60d77884ef3d71b28a", - "shasum": "" - }, - "type": "library", - "autoload": { - "psr-0": { - "Diff": "lib/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Chris Boulton", - "homepage": "http://github.com/chrisboulton" - } - ], - "description": "A comprehensive library for generating differences between two hashable objects (strings or arrays).", - "time": "2013-11-01 13:02:21" - }, - { - "name": "phpspec/phpspec", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/phpspec/phpspec.git", - "reference": "c41c96dcadf2bcaacaa342a84e794254c5bb763a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/phpspec/zipball/c41c96dcadf2bcaacaa342a84e794254c5bb763a", - "reference": "c41c96dcadf2bcaacaa342a84e794254c5bb763a", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "phpspec/php-diff": "~1.0.0", - "phpspec/prophecy": "~1.1", - "symfony/console": "~2.1", - "symfony/event-dispatcher": "~2.1", - "symfony/finder": "~2.1", - "symfony/yaml": "~2.1" - }, - "require-dev": { - "behat/behat": "~2.5", - "bossa/phpspec2-expect": "dev-master" - }, - "suggest": { - "phpspec/nyan-formatters": "~1.0 – Adds Nyan formatters" - }, - "bin": [ - "bin/phpspec" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.0.x-dev" - } - }, - "autoload": { - "psr-0": { - "PhpSpec": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "homepage": "http://marcelloduarte.net/" - } - ], - "description": "Specification-oriented BDD framework for PHP 5.3+", - "homepage": "http://phpspec.net/", - "keywords": [ - "BDD", - "SpecBDD", - "TDD", - "spec", - "specification", - "testing", - "tests" - ], - "time": "2014-02-23 20:29:41" - }, - { - "name": "phpspec/prophecy", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/phpspec/prophecy.git", - "reference": "6bd3659919ae6472454aea07482200077293e75f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/prophecy/zipball/4f9b1eaf0a7da77c362f8d91cbc68ab1f4718d62", - "reference": "6bd3659919ae6472454aea07482200077293e75f", - "shasum": "" - }, - "require": { - "phpdocumentor/reflection-docblock": "~2.0" - }, - "require-dev": { - "phpspec/phpspec": "2.0.*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.1.x-dev" - } - }, - "autoload": { - "psr-0": { - "Prophecy\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Konstantin Kudryashov", - "email": "ever.zet@gmail.com", - "homepage": "http://everzet.com" - }, - { - "name": "Marcello Duarte", - "email": "marcello.duarte@gmail.com" - } - ], - "description": "Highly opinionated mocking framework for PHP 5.3+", - "homepage": "http://phpspec.org", - "keywords": [ - "Double", - "Dummy", - "fake", - "mock", - "spy", - "stub" - ], - "time": "2014-02-18 21:47:37" - }, - { - "name": "phpunit/php-code-coverage", - "version": "1.2.x-dev", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "69e55e68481cf708a6db43aff0b504e31402fe27" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/fe2466802556d3fe4e4d1d58ffd3ccfd0a19be0b", - "reference": "69e55e68481cf708a6db43aff0b504e31402fe27", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "phpunit/php-file-iterator": ">=1.3.0@stable", - "phpunit/php-text-template": ">=1.2.0@stable", - "phpunit/php-token-stream": ">=1.1.3@stable" - }, - "require-dev": { - "phpunit/phpunit": "3.7.*@dev" - }, - "suggest": { - "ext-dom": "*", - "ext-xdebug": ">=2.0.5" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, - "autoload": { - "classmap": [ - "PHP/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "include-path": [ - "" - ], - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", - "homepage": "https://github.com/sebastianbergmann/php-code-coverage", - "keywords": [ - "coverage", - "testing", - "xunit" - ], - "time": "2014-02-25 03:34:05" - }, - { - "name": "phpunit/php-file-iterator", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "acd690379117b042d1c8af1fafd61bde001bf6bb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/6150bf2c35d3fc379e50c7602b75caceaa39dbf0", - "reference": "acd690379117b042d1c8af1fafd61bde001bf6bb", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "autoload": { - "classmap": [ - "File/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "include-path": [ - "" - ], - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "FilterIterator implementation that filters files based on a list of suffixes.", - "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", - "keywords": [ - "filesystem", - "iterator" - ], - "time": "2013-10-10 15:34:57" - }, - { - "name": "phpunit/php-text-template", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "206dfefc0ffe9cebf65c413e3d0e809c82fbf00a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/f87a4b06e9124e7ad58572ff5f481d79423d51e0", - "reference": "206dfefc0ffe9cebf65c413e3d0e809c82fbf00a", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "autoload": { - "classmap": [ - "Text/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "include-path": [ - "" - ], - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Simple template engine.", - "homepage": "https://github.com/sebastianbergmann/php-text-template/", - "keywords": [ - "template" - ], - "time": "2014-01-30 17:20:04" - }, - { - "name": "phpunit/php-timer", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "19689d4354b295ee3d8c54b4f42c3efb69cbc17c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3e82f4e9fc92665fafd9157568e4dcb01d014e5b", - "reference": "19689d4354b295ee3d8c54b4f42c3efb69cbc17c", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "autoload": { - "classmap": [ - "PHP/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "include-path": [ - "" - ], - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Utility class for timing", - "homepage": "https://github.com/sebastianbergmann/php-timer/", - "keywords": [ - "timer" - ], - "time": "2013-08-02 07:42:54" - }, - { - "name": "phpunit/php-token-stream", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "292f4d5772dad5a12775be69f4a8dd663b20f103" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/cab6c6fefee93d7b7c3a01292a0fe0884ea66644", - "reference": "292f4d5772dad5a12775be69f4a8dd663b20f103", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2-dev" - } - }, - "autoload": { - "classmap": [ - "PHP/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "include-path": [ - "" - ], - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Wrapper around PHP's tokenizer extension.", - "homepage": "https://github.com/sebastianbergmann/php-token-stream/", - "keywords": [ - "tokenizer" - ], - "time": "2013-10-21 14:03:39" - }, - { - "name": "phpunit/phpunit", - "version": "3.7.x-dev", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "2752cbb9ea5bd84c2811b34b6953f76965ec7a2f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/38709dc22d519a3d1be46849868aa2ddf822bcf6", - "reference": "2752cbb9ea5bd84c2811b34b6953f76965ec7a2f", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-pcre": "*", - "ext-reflection": "*", - "ext-spl": "*", - "php": ">=5.3.3", - "phpunit/php-code-coverage": "~1.2.1", - "phpunit/php-file-iterator": ">=1.3.1", - "phpunit/php-text-template": ">=1.1.1", - "phpunit/php-timer": ">=1.0.4", - "phpunit/phpunit-mock-objects": "~1.2.0", - "symfony/yaml": "~2.0" - }, - "require-dev": { - "pear-pear.php.net/pear": "1.9.4" - }, - "suggest": { - "ext-json": "*", - "ext-simplexml": "*", - "ext-tokenizer": "*", - "phpunit/php-invoker": ">=1.1.0,<1.2.0" - }, - "bin": [ - "composer/bin/phpunit" - ], - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.7.x-dev" - } - }, - "autoload": { - "classmap": [ - "PHPUnit/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "include-path": [ - "", - "../../symfony/yaml/" - ], - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } - ], - "description": "The PHP Unit Testing framework.", - "homepage": "http://www.phpunit.de/", - "keywords": [ - "phpunit", - "testing", - "xunit" - ], - "time": "2014-02-25 03:47:29" - }, - { - "name": "phpunit/phpunit-mock-objects", - "version": "1.2.x-dev", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "c39c4511c3b007539eb170c32cbc2af49a07351a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/c39c4511c3b007539eb170c32cbc2af49a07351a", - "reference": "c39c4511c3b007539eb170c32cbc2af49a07351a", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "phpunit/php-text-template": ">=1.1.1@stable" - }, - "require-dev": { - "phpunit/phpunit": "3.7.*@dev" - }, - "suggest": { - "ext-soap": "*" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "1.2.x-dev" - } - }, - "autoload": { - "classmap": [ - "PHPUnit/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "include-path": [ - "" - ], - "license": [ - "BSD-3-Clause" - ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sb@sebastian-bergmann.de", - "role": "lead" - } - ], - "description": "Mock Object library for PHPUnit", - "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", - "keywords": [ - "mock", - "xunit" - ], - "time": "2014-02-16 12:43:56" - }, - { - "name": "rackspace/php-opencloud", - "version": "v1.9.2", - "source": { - "type": "git", - "url": "https://github.com/rackspace/php-opencloud.git", - "reference": "6551de7aebcebb369d025662f99ab27c9b4527ac" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/rackspace/php-opencloud/zipball/6551de7aebcebb369d025662f99ab27c9b4527ac", - "reference": "6551de7aebcebb369d025662f99ab27c9b4527ac", - "shasum": "" - }, - "require": { - "guzzle/http": "3.8.*@dev", - "php": ">=5.3.3" - }, - "require-dev": { - "guzzle/guzzle": "dev-master", - "psr/log": "1.0.*", - "satooshi/php-coveralls": "0.6.*@dev" - }, - "type": "library", - "autoload": { - "psr-0": { - "OpenCloud": [ - "lib/", - "tests/" - ] - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Glen Campbell", - "email": "glen.campbell@rackspace.com" - }, - { - "name": "Jamie Hannaford", - "email": "jamie.hannaford@rackspace.com", - "homepage": "https://github.com/jamiehannaford" - } - ], - "description": "PHP SDK for Rackspace/OpenStack APIs", - "keywords": [ - "Openstack", - "nova", - "opencloud", - "rackspace", - "swift" - ], - "time": "2014-02-06 20:53:21" - }, - { - "name": "symfony/console", - "version": "dev-master", - "target-dir": "Symfony/Component/Console", - "source": { - "type": "git", - "url": "https://github.com/symfony/Console.git", - "reference": "ceb261293b65e9df55b98a9665ef29d56de0c48f" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/Console/zipball/ceb261293b65e9df55b98a9665ef29d56de0c48f", - "reference": "ceb261293b65e9df55b98a9665ef29d56de0c48f", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "symfony/event-dispatcher": "~2.1" - }, - "suggest": { - "symfony/event-dispatcher": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.5-dev" - } - }, - "autoload": { - "psr-0": { - "Symfony\\Component\\Console\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com", - "homepage": "http://fabien.potencier.org", - "role": "Lead Developer" - }, - { - "name": "Symfony Community", - "homepage": "http://symfony.com/contributors" - } - ], - "description": "Symfony Console Component", - "homepage": "http://symfony.com", - "time": "2014-02-11 13:52:21" - }, - { - "name": "symfony/event-dispatcher", - "version": "dev-master", - "target-dir": "Symfony/Component/EventDispatcher", - "source": { - "type": "git", - "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "a593aeccba98b024e3480fea5a30b900542784dd" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/a593aeccba98b024e3480fea5a30b900542784dd", - "reference": "8b345992d9e850798f46a6c68804ac9e4965e91a", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "psr/log": "~1.0", - "symfony/dependency-injection": "~2.0", - "symfony/stopwatch": "~2.2" - }, - "suggest": { - "symfony/dependency-injection": "", - "symfony/http-kernel": "" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.5-dev" - } - }, - "autoload": { - "psr-0": { - "Symfony\\Component\\EventDispatcher\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com", - "homepage": "http://fabien.potencier.org", - "role": "Lead Developer" - }, - { - "name": "Symfony Community", - "homepage": "http://symfony.com/contributors" - } - ], - "description": "Symfony EventDispatcher Component", - "homepage": "http://symfony.com", - "time": "2015-09-22 14:11:08" - }, - { - "name": "symfony/finder", - "version": "dev-master", - "target-dir": "Symfony/Component/Finder", - "source": { - "type": "git", - "url": "https://github.com/symfony/Finder.git", - "reference": "a708807d3a54b258abb5bda6832ced8b9bbf8ffb" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/Finder/zipball/a708807d3a54b258abb5bda6832ced8b9bbf8ffb", - "reference": "a708807d3a54b258abb5bda6832ced8b9bbf8ffb", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.5-dev" - } - }, - "autoload": { - "psr-0": { - "Symfony\\Component\\Finder\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com", - "homepage": "http://fabien.potencier.org", - "role": "Lead Developer" - }, - { - "name": "Symfony Community", - "homepage": "http://symfony.com/contributors" - } - ], - "description": "Symfony Finder Component", - "homepage": "http://symfony.com", - "time": "2014-02-24 16:21:51" - }, - { - "name": "symfony/yaml", - "version": "dev-master", - "target-dir": "Symfony/Component/Yaml", - "source": { - "type": "git", - "url": "https://github.com/symfony/yaml.git", - "reference": "6fa3d4a79022c9a02de17f210683516e92024f76" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/6fa3d4a79022c9a02de17f210683516e92024f76", - "reference": "e49a47d60348665261f6e279ba383241deb73cab", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.5-dev" - } - }, - "autoload": { - "psr-0": { - "Symfony\\Component\\Yaml\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com", - "homepage": "http://fabien.potencier.org", - "role": "Lead Developer" - }, - { - "name": "Symfony Community", - "homepage": "http://symfony.com/contributors" - } - ], - "description": "Symfony Yaml Component", - "homepage": "http://symfony.com", - "time": "2015-09-28 12:06:46" - } - ], - "aliases": [], - "minimum-stability": "dev", - "stability-flags": { - "phpseclib/phpseclib": 20, - "microsoft/windowsazure": 20 - }, - "prefer-stable": false, - "prefer-lowest": false, - "platform": { - "php": ">=5.3.2" - }, - "platform-dev": [] -} From 557b3e4ac1d1b722ab88ea256655444b19cd8ba5 Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Mon, 18 Jan 2016 15:35:19 +0200 Subject: [PATCH 145/426] Update phpsec sftp adapter --- composer.json | 2 +- spec/Gaufrette/Adapter/PhpseclibSftpSpec.php | 3 +- src/Gaufrette/Adapter/PhpseclibSftp.php | 40 ++++++++++---------- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/composer.json b/composer.json index 44dd83a2f..1751115c0 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "rackspace/php-opencloud" : "1.9.*", "google/apiclient": "~1.1.3", "phpspec/phpspec": "2.0.*", - "phpseclib/phpseclib": "dev-master", + "phpseclib/phpseclib": "^2.0", "doctrine/dbal": ">=2.3", "dropbox-php/dropbox-php": "*", "herzult/php-ssh": "*", diff --git a/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php b/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php index 734133e48..1eb167018 100644 --- a/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php +++ b/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php @@ -10,6 +10,7 @@ define('NET_SFTP_TYPE_DIRECTORY', 2); } +use phpseclib\Net\SFTP as Base; use PhpSpec\ObjectBehavior; class PhpseclibSftpSpec extends ObjectBehavior @@ -141,7 +142,7 @@ function it_should_create_file($sftp, $filesystem) } } -class Net_SFTP extends \Net_SFTP +class Net_SFTP extends Base { public function __construct() { diff --git a/src/Gaufrette/Adapter/PhpseclibSftp.php b/src/Gaufrette/Adapter/PhpseclibSftp.php index 76dc565af..ce62a1537 100644 --- a/src/Gaufrette/Adapter/PhpseclibSftp.php +++ b/src/Gaufrette/Adapter/PhpseclibSftp.php @@ -3,7 +3,7 @@ namespace Gaufrette\Adapter; use Gaufrette\Adapter; -use \Net_SFTP; +use phpseclib\Net\SFTP; use Gaufrette\Filesystem; use Gaufrette\File; @@ -17,14 +17,14 @@ class PhpseclibSftp implements Adapter, protected $initialized = false; /** - * Constructor + * Constructor. * - * @param \Net_SFTP $sftp An Sftp instance - * @param string $directory The distant directory - * @param boolean $create Whether to create the remote directory if it - * does not exist + * @param SFTP $sftp An Sftp instance + * @param string $directory The distant directory + * @param bool $create Whether to create the remote directory if it + * does not exist */ - public function __construct(Net_SFTP $sftp, $directory = null, $create = false) + public function __construct(SFTP $sftp, $directory = null, $create = false) { $this->sftp = $sftp; $this->directory = $directory; @@ -32,7 +32,7 @@ public function __construct(Net_SFTP $sftp, $directory = null, $create = false) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function read($key) { @@ -40,7 +40,7 @@ public function read($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function rename($sourceKey, $targetKey) { @@ -55,7 +55,7 @@ public function rename($sourceKey, $targetKey) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function write($key, $content) { @@ -71,7 +71,7 @@ public function write($key, $content) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function exists($key) { @@ -81,7 +81,7 @@ public function exists($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function isDirectory($key) { @@ -98,7 +98,7 @@ public function isDirectory($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function keys() { @@ -108,7 +108,7 @@ public function keys() } /** - * {@inheritDoc} + * {@inheritdoc} */ public function listKeys($prefix = '') { @@ -135,7 +135,7 @@ public function listKeys($prefix = '') } /** - * {@inheritDoc} + * {@inheritdoc} */ public function mtime($key) { @@ -147,7 +147,7 @@ public function mtime($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function delete($key) { @@ -155,7 +155,7 @@ public function delete($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function createFile($key, Filesystem $filesystem) { @@ -170,7 +170,7 @@ public function createFile($key, Filesystem $filesystem) } /** - * Performs the adapter's initialization + * Performs the adapter's initialization. * * It will ensure the root directory exists */ @@ -201,7 +201,7 @@ protected function ensureDirectoryExists($directory, $create) protected function computePath($key) { - return $this->directory . '/' . ltrim($key, '/'); + return $this->directory.'/'.ltrim($key, '/'); } protected function fetchKeys($directory = '', $onlyKeys = true) @@ -214,7 +214,7 @@ protected function fetchKeys($directory = '', $onlyKeys = true) continue; } - $path = ltrim($directory . '/' . $filename, '/'); + $path = ltrim($directory.'/'.$filename, '/'); if (isset($stat['type']) && $stat['type'] === NET_SFTP_TYPE_DIRECTORY) { $keys['dirs'][] = $path; } else { From d5c29510cebe3db8f24ec0c4720be209f52d0339 Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Mon, 18 Jan 2016 15:48:35 +0200 Subject: [PATCH 146/426] Fix tests --- spec/Gaufrette/Adapter/PhpseclibSftpSpec.php | 23 ++++++++++--------- src/Gaufrette/Adapter/PhpseclibSftp.php | 13 +++++++---- .../adapters/PhpseclibSftp.php.dist | 2 +- 3 files changed, 21 insertions(+), 17 deletions(-) diff --git a/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php b/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php index 1eb167018..31419849e 100644 --- a/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php +++ b/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php @@ -16,11 +16,12 @@ class PhpseclibSftpSpec extends ObjectBehavior { /** - * @param \spec\Gaufrette\Adapter\Net_SFTP $sftp + * @param \spec\Gaufrette\Adapter\SFTP $sftp */ function let($sftp) { - $this->beConstructedWith($sftp, '/home/l3l0'); + $this->beConstructedWith($sftp, '/home/l3l0', false, 'l3lo', 'password'); + $sftp->login('l3lo', 'password')->shouldBeCalled(); } function it_is_adapter() @@ -39,9 +40,9 @@ function it_supports_native_list_keys() } /** - * @param \spec\Gaufrette\Adapter\Net_SFTP $sftp + * @param \spec\Gaufrette\Adapter\SFTP $sftp */ - function it_fetchs_keys($sftp) + function it_fetches_keys($sftp) { $sftp ->rawlist('/home/l3l0/') @@ -60,7 +61,7 @@ function it_fetchs_keys($sftp) } /** - * @param \spec\Gaufrette\Adapter\Net_SFTP $sftp + * @param \spec\Gaufrette\Adapter\SFTP $sftp */ function it_reads_file($sftp) { @@ -70,7 +71,7 @@ function it_reads_file($sftp) } /** - * @param \spec\Gaufrette\Adapter\Net_SFTP $sftp + * @param \spec\Gaufrette\Adapter\SFTP $sftp */ function it_creates_and_writes_file($sftp) { @@ -83,7 +84,7 @@ function it_creates_and_writes_file($sftp) } /** - * @param \spec\Gaufrette\Adapter\Net_SFTP $sftp + * @param \spec\Gaufrette\Adapter\SFTP $sftp */ function it_renames_file($sftp) { @@ -98,7 +99,7 @@ function it_renames_file($sftp) } /** - * @param \spec\Gaufrette\Adapter\Net_SFTP $sftp + * @param \spec\Gaufrette\Adapter\SFTP $sftp */ function it_should_check_if_file_exists($sftp) { @@ -114,7 +115,7 @@ function it_should_check_if_file_exists($sftp) } /** - * @param \spec\Gaufrette\Adapter\Net_SFTP $sftp + * @param \spec\Gaufrette\Adapter\SFTP $sftp */ function it_should_check_is_directory($sftp) { @@ -128,7 +129,7 @@ function it_should_check_is_directory($sftp) } /** - * @param \spec\Gaufrette\Adapter\Net_SFTP $sftp + * @param \spec\Gaufrette\Adapter\SFTP $sftp * @param \Gaufrette\Filesystem $filesystem */ function it_should_create_file($sftp, $filesystem) @@ -142,7 +143,7 @@ function it_should_create_file($sftp, $filesystem) } } -class Net_SFTP extends Base +class SFTP extends Base { public function __construct() { diff --git a/src/Gaufrette/Adapter/PhpseclibSftp.php b/src/Gaufrette/Adapter/PhpseclibSftp.php index ce62a1537..ae500bc63 100644 --- a/src/Gaufrette/Adapter/PhpseclibSftp.php +++ b/src/Gaufrette/Adapter/PhpseclibSftp.php @@ -19,14 +19,17 @@ class PhpseclibSftp implements Adapter, /** * Constructor. * - * @param SFTP $sftp An Sftp instance - * @param string $directory The distant directory - * @param bool $create Whether to create the remote directory if it - * does not exist + * @param SFTP $sftp An Sftp instance + * @param string $directory The distant directory + * @param bool $create Whether to create the remote directory if it + * does not exist + * @param string|null $username SFTP user name + * @param string|null $password SFTP user password */ - public function __construct(SFTP $sftp, $directory = null, $create = false) + public function __construct(SFTP $sftp, $directory = null, $create = false, $username = null, $password = null) { $this->sftp = $sftp; + $this->sftp->login($username, $password); $this->directory = $directory; $this->create = $create; } diff --git a/tests/Gaufrette/Functional/adapters/PhpseclibSftp.php.dist b/tests/Gaufrette/Functional/adapters/PhpseclibSftp.php.dist index 243832bef..4c4816b1e 100644 --- a/tests/Gaufrette/Functional/adapters/PhpseclibSftp.php.dist +++ b/tests/Gaufrette/Functional/adapters/PhpseclibSftp.php.dist @@ -1,6 +1,6 @@ login('username', 'password'); $directory = '/some/directory'; From 55efbd453b969db3b3dcdb3285eaab7f3fcd7de2 Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Mon, 18 Jan 2016 16:23:37 +0200 Subject: [PATCH 147/426] Add docs --- doc/adapters/phpseclibSftp.md | 28 ++++++++++++++-------------- doc/adapters/sftp.md | 16 ++++++++++++++++ 2 files changed, 30 insertions(+), 14 deletions(-) diff --git a/doc/adapters/phpseclibSftp.md b/doc/adapters/phpseclibSftp.md index 5cc4dec70..460b7756a 100644 --- a/doc/adapters/phpseclibSftp.md +++ b/doc/adapters/phpseclibSftp.md @@ -1,23 +1,23 @@ -Phpseclib SFTP -============== +### Documentation for Gaufrette phpseclib adapter -Example -------- +*N.B.* It is recommended to use this adapter over [SFTP](doc/adapters/sftp.md). -The first argument should be an instance of `\Net_SFTP`. Please refer to -[`phpseclib/phpseclib`](https://github.com/phpseclib/phpseclib) documentation to know how to build it. +#### Prerequisites -The second argument is the base directory you want to use. +* [phpseclib](https://github.com/phpseclib/phpseclib) -The third one indicates whether you want to automatically create directories if they does not exists -(i.e. when you create a file in a directory that does not exist yet). +You can install it via: + +```bash +composer require phpseclib/phpseclib:^2.0 +``` + +#### Configuration ```php - Date: Mon, 18 Jan 2016 16:26:27 +0200 Subject: [PATCH 148/426] Fix links to docs --- doc/adapters/sftp.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/adapters/sftp.md b/doc/adapters/sftp.md index e805cc3b6..00973cae9 100644 --- a/doc/adapters/sftp.md +++ b/doc/adapters/sftp.md @@ -2,7 +2,7 @@ SFTP ==== *N.B.* SFTP adapter is not recommended to use due to https://bugs.php.net/bug.php?id=64169. It is recommended to use -[PHPSeclibSFTP adapter](phpseclib_sftp.md) instead. +[phpseclib SFTP adapter](phpseclib_sftp.md) instead. This adapter is based on the `ssh2` extension. If you don't have this extension available and you can't install it, the [`PhpseclibSftp`](phpseclibSftp.md) adapter is based on a full-php ssh client. From d1e625bcc79a9f76a41425f1d6c9bd6d6d16ac6f Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Sun, 7 Feb 2016 22:42:22 +0200 Subject: [PATCH 149/426] Add doc for Grid FS --- README.md | 2 +- doc/adapters/gridFs.md | 25 ++++++++++++++++++++++++- doc/index.md | 2 +- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 518ac349a..8007e09c6 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,7 @@ This project does not have any stable release yet but we do not want to break BC Symfony integration is available here: [KnpLabs/KnpGaufretteBundle](https://github.com/KnpLabs/KnpGaufretteBundle). -Documentation is available [here](doc). +Documentation is available [here](doc/index.md). Why use Gaufrette? ------------------ diff --git a/doc/adapters/gridFs.md b/doc/adapters/gridFs.md index edbccdbd0..b601ef70c 100644 --- a/doc/adapters/gridFs.md +++ b/doc/adapters/gridFs.md @@ -1,4 +1,27 @@ GridFS ====== -TBW +Prerequisites +------------- + +In order to use GridFS adapter, you should have accesible MongoDB instance and [Mongo PHP driver](http://docs.php.net/manual/en/book.mongo.php) installed. + +You can install the Mongo extension with + +```bash +pecl install mongo +``` + +*N.B.* `mongo` php extension is deprecated in favor of [MongoDB PHP driver](http://mongodb.github.io/mongo-php-driver/#installation). We will switch to it right after [new API supports GridFS](http://mongodb.github.io/mongo-php-library/#mongodb-php-library]). + +Usage +---- + +```php + +$client = new \MongoClient('mongodb://localhost:27007'); +$db = $client->selectDB('dbname'); +$gridFs = new \MongoGridFs($db); + +$adapter = new \Gaufrette\Adapter\GridFS($gridFs); + diff --git a/doc/index.md b/doc/index.md index 2bd6b95f8..8497553bc 100644 --- a/doc/index.md +++ b/doc/index.md @@ -13,7 +13,7 @@ Adapters * [Dropbox](adapters/dropbox.md): TBW * [Ftp](adapters/ftp.md) * [Google Cloud Storage](adapters/googleCloudStorage.md) - * [GridFs](adapters/gridFs.md): TBW + * [GridFS](adapters/gridFs.md) * [InMemory](adapters/inMemory.md) * [Local & SafeLocal](adapters/local.md) * [MogileFS](adapters/mogileFS.md): TBW From e4f46fd63dbf7b7e0ea13e2fc8cfaf636528da5f Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Thu, 3 Mar 2016 10:36:09 +0200 Subject: [PATCH 150/426] Remove login from phpseclib adapter --- src/Gaufrette/Adapter/PhpseclibSftp.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Gaufrette/Adapter/PhpseclibSftp.php b/src/Gaufrette/Adapter/PhpseclibSftp.php index ae500bc63..1c873964e 100644 --- a/src/Gaufrette/Adapter/PhpseclibSftp.php +++ b/src/Gaufrette/Adapter/PhpseclibSftp.php @@ -29,7 +29,6 @@ class PhpseclibSftp implements Adapter, public function __construct(SFTP $sftp, $directory = null, $create = false, $username = null, $password = null) { $this->sftp = $sftp; - $this->sftp->login($username, $password); $this->directory = $directory; $this->create = $create; } From b060d05709838794498eb8832c0c06fc3f10e6e9 Mon Sep 17 00:00:00 2001 From: umpirsky Date: Thu, 3 Mar 2016 09:39:10 +0100 Subject: [PATCH 151/426] Ignore composer.lock --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index a91fe0141..0c7e5ade0 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ vendor/ +composer.lock phpunit.xml tests/adapters/* !tests/adapters/*.dist From 8641634a2ea35441ef6ed78d79606e721d9bcb96 Mon Sep 17 00:00:00 2001 From: umpirsky Date: Thu, 3 Mar 2016 11:17:46 +0100 Subject: [PATCH 152/426] Initialize couscous --- .gitignore | 1 + couscous.yml | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 couscous.yml diff --git a/.gitignore b/.gitignore index 0c7e5ade0..b233594c4 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,4 @@ tests/adapters/* !tests/adapters/*.dist bin/* !bin/configure_test_env.sh +.couscous diff --git a/couscous.yml b/couscous.yml new file mode 100644 index 000000000..c43d70d94 --- /dev/null +++ b/couscous.yml @@ -0,0 +1,4 @@ +title: Gaufrette +subTitle: Filesystem abstraction layer +template: + url: https://github.com/CouscousPHP/Template-Dark From a763fed7bb2016449b41cc078690d6c7f5533e00 Mon Sep 17 00:00:00 2001 From: umpirsky Date: Thu, 3 Mar 2016 11:53:44 +0100 Subject: [PATCH 153/426] Update readme --- README.md | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/README.md b/README.md index 8007e09c6..4d85e0c60 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ Gaufrette ========= -Gaufrette is a PHP5 library that provides a filesystem abstraction layer. +Gaufrette provides a filesystem abstraction layer. This project does not have any stable release yet but we do not want to break BC now. @@ -31,7 +31,6 @@ solution. Try it! ------- - ```bash php composer.phar require knplabs/gaufrette:~0.2 # Stable version php composer.phar require knplabs/gaufrette:0.4.*@dev # Development version @@ -59,11 +58,6 @@ $file = $filesystem->get('myFile'); echo sprintf('%s (modified %s): %s', $file->getKey(), date('d/m/Y, H:i:s', $file->getMtime()), $file->getContent()); ``` -Running the Tests ------------------ - -The tests use phpspec2 and PHPUnit. - ### Setup the vendor libraries As some filesystem adapters use vendor libraries, you should install the vendors: From 4f3134f6cec12f31f60dbfa93ef0f6ca69b50113 Mon Sep 17 00:00:00 2001 From: umpirsky Date: Thu, 3 Mar 2016 11:56:03 +0100 Subject: [PATCH 154/426] Move note to the bottom --- README.md | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 4d85e0c60..27cb81e53 100644 --- a/README.md +++ b/README.md @@ -3,14 +3,10 @@ Gaufrette Gaufrette provides a filesystem abstraction layer. -This project does not have any stable release yet but we do not want to break BC now. - [![Build Status](https://secure.travis-ci.org/KnpLabs/Gaufrette.png)](http://travis-ci.org/KnpLabs/Gaufrette) [![Join the chat at https://gitter.im/KnpLabs/Gaufrette](https://badges.gitter.im/KnpLabs/Gaufrette.svg)](https://gitter.im/KnpLabs/Gaufrette) [![Stories in Ready](https://badge.waffle.io/knplabs/gaufrette.png?label=ready&title=Ready)](https://waffle.io/knplabs/gaufrette) -Symfony integration is available here: [KnpLabs/KnpGaufretteBundle](https://github.com/KnpLabs/KnpGaufretteBundle). - Documentation is available [here](doc/index.md). Why use Gaufrette? @@ -58,6 +54,10 @@ $file = $filesystem->get('myFile'); echo sprintf('%s (modified %s): %s', $file->getKey(), date('d/m/Y, H:i:s', $file->getMtime()), $file->getContent()); ``` +### Symfony integration + +Symfony integration is available through [KnpLabs/KnpGaufretteBundle](https://github.com/KnpLabs/KnpGaufretteBundle). + ### Setup the vendor libraries As some filesystem adapters use vendor libraries, you should install the vendors: @@ -81,3 +81,7 @@ To check basic functionality of the adapters (adapters should be configured you $ bin/phpunit Is it green? + +### Note + +This project does not have any stable release yet but we do not want to break BC now. From c01909e5a873d3afd187ee446bbbb2ff6f6dc6b8 Mon Sep 17 00:00:00 2001 From: umpirsky Date: Thu, 3 Mar 2016 12:00:18 +0100 Subject: [PATCH 155/426] Reorganize readme --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 27cb81e53..a2cab7892 100644 --- a/README.md +++ b/README.md @@ -7,8 +7,6 @@ Gaufrette provides a filesystem abstraction layer. [![Join the chat at https://gitter.im/KnpLabs/Gaufrette](https://badges.gitter.im/KnpLabs/Gaufrette.svg)](https://gitter.im/KnpLabs/Gaufrette) [![Stories in Ready](https://badge.waffle.io/knplabs/gaufrette.png?label=ready&title=Ready)](https://waffle.io/knplabs/gaufrette) -Documentation is available [here](doc/index.md). - Why use Gaufrette? ------------------ @@ -54,6 +52,10 @@ $file = $filesystem->get('myFile'); echo sprintf('%s (modified %s): %s', $file->getKey(), date('d/m/Y, H:i:s', $file->getMtime()), $file->getContent()); ``` +### Documentation + +Read the official [Gaufrette documentation](doc/index.md). + ### Symfony integration Symfony integration is available through [KnpLabs/KnpGaufretteBundle](https://github.com/KnpLabs/KnpGaufretteBundle). From 0e6eef9c04197eedbb30a4a3ff1a7471936fbf7d Mon Sep 17 00:00:00 2001 From: umpirsky Date: Thu, 3 Mar 2016 12:03:11 +0100 Subject: [PATCH 156/426] Nice touch to code example --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a2cab7892..7d17342e5 100644 --- a/README.md +++ b/README.md @@ -48,8 +48,8 @@ $filesystem->write('myFile', 'Hello world!'); // Or use File objects $file = $filesystem->get('myFile'); -// Will print something like: "myFile (modified 17/01/2016 18:40:36): Hello world!" echo sprintf('%s (modified %s): %s', $file->getKey(), date('d/m/Y, H:i:s', $file->getMtime()), $file->getContent()); +// Will print something like: "myFile (modified 17/01/2016 18:40:36): Hello world!" ``` ### Documentation From 1305c467afd47ec2a2fb059b8180de8867ab250d Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Thu, 3 Mar 2016 13:04:38 +0200 Subject: [PATCH 157/426] Fix phpspec version and predictions --- composer.json | 2 +- spec/Gaufrette/Adapter/PhpseclibSftpSpec.php | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 1751115c0..618ef5f1f 100644 --- a/composer.json +++ b/composer.json @@ -30,7 +30,7 @@ "amazonwebservices/aws-sdk-for-php": "1.5.*", "rackspace/php-opencloud" : "1.9.*", "google/apiclient": "~1.1.3", - "phpspec/phpspec": "2.0.*", + "phpspec/phpspec": "~2.4", "phpseclib/phpseclib": "^2.0", "doctrine/dbal": ">=2.3", "dropbox-php/dropbox-php": "*", diff --git a/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php b/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php index 31419849e..e75b6feb6 100644 --- a/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php +++ b/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php @@ -21,7 +21,6 @@ class PhpseclibSftpSpec extends ObjectBehavior function let($sftp) { $this->beConstructedWith($sftp, '/home/l3l0', false, 'l3lo', 'password'); - $sftp->login('l3lo', 'password')->shouldBeCalled(); } function it_is_adapter() From f1c478166de6ae607be72b051ac1437ae2e2711d Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Thu, 3 Mar 2016 13:16:50 +0200 Subject: [PATCH 158/426] Drop ssh2 extension in builds and switch to travis docker-based infra --- .travis.yml | 7 +++++-- bin/configure_test_env.sh | 17 ----------------- 2 files changed, 5 insertions(+), 19 deletions(-) delete mode 100755 bin/configure_test_env.sh diff --git a/.travis.yml b/.travis.yml index 6c2c72640..ae0eea633 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,7 @@ language: php +sudo: false + php: - 5.3 - 5.4 @@ -12,8 +14,9 @@ matrix: - php: hhvm before_script: - - ./bin/configure_test_env.sh - - composer self-update && composer install --dev --prefer-source --no-interaction + - echo "extension = mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini + - cp tests/Gaufrette/Functional/adapters/DoctrineDbal.php.dist tests/Gaufrette/Functional/adapters/DoctrineDbal.php -f + - composer self-update && composer install --dev --prefer-source --no-interaction --ignore-platform-reqs script: - php bin/phpspec run -fpretty --verbose diff --git a/bin/configure_test_env.sh b/bin/configure_test_env.sh deleted file mode 100755 index 5fb5feef0..000000000 --- a/bin/configure_test_env.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/bin/bash - -sudo apt-get update -qq -sudo apt-get install -qq libssh2-1-dev libssh2-php -echo "extension = mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini -touch .interactive -(pecl install -f ssh2 < .interactive) - -if grep -Fxq "ssh2" ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini -then - echo "SSH2 extension was already installed. "; -else - echo "SSH2 was not enabled during installation. Enabling it manually"; - echo "extension = ssh2.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini -fi - -cp tests/Gaufrette/Functional/adapters/DoctrineDbal.php.dist tests/Gaufrette/Functional/adapters/DoctrineDbal.php -f From 402bfe35dd0136e46b0bf3b8a0e213c447ff6540 Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Thu, 3 Mar 2016 13:18:02 +0200 Subject: [PATCH 159/426] Plug composer cache --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index ae0eea633..612b07859 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,10 @@ language: php sudo: false +cache: + directories: + - $HOME/.composer/cache + php: - 5.3 - 5.4 From afccf7fe1a7cea56dca75ff122d7166ec7608caf Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Thu, 3 Mar 2016 13:19:39 +0200 Subject: [PATCH 160/426] Enable fast-finish --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 612b07859..269e11c98 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,6 +16,7 @@ php: matrix: allow_failures: - php: hhvm + fast_finish: true before_script: - echo "extension = mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini From ad032e7ec2b19a9d639d3f409bad1b5c66a388e9 Mon Sep 17 00:00:00 2001 From: umpirsky Date: Thu, 3 Mar 2016 12:49:03 +0100 Subject: [PATCH 161/426] Setup couscous doc structure --- .gitignore | 2 +- README.md | 5 ----- couscous.yml | 4 ---- doc/basic-usage.md | 27 +++++++++++++++++++++++++++ doc/couscous.yml | 18 ++++++++++++++++++ doc/{index.md => index.bak.md} | 5 +---- doc/installation.md | 12 ++++++++++++ doc/introduction.md | 21 +++++++++++++++++++++ 8 files changed, 80 insertions(+), 14 deletions(-) delete mode 100644 couscous.yml create mode 100644 doc/basic-usage.md create mode 100644 doc/couscous.yml rename doc/{index.md => index.bak.md} (96%) create mode 100644 doc/installation.md create mode 100644 doc/introduction.md diff --git a/.gitignore b/.gitignore index b233594c4..81c6ed309 100644 --- a/.gitignore +++ b/.gitignore @@ -5,4 +5,4 @@ tests/adapters/* !tests/adapters/*.dist bin/* !bin/configure_test_env.sh -.couscous +doc/.couscous diff --git a/README.md b/README.md index 7d17342e5..59e70d75b 100644 --- a/README.md +++ b/README.md @@ -25,11 +25,6 @@ solution. Try it! ------- -```bash -php composer.phar require knplabs/gaufrette:~0.2 # Stable version -php composer.phar require knplabs/gaufrette:0.4.*@dev # Development version -``` - Following an example with the local filesystem adapter. To setup other adapters, look up [their respective documentation](https://github.com/KnpLabs/Gaufrette/tree/master/doc/#adapters). ```php diff --git a/couscous.yml b/couscous.yml deleted file mode 100644 index c43d70d94..000000000 --- a/couscous.yml +++ /dev/null @@ -1,4 +0,0 @@ -title: Gaufrette -subTitle: Filesystem abstraction layer -template: - url: https://github.com/CouscousPHP/Template-Dark diff --git a/doc/basic-usage.md b/doc/basic-usage.md new file mode 100644 index 000000000..6e62d8b98 --- /dev/null +++ b/doc/basic-usage.md @@ -0,0 +1,27 @@ +--- +currentMenu: basic-usage +--- + +# Basic Usage + +Following an example with the local filesystem adapter. To setup other adapters, look up [their respective documentation](https://github.com/KnpLabs/Gaufrette/tree/master/doc/#adapters). + +```php +read('myFile')); // bool(false) +$filesystem->write('myFile', 'Hello world!'); + +// Or use File objects +$file = $filesystem->get('myFile'); +echo sprintf('%s (modified %s): %s', $file->getKey(), date('d/m/Y, H:i:s', $file->getMtime()), $file->getContent()); +// Will print something like: "myFile (modified 17/01/2016 18:40:36): Hello world!" +``` diff --git a/doc/couscous.yml b/doc/couscous.yml new file mode 100644 index 000000000..b7979af3b --- /dev/null +++ b/doc/couscous.yml @@ -0,0 +1,18 @@ +title: Gaufrette +subTitle: Filesystem abstraction layer +template: + url: https://github.com/CouscousPHP/Template-Dark + index: introduction.md +menu: + sections: + main: + name: Table of Contents + items: + introduction: + text: Introduction + installation: + text: Installation + relativeUrl: installation.html + basic-usage: + text: Basic Usage + relativeUrl: basic-usage.html diff --git a/doc/index.md b/doc/index.bak.md similarity index 96% rename from doc/index.md rename to doc/index.bak.md index 8497553bc..e27b5f528 100644 --- a/doc/index.md +++ b/doc/index.bak.md @@ -1,6 +1,3 @@ -Gaufrette Documentation -============= - Adapters -------- @@ -58,7 +55,7 @@ The third parameter of the cache adapter is the time to live of the cache. Streaming Files --------------- -Sometimes, you don't have the choice, you must get a streamable file URL (i.e to use native file functions). +Sometimes, you don't have the choice, you must get a streamable file URL (i.e to use native file functions). Let's take a look at the following example: ```php diff --git a/doc/installation.md b/doc/installation.md new file mode 100644 index 000000000..7328224fd --- /dev/null +++ b/doc/installation.md @@ -0,0 +1,12 @@ +--- +currentMenu: installation +--- + +# Installation + +You can install it through Composer: + +```bash +$ composer require knplabs/gaufrette:~0.2 # Stable version +$ composer require knplabs/gaufrette:0.4.*@dev # Development version +``` diff --git a/doc/introduction.md b/doc/introduction.md new file mode 100644 index 000000000..e86e042bb --- /dev/null +++ b/doc/introduction.md @@ -0,0 +1,21 @@ +--- +currentMenu: introduction +--- + +# Gaufrette + +Gaufrette provides a filesystem abstraction layer. + +## Why use Gaufrette? + +Imagine you have to manage a lot of medias in a PHP project. Lets see how to +take this situation in your advantage using Gaufrette. + +The filesystem abstraction layer permits you to develop your application without +the need to know were all those medias will be stored and how. + +Another advantage of this is the possibility to update the files location +without any impact on the code apart from the definition of your filesystem. +In example, if your project grows up very fast and if your server reaches its +limits, you can easily move your medias in an Amazon S3 server or any other +solution. From 96aeebd171d1817da130cd0f43bb026a447694f8 Mon Sep 17 00:00:00 2001 From: umpirsky Date: Thu, 3 Mar 2016 12:50:03 +0100 Subject: [PATCH 162/426] Remove try it from readme --- README.md | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/README.md b/README.md index 59e70d75b..69f1123a2 100644 --- a/README.md +++ b/README.md @@ -22,31 +22,6 @@ In example, if your project grows up very fast and if your server reaches its limits, you can easily move your medias in an Amazon S3 server or any other solution. -Try it! -------- - -Following an example with the local filesystem adapter. To setup other adapters, look up [their respective documentation](https://github.com/KnpLabs/Gaufrette/tree/master/doc/#adapters). - -```php -read('myFile')); // bool(false) -$filesystem->write('myFile', 'Hello world!'); - -// Or use File objects -$file = $filesystem->get('myFile'); -echo sprintf('%s (modified %s): %s', $file->getKey(), date('d/m/Y, H:i:s', $file->getMtime()), $file->getContent()); -// Will print something like: "myFile (modified 17/01/2016 18:40:36): Hello world!" -``` - ### Documentation Read the official [Gaufrette documentation](doc/index.md). From 5b03945da43c76bbd02b97e5c98b26a60304b8c3 Mon Sep 17 00:00:00 2001 From: umpirsky Date: Thu, 3 Mar 2016 12:54:32 +0100 Subject: [PATCH 163/426] Added caching and streaming page --- doc/caching.md | 35 +++++++++++++++++++++++++++++++ doc/couscous.yml | 6 ++++++ doc/index.bak.md | 54 ------------------------------------------------ doc/streaming.md | 23 +++++++++++++++++++++ 4 files changed, 64 insertions(+), 54 deletions(-) create mode 100644 doc/caching.md create mode 100644 doc/streaming.md diff --git a/doc/caching.md b/doc/caching.md new file mode 100644 index 000000000..a8436fd45 --- /dev/null +++ b/doc/caching.md @@ -0,0 +1,35 @@ +--- +currentMenu: caching +--- + +# Caching + +If you have to deal with a slow filesystem, it is out of question to use it directly. +So, you need a cache! Happily, Gaufrette offers a cache system ready for use. +It consist of an adapter decorator itself composed of two adapters: + + * The *source* adapter that should be cached + * The *cache* adapter that is used to cache + +Here is an example of how to cache an ftp filesystem: + +```php + 'Hello World!')); -$filesystem = new Filesystem($adapter); - -$map = StreamWrapper::getFilesystemMap(); -$map->set('foo', $filesystem); - -StreamWrapper::register(); - -copy('gaufrette://foo/hello.txt', 'gaufrette://foo/world.txt'); -unlink('gaufrette://foo/hello.txt'); - -echo file_get_contents('gaufrette://foo/world.txt'); // Says "Hello World!" -``` diff --git a/doc/streaming.md b/doc/streaming.md new file mode 100644 index 000000000..56a590f93 --- /dev/null +++ b/doc/streaming.md @@ -0,0 +1,23 @@ +--- +currentMenu: streaming +--- + +# Streaming + +Sometimes, you don't have the choice, you must get a streamable file URL (i.e to use native file functions). +Let's take a look at the following example: + +```php +$adapter = new InMemoryAdapter(array('hello.txt' => 'Hello World!')); +$filesystem = new Filesystem($adapter); + +$map = StreamWrapper::getFilesystemMap(); +$map->set('foo', $filesystem); + +StreamWrapper::register(); + +copy('gaufrette://foo/hello.txt', 'gaufrette://foo/world.txt'); +unlink('gaufrette://foo/hello.txt'); + +echo file_get_contents('gaufrette://foo/world.txt'); // Says "Hello World!" +``` From 8842982ba0ebda1774f658926d90bfbb09060e18 Mon Sep 17 00:00:00 2001 From: umpirsky Date: Thu, 3 Mar 2016 14:47:02 +0100 Subject: [PATCH 164/426] Add adapters to the doc --- doc/adapters/{amazonS3.md => amazon-s3.md} | 3 +- doc/adapters/awsS3.md | 3 +- doc/couscous.yml | 45 ++++++++++++++++++++++ 3 files changed, 47 insertions(+), 4 deletions(-) rename doc/adapters/{amazonS3.md => amazon-s3.md} (89%) diff --git a/doc/adapters/amazonS3.md b/doc/adapters/amazon-s3.md similarity index 89% rename from doc/adapters/amazonS3.md rename to doc/adapters/amazon-s3.md index 7319139be..1c704b41f 100644 --- a/doc/adapters/amazonS3.md +++ b/doc/adapters/amazon-s3.md @@ -1,5 +1,4 @@ -Amazon S3 & AclAwareAmazonS3 -============================ +# AmazonS3 & AclAwareAmazonS3 When using the legacy Amazon S3 adapters (aws-sdk-php < 2), you will need to specify a CA certificate to be able to talk to Amazon servers in https. You can use diff --git a/doc/adapters/awsS3.md b/doc/adapters/awsS3.md index 002789240..db3baead4 100644 --- a/doc/adapters/awsS3.md +++ b/doc/adapters/awsS3.md @@ -1,5 +1,4 @@ -Amazon S3 -========= +# AWS S3 First, you will need to install AWS SDK for PHP: ```bash diff --git a/doc/couscous.yml b/doc/couscous.yml index c9dc3f048..4beaa9d83 100644 --- a/doc/couscous.yml +++ b/doc/couscous.yml @@ -1,5 +1,8 @@ title: Gaufrette subTitle: Filesystem abstraction layer +github: + user: KnpLabs + repo: Gaufrette template: url: https://github.com/CouscousPHP/Template-Dark index: introduction.md @@ -22,3 +25,45 @@ menu: streaming: text: Streaming relativeUrl: streaming.html + adapters: + name: Adapters + items: + amazonS3: + text: AmazonS3 + relativeUrl: adapters/amazon-s3.html + apc: + text: Apc + relativeUrl: adapters/apc.html + awss3: + text: AWS S3 + relativeUrl: adapters/awss3.html + azureBlobStorage: + text: AzureBlobStorage + relativeUrl: adapters/azureBlobStorage.html + gtp: + text: Ftp + relativeUrl: adapters/ftp.html + googleCloudStorage: + text: Google Cloud Storage + relativeUrl: adapters/googleCloudStorage.html + gridFs: + text: GridFS + relativeUrl: adapters/gridFs.html + InMemory: + text: InMemory + relativeUrl: adapters/inMemory.html + local: + text: Local & SafeLocal + relativeUrl: adapters/local.html + openCloud: + text: OpenCloud & LazyOpenCloud + relativeUrl: adapters/openCloud.html + phpseclibSftp: + text: PhpseclibSftp + relativeUrl: adapters/phpseclibSftp.html + sftp: + text: Sftp + relativeUrl: adapters/sftp.html + zip: + text: Zip + relativeUrl: adapters/zip.html From c6c77dc659b1565be7f960d4150c9ceef144f53b Mon Sep 17 00:00:00 2001 From: umpirsky Date: Thu, 3 Mar 2016 15:29:07 +0100 Subject: [PATCH 165/426] Update some adapter pages --- doc/adapters/apc.md | 6 ++---- doc/adapters/awsS3.md | 3 +-- doc/adapters/azureBlobStorage.md | 6 ++---- doc/adapters/doctrineDbal.md | 4 ---- doc/adapters/dropbox.md | 4 ---- doc/adapters/ftp.md | 11 ++++------- 6 files changed, 9 insertions(+), 25 deletions(-) delete mode 100644 doc/adapters/doctrineDbal.md delete mode 100644 doc/adapters/dropbox.md diff --git a/doc/adapters/apc.md b/doc/adapters/apc.md index 9b4d484dd..01344a28f 100644 --- a/doc/adapters/apc.md +++ b/doc/adapters/apc.md @@ -1,10 +1,8 @@ -APC -=== +# APC `apc` extension should be enabled in order to use this adapter. -Example -------- +## Example `Apc` adpater takes only two arguments : * the first, mandatory, is a prefix to avoid conflicts between filesystems diff --git a/doc/adapters/awsS3.md b/doc/adapters/awsS3.md index db3baead4..e60b7b3d7 100644 --- a/doc/adapters/awsS3.md +++ b/doc/adapters/awsS3.md @@ -5,8 +5,7 @@ First, you will need to install AWS SDK for PHP: composer require aws/aws-sdk-php ``` -Example -------- +## Example ```php Date: Thu, 3 Mar 2016 15:33:57 +0100 Subject: [PATCH 166/426] Override template until https://github.com/CouscousPHP/Template-Dark/pull/5 is merged --- doc/couscous.yml | 2 +- doc/website/.gitignore | 1 + doc/website/LICENSE | 18 + doc/website/README.md | 71 +++ doc/website/couscous.yml | 27 + doc/website/css/bootstrap.min.css | 7 + doc/website/css/font-awesome.min.css | 4 + doc/website/css/highlight.tomorrow-night.css | 45 ++ doc/website/css/main.css | 98 ++++ doc/website/default.twig | 80 +++ doc/website/fonts/FontAwesome.otf | Bin 0 -> 85908 bytes doc/website/fonts/fontawesome-webfont.eot | Bin 0 -> 56006 bytes doc/website/fonts/fontawesome-webfont.svg | 520 +++++++++++++++++++ doc/website/fonts/fontawesome-webfont.ttf | Bin 0 -> 112160 bytes doc/website/fonts/fontawesome-webfont.woff | Bin 0 -> 65452 bytes doc/website/screenshot.png | Bin 0 -> 245289 bytes 16 files changed, 872 insertions(+), 1 deletion(-) create mode 100644 doc/website/.gitignore create mode 100644 doc/website/LICENSE create mode 100644 doc/website/README.md create mode 100644 doc/website/couscous.yml create mode 100644 doc/website/css/bootstrap.min.css create mode 100644 doc/website/css/font-awesome.min.css create mode 100644 doc/website/css/highlight.tomorrow-night.css create mode 100644 doc/website/css/main.css create mode 100644 doc/website/default.twig create mode 100644 doc/website/fonts/FontAwesome.otf create mode 100644 doc/website/fonts/fontawesome-webfont.eot create mode 100644 doc/website/fonts/fontawesome-webfont.svg create mode 100644 doc/website/fonts/fontawesome-webfont.ttf create mode 100644 doc/website/fonts/fontawesome-webfont.woff create mode 100644 doc/website/screenshot.png diff --git a/doc/couscous.yml b/doc/couscous.yml index 4beaa9d83..08ee74226 100644 --- a/doc/couscous.yml +++ b/doc/couscous.yml @@ -4,7 +4,7 @@ github: user: KnpLabs repo: Gaufrette template: - url: https://github.com/CouscousPHP/Template-Dark + # url: https://github.com/CouscousPHP/Template-Dark index: introduction.md menu: sections: diff --git a/doc/website/.gitignore b/doc/website/.gitignore new file mode 100644 index 000000000..9ca439cb4 --- /dev/null +++ b/doc/website/.gitignore @@ -0,0 +1 @@ +.couscous/ diff --git a/doc/website/LICENSE b/doc/website/LICENSE new file mode 100644 index 000000000..1d7c7cc50 --- /dev/null +++ b/doc/website/LICENSE @@ -0,0 +1,18 @@ +Couscous + +Copyright (C) Matthieu Napoli + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, +including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, +subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT +NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/doc/website/README.md b/doc/website/README.md new file mode 100644 index 000000000..d98582001 --- /dev/null +++ b/doc/website/README.md @@ -0,0 +1,71 @@ +--- +currentMenu: home +--- +# Couscous Dark template + +![](screenshot.png) + +## Usage + +To use the template, set it up in your `couscous.yml` configuration file: + +```yaml +template: + url: https://github.com/CouscousPHP/Template-Dark +``` + +## Configuration + +Here are all the variables you can set in your `couscous.yml`: + +```yaml +# Base URL of the published website +baseUrl: http://username.github.io/project + +# Used to link to the GitHub project +github: + user: myself + repo: my-project + +title: My project +subTitle: This is a great project. + +# The left menu bar +menu: + sections: + main: + name: Main documentation + items: + home: + text: Home page + # You can use relative urls + relativeUrl: doc/faq.html + foo: + text: Another link + # Or absolute urls + absoluteUrl: https://example.com + other: + name: Other topics + items: +``` + +Note that the menu items can also contain HTML: + +```yaml +home: + text: " Home page" + relativeUrl: doc/faq.html +``` + +## Menu + +To set the current menu item (i.e. highlighted menu item), set the `currentMenu` +key in the Markdown files: + +```markdown +--- +currentMenu: home +--- + +# Welcome +``` diff --git a/doc/website/couscous.yml b/doc/website/couscous.yml new file mode 100644 index 000000000..b482893ea --- /dev/null +++ b/doc/website/couscous.yml @@ -0,0 +1,27 @@ +template: + directory: . + +github: + user: CouscousPHP + repo: Template-Dark + +title: Couscous template +subTitle: The dark template for Couscous. + +# The left menu bar +menu: + sections: + main: + name: Main documentation + items: + home: + text: Home page + # You can use relative urls + relativeUrl: doc/faq.html + foo: + text: Another link + # Or absolute urls + absoluteUrl: https://example.com + other: + name: Other topics + items: diff --git a/doc/website/css/bootstrap.min.css b/doc/website/css/bootstrap.min.css new file mode 100644 index 000000000..1020513c2 --- /dev/null +++ b/doc/website/css/bootstrap.min.css @@ -0,0 +1,7 @@ +@import url("//fonts.googleapis.com/css?family=Lato:300,400,700");/*! + * Bootswatch v3.1.1+1 + * Homepage: http://bootswatch.com + * Copyright 2012-2014 Thomas Park + * Licensed under MIT + * Based on Bootstrap +*//*! normalize.css v3.0.0 | MIT License | git.io/normalize */html{font-family:sans-serif;-ms-text-size-adjust:100%;-webkit-text-size-adjust:100%}body{margin:0}article,aside,details,figcaption,figure,footer,header,hgroup,main,nav,section,summary{display:block}audio,canvas,progress,video{display:inline-block;vertical-align:baseline}audio:not([controls]){display:none;height:0}[hidden],template{display:none}a{background:transparent}a:active,a:hover{outline:0}abbr[title]{border-bottom:1px dotted}b,strong{font-weight:bold}dfn{font-style:italic}h1{font-size:2em;margin:0.67em 0}mark{background:#ff0;color:#000}small{font-size:80%}sub,sup{font-size:75%;line-height:0;position:relative;vertical-align:baseline}sup{top:-0.5em}sub{bottom:-0.25em}img{border:0}svg:not(:root){overflow:hidden}figure{margin:1em 40px}hr{-moz-box-sizing:content-box;box-sizing:content-box;height:0}pre{overflow:auto}code,kbd,pre,samp{font-family:monospace, monospace;font-size:1em}button,input,optgroup,select,textarea{color:inherit;font:inherit;margin:0}button{overflow:visible}button,select{text-transform:none}button,html input[type="button"],input[type="reset"],input[type="submit"]{-webkit-appearance:button;cursor:pointer}button[disabled],html input[disabled]{cursor:default}button::-moz-focus-inner,input::-moz-focus-inner{border:0;padding:0}input{line-height:normal}input[type="checkbox"],input[type="radio"]{box-sizing:border-box;padding:0}input[type="number"]::-webkit-inner-spin-button,input[type="number"]::-webkit-outer-spin-button{height:auto}input[type="search"]{-webkit-appearance:textfield;-moz-box-sizing:content-box;-webkit-box-sizing:content-box;box-sizing:content-box}input[type="search"]::-webkit-search-cancel-button,input[type="search"]::-webkit-search-decoration{-webkit-appearance:none}fieldset{border:1px solid #c0c0c0;margin:0 2px;padding:0.35em 0.625em 0.75em}legend{border:0;padding:0}textarea{overflow:auto}optgroup{font-weight:bold}table{border-collapse:collapse;border-spacing:0}td,th{padding:0}@media print{*{text-shadow:none !important;color:#000 !important;background:transparent !important;box-shadow:none !important}a,a:visited{text-decoration:underline}a[href]:after{content:" (" attr(href) ")"}abbr[title]:after{content:" (" attr(title) ")"}a[href^="javascript:"]:after,a[href^="#"]:after{content:""}pre,blockquote{border:1px solid #999;page-break-inside:avoid}thead{display:table-header-group}tr,img{page-break-inside:avoid}img{max-width:100% !important}p,h2,h3{orphans:3;widows:3}h2,h3{page-break-after:avoid}select{background:#fff !important}.navbar{display:none}.table td,.table th{background-color:#fff !important}.btn>.caret,.dropup>.btn>.caret{border-top-color:#000 !important}.label{border:1px solid #000}.table{border-collapse:collapse !important}.table-bordered th,.table-bordered td{border:1px solid #ddd !important}}*{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}*:before,*:after{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}html{font-size:62.5%;-webkit-tap-highlight-color:rgba(0,0,0,0)}body{font-family:"Lato","Helvetica Neue",Helvetica,Arial,sans-serif;font-size:15px;line-height:1.42857143;color:#ebebeb;background-color:#2b3e50}input,button,select,textarea{font-family:inherit;font-size:inherit;line-height:inherit}a{color:#df691a;text-decoration:none}a:hover,a:focus{color:#df691a;text-decoration:underline}a:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}figure{margin:0}img{vertical-align:middle}.img-responsive,.thumbnail>img,.thumbnail a>img,.carousel-inner>.item>img,.carousel-inner>.item>a>img{display:block;max-width:100%;height:auto}.img-rounded{border-radius:0}.img-thumbnail{padding:4px;line-height:1.42857143;background-color:#2b3e50;border:1px solid #dddddd;border-radius:0;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out;display:inline-block;max-width:100%;height:auto}.img-circle{border-radius:50%}hr{margin-top:21px;margin-bottom:21px;border:0;border-top:1px solid #4e5d6c}.sr-only{position:absolute;width:1px;height:1px;margin:-1px;padding:0;overflow:hidden;clip:rect(0, 0, 0, 0);border:0}h1,h2,h3,h4,h5,h6,.h1,.h2,.h3,.h4,.h5,.h6{font-family:inherit;font-weight:300;line-height:1.1;color:inherit}h1 small,h2 small,h3 small,h4 small,h5 small,h6 small,.h1 small,.h2 small,.h3 small,.h4 small,.h5 small,.h6 small,h1 .small,h2 .small,h3 .small,h4 .small,h5 .small,h6 .small,.h1 .small,.h2 .small,.h3 .small,.h4 .small,.h5 .small,.h6 .small{font-weight:normal;line-height:1;color:#ebebeb}h1,.h1,h2,.h2,h3,.h3{margin-top:21px;margin-bottom:10.5px}h1 small,.h1 small,h2 small,.h2 small,h3 small,.h3 small,h1 .small,.h1 .small,h2 .small,.h2 .small,h3 .small,.h3 .small{font-size:65%}h4,.h4,h5,.h5,h6,.h6{margin-top:10.5px;margin-bottom:10.5px}h4 small,.h4 small,h5 small,.h5 small,h6 small,.h6 small,h4 .small,.h4 .small,h5 .small,.h5 .small,h6 .small,.h6 .small{font-size:75%}h1,.h1{font-size:39px}h2,.h2{font-size:32px}h3,.h3{font-size:26px}h4,.h4{font-size:19px}h5,.h5{font-size:15px}h6,.h6{font-size:13px}p{margin:0 0 10.5px}.lead{margin-bottom:21px;font-size:17px;font-weight:200;line-height:1.4}@media (min-width:768px){.lead{font-size:22.5px}}small,.small{font-size:85%}cite{font-style:normal}.text-left{text-align:left}.text-right{text-align:right}.text-center{text-align:center}.text-justify{text-align:justify}.text-muted{color:#4e5d6c}.text-primary{color:#df691a}a.text-primary:hover{color:#b15315}.text-success{color:#ebebeb}a.text-success:hover{color:#d2d2d2}.text-info{color:#ebebeb}a.text-info:hover{color:#d2d2d2}.text-warning{color:#ebebeb}a.text-warning:hover{color:#d2d2d2}.text-danger{color:#ebebeb}a.text-danger:hover{color:#d2d2d2}.bg-primary{color:#fff;background-color:#df691a}a.bg-primary:hover{background-color:#b15315}.bg-success{background-color:#5cb85c}a.bg-success:hover{background-color:#449d44}.bg-info{background-color:#5bc0de}a.bg-info:hover{background-color:#31b0d5}.bg-warning{background-color:#f0ad4e}a.bg-warning:hover{background-color:#ec971f}.bg-danger{background-color:#d9534f}a.bg-danger:hover{background-color:#c9302c}.page-header{padding-bottom:9.5px;margin:42px 0 21px;border-bottom:1px solid #ebebeb}ul,ol{margin-top:0;margin-bottom:10.5px}ul ul,ol ul,ul ol,ol ol{margin-bottom:0}.list-unstyled{padding-left:0;list-style:none}.list-inline{padding-left:0;list-style:none;margin-left:-5px}.list-inline>li{display:inline-block;padding-left:5px;padding-right:5px}dl{margin-top:0;margin-bottom:21px}dt,dd{line-height:1.42857143}dt{font-weight:bold}dd{margin-left:0}@media (min-width:768px){.dl-horizontal dt{float:left;width:160px;clear:left;text-align:right;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.dl-horizontal dd{margin-left:180px}}abbr[title],abbr[data-original-title]{cursor:help;border-bottom:1px dotted #4e5d6c}.initialism{font-size:90%;text-transform:uppercase}blockquote{padding:10.5px 21px;margin:0 0 21px;font-size:18.75px;border-left:5px solid #4e5d6c}blockquote p:last-child,blockquote ul:last-child,blockquote ol:last-child{margin-bottom:0}blockquote footer,blockquote small,blockquote .small{display:block;font-size:80%;line-height:1.42857143;color:#ebebeb}blockquote footer:before,blockquote small:before,blockquote .small:before{content:'\2014 \00A0'}.blockquote-reverse,blockquote.pull-right{padding-right:15px;padding-left:0;border-right:5px solid #4e5d6c;border-left:0;text-align:right}.blockquote-reverse footer:before,blockquote.pull-right footer:before,.blockquote-reverse small:before,blockquote.pull-right small:before,.blockquote-reverse .small:before,blockquote.pull-right .small:before{content:''}.blockquote-reverse footer:after,blockquote.pull-right footer:after,.blockquote-reverse small:after,blockquote.pull-right small:after,.blockquote-reverse .small:after,blockquote.pull-right .small:after{content:'\00A0 \2014'}blockquote:before,blockquote:after{content:""}address{margin-bottom:21px;font-style:normal;line-height:1.42857143}code,kbd,pre,samp{font-family:Menlo,Monaco,Consolas,"Courier New",monospace}code{padding:2px 4px;font-size:90%;color:#c7254e;background-color:#f9f2f4;white-space:nowrap;border-radius:0}kbd{padding:2px 4px;font-size:90%;color:#ffffff;background-color:#333333;border-radius:0;box-shadow:inset 0 -1px 0 rgba(0,0,0,0.25)}pre{display:block;padding:10px;margin:0 0 10.5px;font-size:14px;line-height:1.42857143;word-break:break-all;word-wrap:break-word;color:#333333;background-color:#f5f5f5;border:1px solid #cccccc;border-radius:0}pre code{padding:0;font-size:inherit;color:inherit;white-space:pre-wrap;background-color:transparent;border-radius:0}.pre-scrollable{max-height:340px;overflow-y:scroll}.container{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}@media (min-width:768px){.container{width:750px}}@media (min-width:992px){.container{width:970px}}@media (min-width:1200px){.container{width:1170px}}.container-fluid{margin-right:auto;margin-left:auto;padding-left:15px;padding-right:15px}.row{margin-left:-15px;margin-right:-15px}.col-xs-1,.col-sm-1,.col-md-1,.col-lg-1,.col-xs-2,.col-sm-2,.col-md-2,.col-lg-2,.col-xs-3,.col-sm-3,.col-md-3,.col-lg-3,.col-xs-4,.col-sm-4,.col-md-4,.col-lg-4,.col-xs-5,.col-sm-5,.col-md-5,.col-lg-5,.col-xs-6,.col-sm-6,.col-md-6,.col-lg-6,.col-xs-7,.col-sm-7,.col-md-7,.col-lg-7,.col-xs-8,.col-sm-8,.col-md-8,.col-lg-8,.col-xs-9,.col-sm-9,.col-md-9,.col-lg-9,.col-xs-10,.col-sm-10,.col-md-10,.col-lg-10,.col-xs-11,.col-sm-11,.col-md-11,.col-lg-11,.col-xs-12,.col-sm-12,.col-md-12,.col-lg-12{position:relative;min-height:1px;padding-left:15px;padding-right:15px}.col-xs-1,.col-xs-2,.col-xs-3,.col-xs-4,.col-xs-5,.col-xs-6,.col-xs-7,.col-xs-8,.col-xs-9,.col-xs-10,.col-xs-11,.col-xs-12{float:left}.col-xs-12{width:100%}.col-xs-11{width:91.66666667%}.col-xs-10{width:83.33333333%}.col-xs-9{width:75%}.col-xs-8{width:66.66666667%}.col-xs-7{width:58.33333333%}.col-xs-6{width:50%}.col-xs-5{width:41.66666667%}.col-xs-4{width:33.33333333%}.col-xs-3{width:25%}.col-xs-2{width:16.66666667%}.col-xs-1{width:8.33333333%}.col-xs-pull-12{right:100%}.col-xs-pull-11{right:91.66666667%}.col-xs-pull-10{right:83.33333333%}.col-xs-pull-9{right:75%}.col-xs-pull-8{right:66.66666667%}.col-xs-pull-7{right:58.33333333%}.col-xs-pull-6{right:50%}.col-xs-pull-5{right:41.66666667%}.col-xs-pull-4{right:33.33333333%}.col-xs-pull-3{right:25%}.col-xs-pull-2{right:16.66666667%}.col-xs-pull-1{right:8.33333333%}.col-xs-pull-0{right:0%}.col-xs-push-12{left:100%}.col-xs-push-11{left:91.66666667%}.col-xs-push-10{left:83.33333333%}.col-xs-push-9{left:75%}.col-xs-push-8{left:66.66666667%}.col-xs-push-7{left:58.33333333%}.col-xs-push-6{left:50%}.col-xs-push-5{left:41.66666667%}.col-xs-push-4{left:33.33333333%}.col-xs-push-3{left:25%}.col-xs-push-2{left:16.66666667%}.col-xs-push-1{left:8.33333333%}.col-xs-push-0{left:0%}.col-xs-offset-12{margin-left:100%}.col-xs-offset-11{margin-left:91.66666667%}.col-xs-offset-10{margin-left:83.33333333%}.col-xs-offset-9{margin-left:75%}.col-xs-offset-8{margin-left:66.66666667%}.col-xs-offset-7{margin-left:58.33333333%}.col-xs-offset-6{margin-left:50%}.col-xs-offset-5{margin-left:41.66666667%}.col-xs-offset-4{margin-left:33.33333333%}.col-xs-offset-3{margin-left:25%}.col-xs-offset-2{margin-left:16.66666667%}.col-xs-offset-1{margin-left:8.33333333%}.col-xs-offset-0{margin-left:0%}@media (min-width:768px){.col-sm-1,.col-sm-2,.col-sm-3,.col-sm-4,.col-sm-5,.col-sm-6,.col-sm-7,.col-sm-8,.col-sm-9,.col-sm-10,.col-sm-11,.col-sm-12{float:left}.col-sm-12{width:100%}.col-sm-11{width:91.66666667%}.col-sm-10{width:83.33333333%}.col-sm-9{width:75%}.col-sm-8{width:66.66666667%}.col-sm-7{width:58.33333333%}.col-sm-6{width:50%}.col-sm-5{width:41.66666667%}.col-sm-4{width:33.33333333%}.col-sm-3{width:25%}.col-sm-2{width:16.66666667%}.col-sm-1{width:8.33333333%}.col-sm-pull-12{right:100%}.col-sm-pull-11{right:91.66666667%}.col-sm-pull-10{right:83.33333333%}.col-sm-pull-9{right:75%}.col-sm-pull-8{right:66.66666667%}.col-sm-pull-7{right:58.33333333%}.col-sm-pull-6{right:50%}.col-sm-pull-5{right:41.66666667%}.col-sm-pull-4{right:33.33333333%}.col-sm-pull-3{right:25%}.col-sm-pull-2{right:16.66666667%}.col-sm-pull-1{right:8.33333333%}.col-sm-pull-0{right:0%}.col-sm-push-12{left:100%}.col-sm-push-11{left:91.66666667%}.col-sm-push-10{left:83.33333333%}.col-sm-push-9{left:75%}.col-sm-push-8{left:66.66666667%}.col-sm-push-7{left:58.33333333%}.col-sm-push-6{left:50%}.col-sm-push-5{left:41.66666667%}.col-sm-push-4{left:33.33333333%}.col-sm-push-3{left:25%}.col-sm-push-2{left:16.66666667%}.col-sm-push-1{left:8.33333333%}.col-sm-push-0{left:0%}.col-sm-offset-12{margin-left:100%}.col-sm-offset-11{margin-left:91.66666667%}.col-sm-offset-10{margin-left:83.33333333%}.col-sm-offset-9{margin-left:75%}.col-sm-offset-8{margin-left:66.66666667%}.col-sm-offset-7{margin-left:58.33333333%}.col-sm-offset-6{margin-left:50%}.col-sm-offset-5{margin-left:41.66666667%}.col-sm-offset-4{margin-left:33.33333333%}.col-sm-offset-3{margin-left:25%}.col-sm-offset-2{margin-left:16.66666667%}.col-sm-offset-1{margin-left:8.33333333%}.col-sm-offset-0{margin-left:0%}}@media (min-width:992px){.col-md-1,.col-md-2,.col-md-3,.col-md-4,.col-md-5,.col-md-6,.col-md-7,.col-md-8,.col-md-9,.col-md-10,.col-md-11,.col-md-12{float:left}.col-md-12{width:100%}.col-md-11{width:91.66666667%}.col-md-10{width:83.33333333%}.col-md-9{width:75%}.col-md-8{width:66.66666667%}.col-md-7{width:58.33333333%}.col-md-6{width:50%}.col-md-5{width:41.66666667%}.col-md-4{width:33.33333333%}.col-md-3{width:25%}.col-md-2{width:16.66666667%}.col-md-1{width:8.33333333%}.col-md-pull-12{right:100%}.col-md-pull-11{right:91.66666667%}.col-md-pull-10{right:83.33333333%}.col-md-pull-9{right:75%}.col-md-pull-8{right:66.66666667%}.col-md-pull-7{right:58.33333333%}.col-md-pull-6{right:50%}.col-md-pull-5{right:41.66666667%}.col-md-pull-4{right:33.33333333%}.col-md-pull-3{right:25%}.col-md-pull-2{right:16.66666667%}.col-md-pull-1{right:8.33333333%}.col-md-pull-0{right:0%}.col-md-push-12{left:100%}.col-md-push-11{left:91.66666667%}.col-md-push-10{left:83.33333333%}.col-md-push-9{left:75%}.col-md-push-8{left:66.66666667%}.col-md-push-7{left:58.33333333%}.col-md-push-6{left:50%}.col-md-push-5{left:41.66666667%}.col-md-push-4{left:33.33333333%}.col-md-push-3{left:25%}.col-md-push-2{left:16.66666667%}.col-md-push-1{left:8.33333333%}.col-md-push-0{left:0%}.col-md-offset-12{margin-left:100%}.col-md-offset-11{margin-left:91.66666667%}.col-md-offset-10{margin-left:83.33333333%}.col-md-offset-9{margin-left:75%}.col-md-offset-8{margin-left:66.66666667%}.col-md-offset-7{margin-left:58.33333333%}.col-md-offset-6{margin-left:50%}.col-md-offset-5{margin-left:41.66666667%}.col-md-offset-4{margin-left:33.33333333%}.col-md-offset-3{margin-left:25%}.col-md-offset-2{margin-left:16.66666667%}.col-md-offset-1{margin-left:8.33333333%}.col-md-offset-0{margin-left:0%}}@media (min-width:1200px){.col-lg-1,.col-lg-2,.col-lg-3,.col-lg-4,.col-lg-5,.col-lg-6,.col-lg-7,.col-lg-8,.col-lg-9,.col-lg-10,.col-lg-11,.col-lg-12{float:left}.col-lg-12{width:100%}.col-lg-11{width:91.66666667%}.col-lg-10{width:83.33333333%}.col-lg-9{width:75%}.col-lg-8{width:66.66666667%}.col-lg-7{width:58.33333333%}.col-lg-6{width:50%}.col-lg-5{width:41.66666667%}.col-lg-4{width:33.33333333%}.col-lg-3{width:25%}.col-lg-2{width:16.66666667%}.col-lg-1{width:8.33333333%}.col-lg-pull-12{right:100%}.col-lg-pull-11{right:91.66666667%}.col-lg-pull-10{right:83.33333333%}.col-lg-pull-9{right:75%}.col-lg-pull-8{right:66.66666667%}.col-lg-pull-7{right:58.33333333%}.col-lg-pull-6{right:50%}.col-lg-pull-5{right:41.66666667%}.col-lg-pull-4{right:33.33333333%}.col-lg-pull-3{right:25%}.col-lg-pull-2{right:16.66666667%}.col-lg-pull-1{right:8.33333333%}.col-lg-pull-0{right:0%}.col-lg-push-12{left:100%}.col-lg-push-11{left:91.66666667%}.col-lg-push-10{left:83.33333333%}.col-lg-push-9{left:75%}.col-lg-push-8{left:66.66666667%}.col-lg-push-7{left:58.33333333%}.col-lg-push-6{left:50%}.col-lg-push-5{left:41.66666667%}.col-lg-push-4{left:33.33333333%}.col-lg-push-3{left:25%}.col-lg-push-2{left:16.66666667%}.col-lg-push-1{left:8.33333333%}.col-lg-push-0{left:0%}.col-lg-offset-12{margin-left:100%}.col-lg-offset-11{margin-left:91.66666667%}.col-lg-offset-10{margin-left:83.33333333%}.col-lg-offset-9{margin-left:75%}.col-lg-offset-8{margin-left:66.66666667%}.col-lg-offset-7{margin-left:58.33333333%}.col-lg-offset-6{margin-left:50%}.col-lg-offset-5{margin-left:41.66666667%}.col-lg-offset-4{margin-left:33.33333333%}.col-lg-offset-3{margin-left:25%}.col-lg-offset-2{margin-left:16.66666667%}.col-lg-offset-1{margin-left:8.33333333%}.col-lg-offset-0{margin-left:0%}}table{max-width:100%;background-color:transparent}th{text-align:left}.table{width:100%;margin-bottom:21px}.table>thead>tr>th,.table>tbody>tr>th,.table>tfoot>tr>th,.table>thead>tr>td,.table>tbody>tr>td,.table>tfoot>tr>td{padding:6px;line-height:1.42857143;vertical-align:top;border-top:1px solid #4e5d6c}.table>thead>tr>th{vertical-align:bottom;border-bottom:2px solid #4e5d6c}.table>caption+thead>tr:first-child>th,.table>colgroup+thead>tr:first-child>th,.table>thead:first-child>tr:first-child>th,.table>caption+thead>tr:first-child>td,.table>colgroup+thead>tr:first-child>td,.table>thead:first-child>tr:first-child>td{border-top:0}.table>tbody+tbody{border-top:2px solid #4e5d6c}.table .table{background-color:#2b3e50}.table-condensed>thead>tr>th,.table-condensed>tbody>tr>th,.table-condensed>tfoot>tr>th,.table-condensed>thead>tr>td,.table-condensed>tbody>tr>td,.table-condensed>tfoot>tr>td{padding:3px}.table-bordered{border:1px solid #4e5d6c}.table-bordered>thead>tr>th,.table-bordered>tbody>tr>th,.table-bordered>tfoot>tr>th,.table-bordered>thead>tr>td,.table-bordered>tbody>tr>td,.table-bordered>tfoot>tr>td{border:1px solid #4e5d6c}.table-bordered>thead>tr>th,.table-bordered>thead>tr>td{border-bottom-width:2px}.table-striped>tbody>tr:nth-child(odd)>td,.table-striped>tbody>tr:nth-child(odd)>th{background-color:#4e5d6c}.table-hover>tbody>tr:hover>td,.table-hover>tbody>tr:hover>th{background-color:#485563}table col[class*="col-"]{position:static;float:none;display:table-column}table td[class*="col-"],table th[class*="col-"]{position:static;float:none;display:table-cell}.table>thead>tr>td.active,.table>tbody>tr>td.active,.table>tfoot>tr>td.active,.table>thead>tr>th.active,.table>tbody>tr>th.active,.table>tfoot>tr>th.active,.table>thead>tr.active>td,.table>tbody>tr.active>td,.table>tfoot>tr.active>td,.table>thead>tr.active>th,.table>tbody>tr.active>th,.table>tfoot>tr.active>th{background-color:#485563}.table-hover>tbody>tr>td.active:hover,.table-hover>tbody>tr>th.active:hover,.table-hover>tbody>tr.active:hover>td,.table-hover>tbody>tr.active:hover>th{background-color:#3d4954}.table>thead>tr>td.success,.table>tbody>tr>td.success,.table>tfoot>tr>td.success,.table>thead>tr>th.success,.table>tbody>tr>th.success,.table>tfoot>tr>th.success,.table>thead>tr.success>td,.table>tbody>tr.success>td,.table>tfoot>tr.success>td,.table>thead>tr.success>th,.table>tbody>tr.success>th,.table>tfoot>tr.success>th{background-color:#5cb85c}.table-hover>tbody>tr>td.success:hover,.table-hover>tbody>tr>th.success:hover,.table-hover>tbody>tr.success:hover>td,.table-hover>tbody>tr.success:hover>th{background-color:#4cae4c}.table>thead>tr>td.info,.table>tbody>tr>td.info,.table>tfoot>tr>td.info,.table>thead>tr>th.info,.table>tbody>tr>th.info,.table>tfoot>tr>th.info,.table>thead>tr.info>td,.table>tbody>tr.info>td,.table>tfoot>tr.info>td,.table>thead>tr.info>th,.table>tbody>tr.info>th,.table>tfoot>tr.info>th{background-color:#5bc0de}.table-hover>tbody>tr>td.info:hover,.table-hover>tbody>tr>th.info:hover,.table-hover>tbody>tr.info:hover>td,.table-hover>tbody>tr.info:hover>th{background-color:#46b8da}.table>thead>tr>td.warning,.table>tbody>tr>td.warning,.table>tfoot>tr>td.warning,.table>thead>tr>th.warning,.table>tbody>tr>th.warning,.table>tfoot>tr>th.warning,.table>thead>tr.warning>td,.table>tbody>tr.warning>td,.table>tfoot>tr.warning>td,.table>thead>tr.warning>th,.table>tbody>tr.warning>th,.table>tfoot>tr.warning>th{background-color:#f0ad4e}.table-hover>tbody>tr>td.warning:hover,.table-hover>tbody>tr>th.warning:hover,.table-hover>tbody>tr.warning:hover>td,.table-hover>tbody>tr.warning:hover>th{background-color:#eea236}.table>thead>tr>td.danger,.table>tbody>tr>td.danger,.table>tfoot>tr>td.danger,.table>thead>tr>th.danger,.table>tbody>tr>th.danger,.table>tfoot>tr>th.danger,.table>thead>tr.danger>td,.table>tbody>tr.danger>td,.table>tfoot>tr.danger>td,.table>thead>tr.danger>th,.table>tbody>tr.danger>th,.table>tfoot>tr.danger>th{background-color:#d9534f}.table-hover>tbody>tr>td.danger:hover,.table-hover>tbody>tr>th.danger:hover,.table-hover>tbody>tr.danger:hover>td,.table-hover>tbody>tr.danger:hover>th{background-color:#d43f3a}@media (max-width:767px){.table-responsive{width:100%;margin-bottom:15.75px;overflow-y:hidden;overflow-x:scroll;-ms-overflow-style:-ms-autohiding-scrollbar;border:1px solid #4e5d6c;-webkit-overflow-scrolling:touch}.table-responsive>.table{margin-bottom:0}.table-responsive>.table>thead>tr>th,.table-responsive>.table>tbody>tr>th,.table-responsive>.table>tfoot>tr>th,.table-responsive>.table>thead>tr>td,.table-responsive>.table>tbody>tr>td,.table-responsive>.table>tfoot>tr>td{white-space:nowrap}.table-responsive>.table-bordered{border:0}.table-responsive>.table-bordered>thead>tr>th:first-child,.table-responsive>.table-bordered>tbody>tr>th:first-child,.table-responsive>.table-bordered>tfoot>tr>th:first-child,.table-responsive>.table-bordered>thead>tr>td:first-child,.table-responsive>.table-bordered>tbody>tr>td:first-child,.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.table-responsive>.table-bordered>thead>tr>th:last-child,.table-responsive>.table-bordered>tbody>tr>th:last-child,.table-responsive>.table-bordered>tfoot>tr>th:last-child,.table-responsive>.table-bordered>thead>tr>td:last-child,.table-responsive>.table-bordered>tbody>tr>td:last-child,.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.table-responsive>.table-bordered>tbody>tr:last-child>th,.table-responsive>.table-bordered>tfoot>tr:last-child>th,.table-responsive>.table-bordered>tbody>tr:last-child>td,.table-responsive>.table-bordered>tfoot>tr:last-child>td{border-bottom:0}}fieldset{padding:0;margin:0;border:0;min-width:0}legend{display:block;width:100%;padding:0;margin-bottom:21px;font-size:22.5px;line-height:inherit;color:#ebebeb;border:0;border-bottom:1px solid #4e5d6c}label{display:inline-block;margin-bottom:5px;font-weight:bold}input[type="search"]{-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}input[type="radio"],input[type="checkbox"]{margin:4px 0 0;margin-top:1px \9;line-height:normal}input[type="file"]{display:block}input[type="range"]{display:block;width:100%}select[multiple],select[size]{height:auto}input[type="file"]:focus,input[type="radio"]:focus,input[type="checkbox"]:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}output{display:block;padding-top:9px;font-size:15px;line-height:1.42857143;color:#2b3e50}.form-control{display:block;width:100%;height:39px;padding:8px 16px;font-size:15px;line-height:1.42857143;color:#2b3e50;background-color:#ffffff;background-image:none;border:1px solid transparent;border-radius:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);-webkit-transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s;transition:border-color ease-in-out .15s,box-shadow ease-in-out .15s}.form-control:focus{border-color:transparent;outline:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(0,0,0,0.6);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 8px rgba(0,0,0,0.6)}.form-control::-moz-placeholder{color:#cccccc;opacity:1}.form-control:-ms-input-placeholder{color:#cccccc}.form-control::-webkit-input-placeholder{color:#cccccc}.form-control[disabled],.form-control[readonly],fieldset[disabled] .form-control{cursor:not-allowed;background-color:#ebebeb;opacity:1}textarea.form-control{height:auto}input[type="search"]{-webkit-appearance:none}input[type="date"]{line-height:39px}.form-group{margin-bottom:15px}.radio,.checkbox{display:block;min-height:21px;margin-top:10px;margin-bottom:10px;padding-left:20px}.radio label,.checkbox label{display:inline;font-weight:normal;cursor:pointer}.radio input[type="radio"],.radio-inline input[type="radio"],.checkbox input[type="checkbox"],.checkbox-inline input[type="checkbox"]{float:left;margin-left:-20px}.radio+.radio,.checkbox+.checkbox{margin-top:-5px}.radio-inline,.checkbox-inline{display:inline-block;padding-left:20px;margin-bottom:0;vertical-align:middle;font-weight:normal;cursor:pointer}.radio-inline+.radio-inline,.checkbox-inline+.checkbox-inline{margin-top:0;margin-left:10px}input[type="radio"][disabled],input[type="checkbox"][disabled],.radio[disabled],.radio-inline[disabled],.checkbox[disabled],.checkbox-inline[disabled],fieldset[disabled] input[type="radio"],fieldset[disabled] input[type="checkbox"],fieldset[disabled] .radio,fieldset[disabled] .radio-inline,fieldset[disabled] .checkbox,fieldset[disabled] .checkbox-inline{cursor:not-allowed}.input-sm{height:31px;padding:5px 10px;font-size:13px;line-height:1.5;border-radius:0}select.input-sm{height:31px;line-height:31px}textarea.input-sm,select[multiple].input-sm{height:auto}.input-lg{height:52px;padding:12px 24px;font-size:19px;line-height:1.33;border-radius:0}select.input-lg{height:52px;line-height:52px}textarea.input-lg,select[multiple].input-lg{height:auto}.has-feedback{position:relative}.has-feedback .form-control{padding-right:48.75px}.has-feedback .form-control-feedback{position:absolute;top:26px;right:0;display:block;width:39px;height:39px;line-height:39px;text-align:center}.has-success .help-block,.has-success .control-label,.has-success .radio,.has-success .checkbox,.has-success .radio-inline,.has-success .checkbox-inline{color:#ebebeb}.has-success .form-control{border-color:#ebebeb;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-success .form-control:focus{border-color:#d2d2d2;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff}.has-success .input-group-addon{color:#ebebeb;border-color:#ebebeb;background-color:#5cb85c}.has-success .form-control-feedback{color:#ebebeb}.has-warning .help-block,.has-warning .control-label,.has-warning .radio,.has-warning .checkbox,.has-warning .radio-inline,.has-warning .checkbox-inline{color:#ebebeb}.has-warning .form-control{border-color:#ebebeb;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-warning .form-control:focus{border-color:#d2d2d2;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff}.has-warning .input-group-addon{color:#ebebeb;border-color:#ebebeb;background-color:#f0ad4e}.has-warning .form-control-feedback{color:#ebebeb}.has-error .help-block,.has-error .control-label,.has-error .radio,.has-error .checkbox,.has-error .radio-inline,.has-error .checkbox-inline{color:#ebebeb}.has-error .form-control{border-color:#ebebeb;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075);box-shadow:inset 0 1px 1px rgba(0,0,0,0.075)}.has-error .form-control:focus{border-color:#d2d2d2;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff;box-shadow:inset 0 1px 1px rgba(0,0,0,0.075),0 0 6px #fff}.has-error .input-group-addon{color:#ebebeb;border-color:#ebebeb;background-color:#d9534f}.has-error .form-control-feedback{color:#ebebeb}.form-control-static{margin-bottom:0}.help-block{display:block;margin-top:5px;margin-bottom:10px;color:#ffffff}@media (min-width:768px){.form-inline .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.form-inline .form-control{display:inline-block;width:auto;vertical-align:middle}.form-inline .input-group>.form-control{width:100%}.form-inline .control-label{margin-bottom:0;vertical-align:middle}.form-inline .radio,.form-inline .checkbox{display:inline-block;margin-top:0;margin-bottom:0;padding-left:0;vertical-align:middle}.form-inline .radio input[type="radio"],.form-inline .checkbox input[type="checkbox"]{float:none;margin-left:0}.form-inline .has-feedback .form-control-feedback{top:0}}.form-horizontal .control-label,.form-horizontal .radio,.form-horizontal .checkbox,.form-horizontal .radio-inline,.form-horizontal .checkbox-inline{margin-top:0;margin-bottom:0;padding-top:9px}.form-horizontal .radio,.form-horizontal .checkbox{min-height:30px}.form-horizontal .form-group{margin-left:-15px;margin-right:-15px}.form-horizontal .form-control-static{padding-top:9px}@media (min-width:768px){.form-horizontal .control-label{text-align:right}}.form-horizontal .has-feedback .form-control-feedback{top:0;right:15px}.btn{display:inline-block;margin-bottom:0;font-weight:normal;text-align:center;vertical-align:middle;cursor:pointer;background-image:none;border:1px solid transparent;white-space:nowrap;padding:8px 16px;font-size:15px;line-height:1.42857143;border-radius:0;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.btn:focus,.btn:active:focus,.btn.active:focus{outline:thin dotted;outline:5px auto -webkit-focus-ring-color;outline-offset:-2px}.btn:hover,.btn:focus{color:#ffffff;text-decoration:none}.btn:active,.btn.active{outline:0;background-image:none;-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn.disabled,.btn[disabled],fieldset[disabled] .btn{cursor:not-allowed;pointer-events:none;opacity:0.65;filter:alpha(opacity=65);-webkit-box-shadow:none;box-shadow:none}.btn-default{color:#ffffff;background-color:#4e5d6c;border-color:transparent}.btn-default:hover,.btn-default:focus,.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{color:#ffffff;background-color:#3d4954;border-color:rgba(0,0,0,0)}.btn-default:active,.btn-default.active,.open .dropdown-toggle.btn-default{background-image:none}.btn-default.disabled,.btn-default[disabled],fieldset[disabled] .btn-default,.btn-default.disabled:hover,.btn-default[disabled]:hover,fieldset[disabled] .btn-default:hover,.btn-default.disabled:focus,.btn-default[disabled]:focus,fieldset[disabled] .btn-default:focus,.btn-default.disabled:active,.btn-default[disabled]:active,fieldset[disabled] .btn-default:active,.btn-default.disabled.active,.btn-default[disabled].active,fieldset[disabled] .btn-default.active{background-color:#4e5d6c;border-color:transparent}.btn-default .badge{color:#4e5d6c;background-color:#ffffff}.btn-primary{color:#ffffff;background-color:#df691a;border-color:transparent}.btn-primary:hover,.btn-primary:focus,.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{color:#ffffff;background-color:#ba5816;border-color:rgba(0,0,0,0)}.btn-primary:active,.btn-primary.active,.open .dropdown-toggle.btn-primary{background-image:none}.btn-primary.disabled,.btn-primary[disabled],fieldset[disabled] .btn-primary,.btn-primary.disabled:hover,.btn-primary[disabled]:hover,fieldset[disabled] .btn-primary:hover,.btn-primary.disabled:focus,.btn-primary[disabled]:focus,fieldset[disabled] .btn-primary:focus,.btn-primary.disabled:active,.btn-primary[disabled]:active,fieldset[disabled] .btn-primary:active,.btn-primary.disabled.active,.btn-primary[disabled].active,fieldset[disabled] .btn-primary.active{background-color:#df691a;border-color:transparent}.btn-primary .badge{color:#df691a;background-color:#ffffff}.btn-success{color:#ffffff;background-color:#5cb85c;border-color:transparent}.btn-success:hover,.btn-success:focus,.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{color:#ffffff;background-color:#47a447;border-color:rgba(0,0,0,0)}.btn-success:active,.btn-success.active,.open .dropdown-toggle.btn-success{background-image:none}.btn-success.disabled,.btn-success[disabled],fieldset[disabled] .btn-success,.btn-success.disabled:hover,.btn-success[disabled]:hover,fieldset[disabled] .btn-success:hover,.btn-success.disabled:focus,.btn-success[disabled]:focus,fieldset[disabled] .btn-success:focus,.btn-success.disabled:active,.btn-success[disabled]:active,fieldset[disabled] .btn-success:active,.btn-success.disabled.active,.btn-success[disabled].active,fieldset[disabled] .btn-success.active{background-color:#5cb85c;border-color:transparent}.btn-success .badge{color:#5cb85c;background-color:#ffffff}.btn-info{color:#ffffff;background-color:#5bc0de;border-color:transparent}.btn-info:hover,.btn-info:focus,.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{color:#ffffff;background-color:#39b3d7;border-color:rgba(0,0,0,0)}.btn-info:active,.btn-info.active,.open .dropdown-toggle.btn-info{background-image:none}.btn-info.disabled,.btn-info[disabled],fieldset[disabled] .btn-info,.btn-info.disabled:hover,.btn-info[disabled]:hover,fieldset[disabled] .btn-info:hover,.btn-info.disabled:focus,.btn-info[disabled]:focus,fieldset[disabled] .btn-info:focus,.btn-info.disabled:active,.btn-info[disabled]:active,fieldset[disabled] .btn-info:active,.btn-info.disabled.active,.btn-info[disabled].active,fieldset[disabled] .btn-info.active{background-color:#5bc0de;border-color:transparent}.btn-info .badge{color:#5bc0de;background-color:#ffffff}.btn-warning{color:#ffffff;background-color:#f0ad4e;border-color:transparent}.btn-warning:hover,.btn-warning:focus,.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{color:#ffffff;background-color:#ed9c28;border-color:rgba(0,0,0,0)}.btn-warning:active,.btn-warning.active,.open .dropdown-toggle.btn-warning{background-image:none}.btn-warning.disabled,.btn-warning[disabled],fieldset[disabled] .btn-warning,.btn-warning.disabled:hover,.btn-warning[disabled]:hover,fieldset[disabled] .btn-warning:hover,.btn-warning.disabled:focus,.btn-warning[disabled]:focus,fieldset[disabled] .btn-warning:focus,.btn-warning.disabled:active,.btn-warning[disabled]:active,fieldset[disabled] .btn-warning:active,.btn-warning.disabled.active,.btn-warning[disabled].active,fieldset[disabled] .btn-warning.active{background-color:#f0ad4e;border-color:transparent}.btn-warning .badge{color:#f0ad4e;background-color:#ffffff}.btn-danger{color:#ffffff;background-color:#d9534f;border-color:transparent}.btn-danger:hover,.btn-danger:focus,.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{color:#ffffff;background-color:#d2322d;border-color:rgba(0,0,0,0)}.btn-danger:active,.btn-danger.active,.open .dropdown-toggle.btn-danger{background-image:none}.btn-danger.disabled,.btn-danger[disabled],fieldset[disabled] .btn-danger,.btn-danger.disabled:hover,.btn-danger[disabled]:hover,fieldset[disabled] .btn-danger:hover,.btn-danger.disabled:focus,.btn-danger[disabled]:focus,fieldset[disabled] .btn-danger:focus,.btn-danger.disabled:active,.btn-danger[disabled]:active,fieldset[disabled] .btn-danger:active,.btn-danger.disabled.active,.btn-danger[disabled].active,fieldset[disabled] .btn-danger.active{background-color:#d9534f;border-color:transparent}.btn-danger .badge{color:#d9534f;background-color:#ffffff}.btn-link{color:#df691a;font-weight:normal;cursor:pointer;border-radius:0}.btn-link,.btn-link:active,.btn-link[disabled],fieldset[disabled] .btn-link{background-color:transparent;-webkit-box-shadow:none;box-shadow:none}.btn-link,.btn-link:hover,.btn-link:focus,.btn-link:active{border-color:transparent}.btn-link:hover,.btn-link:focus{color:#df691a;text-decoration:underline;background-color:transparent}.btn-link[disabled]:hover,fieldset[disabled] .btn-link:hover,.btn-link[disabled]:focus,fieldset[disabled] .btn-link:focus{color:#4e5d6c;text-decoration:none}.btn-lg,.btn-group-lg>.btn{padding:12px 24px;font-size:19px;line-height:1.33;border-radius:0}.btn-sm,.btn-group-sm>.btn{padding:5px 10px;font-size:13px;line-height:1.5;border-radius:0}.btn-xs,.btn-group-xs>.btn{padding:1px 5px;font-size:13px;line-height:1.5;border-radius:0}.btn-block{display:block;width:100%;padding-left:0;padding-right:0}.btn-block+.btn-block{margin-top:5px}input[type="submit"].btn-block,input[type="reset"].btn-block,input[type="button"].btn-block{width:100%}.fade{opacity:0;-webkit-transition:opacity 0.15s linear;transition:opacity 0.15s linear}.fade.in{opacity:1}.collapse{display:none}.collapse.in{display:block}.collapsing{position:relative;height:0;overflow:hidden;-webkit-transition:height 0.35s ease;transition:height 0.35s ease}@font-face{font-family:'Glyphicons Halflings';src:url('../fonts/glyphicons-halflings-regular.eot');src:url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'),url('../fonts/glyphicons-halflings-regular.woff') format('woff'),url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'),url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg')}.glyphicon{position:relative;top:1px;display:inline-block;font-family:'Glyphicons Halflings';font-style:normal;font-weight:normal;line-height:1;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.glyphicon-asterisk:before{content:"\2a"}.glyphicon-plus:before{content:"\2b"}.glyphicon-euro:before{content:"\20ac"}.glyphicon-minus:before{content:"\2212"}.glyphicon-cloud:before{content:"\2601"}.glyphicon-envelope:before{content:"\2709"}.glyphicon-pencil:before{content:"\270f"}.glyphicon-glass:before{content:"\e001"}.glyphicon-music:before{content:"\e002"}.glyphicon-search:before{content:"\e003"}.glyphicon-heart:before{content:"\e005"}.glyphicon-star:before{content:"\e006"}.glyphicon-star-empty:before{content:"\e007"}.glyphicon-user:before{content:"\e008"}.glyphicon-film:before{content:"\e009"}.glyphicon-th-large:before{content:"\e010"}.glyphicon-th:before{content:"\e011"}.glyphicon-th-list:before{content:"\e012"}.glyphicon-ok:before{content:"\e013"}.glyphicon-remove:before{content:"\e014"}.glyphicon-zoom-in:before{content:"\e015"}.glyphicon-zoom-out:before{content:"\e016"}.glyphicon-off:before{content:"\e017"}.glyphicon-signal:before{content:"\e018"}.glyphicon-cog:before{content:"\e019"}.glyphicon-trash:before{content:"\e020"}.glyphicon-home:before{content:"\e021"}.glyphicon-file:before{content:"\e022"}.glyphicon-time:before{content:"\e023"}.glyphicon-road:before{content:"\e024"}.glyphicon-download-alt:before{content:"\e025"}.glyphicon-download:before{content:"\e026"}.glyphicon-upload:before{content:"\e027"}.glyphicon-inbox:before{content:"\e028"}.glyphicon-play-circle:before{content:"\e029"}.glyphicon-repeat:before{content:"\e030"}.glyphicon-refresh:before{content:"\e031"}.glyphicon-list-alt:before{content:"\e032"}.glyphicon-lock:before{content:"\e033"}.glyphicon-flag:before{content:"\e034"}.glyphicon-headphones:before{content:"\e035"}.glyphicon-volume-off:before{content:"\e036"}.glyphicon-volume-down:before{content:"\e037"}.glyphicon-volume-up:before{content:"\e038"}.glyphicon-qrcode:before{content:"\e039"}.glyphicon-barcode:before{content:"\e040"}.glyphicon-tag:before{content:"\e041"}.glyphicon-tags:before{content:"\e042"}.glyphicon-book:before{content:"\e043"}.glyphicon-bookmark:before{content:"\e044"}.glyphicon-print:before{content:"\e045"}.glyphicon-camera:before{content:"\e046"}.glyphicon-font:before{content:"\e047"}.glyphicon-bold:before{content:"\e048"}.glyphicon-italic:before{content:"\e049"}.glyphicon-text-height:before{content:"\e050"}.glyphicon-text-width:before{content:"\e051"}.glyphicon-align-left:before{content:"\e052"}.glyphicon-align-center:before{content:"\e053"}.glyphicon-align-right:before{content:"\e054"}.glyphicon-align-justify:before{content:"\e055"}.glyphicon-list:before{content:"\e056"}.glyphicon-indent-left:before{content:"\e057"}.glyphicon-indent-right:before{content:"\e058"}.glyphicon-facetime-video:before{content:"\e059"}.glyphicon-picture:before{content:"\e060"}.glyphicon-map-marker:before{content:"\e062"}.glyphicon-adjust:before{content:"\e063"}.glyphicon-tint:before{content:"\e064"}.glyphicon-edit:before{content:"\e065"}.glyphicon-share:before{content:"\e066"}.glyphicon-check:before{content:"\e067"}.glyphicon-move:before{content:"\e068"}.glyphicon-step-backward:before{content:"\e069"}.glyphicon-fast-backward:before{content:"\e070"}.glyphicon-backward:before{content:"\e071"}.glyphicon-play:before{content:"\e072"}.glyphicon-pause:before{content:"\e073"}.glyphicon-stop:before{content:"\e074"}.glyphicon-forward:before{content:"\e075"}.glyphicon-fast-forward:before{content:"\e076"}.glyphicon-step-forward:before{content:"\e077"}.glyphicon-eject:before{content:"\e078"}.glyphicon-chevron-left:before{content:"\e079"}.glyphicon-chevron-right:before{content:"\e080"}.glyphicon-plus-sign:before{content:"\e081"}.glyphicon-minus-sign:before{content:"\e082"}.glyphicon-remove-sign:before{content:"\e083"}.glyphicon-ok-sign:before{content:"\e084"}.glyphicon-question-sign:before{content:"\e085"}.glyphicon-info-sign:before{content:"\e086"}.glyphicon-screenshot:before{content:"\e087"}.glyphicon-remove-circle:before{content:"\e088"}.glyphicon-ok-circle:before{content:"\e089"}.glyphicon-ban-circle:before{content:"\e090"}.glyphicon-arrow-left:before{content:"\e091"}.glyphicon-arrow-right:before{content:"\e092"}.glyphicon-arrow-up:before{content:"\e093"}.glyphicon-arrow-down:before{content:"\e094"}.glyphicon-share-alt:before{content:"\e095"}.glyphicon-resize-full:before{content:"\e096"}.glyphicon-resize-small:before{content:"\e097"}.glyphicon-exclamation-sign:before{content:"\e101"}.glyphicon-gift:before{content:"\e102"}.glyphicon-leaf:before{content:"\e103"}.glyphicon-fire:before{content:"\e104"}.glyphicon-eye-open:before{content:"\e105"}.glyphicon-eye-close:before{content:"\e106"}.glyphicon-warning-sign:before{content:"\e107"}.glyphicon-plane:before{content:"\e108"}.glyphicon-calendar:before{content:"\e109"}.glyphicon-random:before{content:"\e110"}.glyphicon-comment:before{content:"\e111"}.glyphicon-magnet:before{content:"\e112"}.glyphicon-chevron-up:before{content:"\e113"}.glyphicon-chevron-down:before{content:"\e114"}.glyphicon-retweet:before{content:"\e115"}.glyphicon-shopping-cart:before{content:"\e116"}.glyphicon-folder-close:before{content:"\e117"}.glyphicon-folder-open:before{content:"\e118"}.glyphicon-resize-vertical:before{content:"\e119"}.glyphicon-resize-horizontal:before{content:"\e120"}.glyphicon-hdd:before{content:"\e121"}.glyphicon-bullhorn:before{content:"\e122"}.glyphicon-bell:before{content:"\e123"}.glyphicon-certificate:before{content:"\e124"}.glyphicon-thumbs-up:before{content:"\e125"}.glyphicon-thumbs-down:before{content:"\e126"}.glyphicon-hand-right:before{content:"\e127"}.glyphicon-hand-left:before{content:"\e128"}.glyphicon-hand-up:before{content:"\e129"}.glyphicon-hand-down:before{content:"\e130"}.glyphicon-circle-arrow-right:before{content:"\e131"}.glyphicon-circle-arrow-left:before{content:"\e132"}.glyphicon-circle-arrow-up:before{content:"\e133"}.glyphicon-circle-arrow-down:before{content:"\e134"}.glyphicon-globe:before{content:"\e135"}.glyphicon-wrench:before{content:"\e136"}.glyphicon-tasks:before{content:"\e137"}.glyphicon-filter:before{content:"\e138"}.glyphicon-briefcase:before{content:"\e139"}.glyphicon-fullscreen:before{content:"\e140"}.glyphicon-dashboard:before{content:"\e141"}.glyphicon-paperclip:before{content:"\e142"}.glyphicon-heart-empty:before{content:"\e143"}.glyphicon-link:before{content:"\e144"}.glyphicon-phone:before{content:"\e145"}.glyphicon-pushpin:before{content:"\e146"}.glyphicon-usd:before{content:"\e148"}.glyphicon-gbp:before{content:"\e149"}.glyphicon-sort:before{content:"\e150"}.glyphicon-sort-by-alphabet:before{content:"\e151"}.glyphicon-sort-by-alphabet-alt:before{content:"\e152"}.glyphicon-sort-by-order:before{content:"\e153"}.glyphicon-sort-by-order-alt:before{content:"\e154"}.glyphicon-sort-by-attributes:before{content:"\e155"}.glyphicon-sort-by-attributes-alt:before{content:"\e156"}.glyphicon-unchecked:before{content:"\e157"}.glyphicon-expand:before{content:"\e158"}.glyphicon-collapse-down:before{content:"\e159"}.glyphicon-collapse-up:before{content:"\e160"}.glyphicon-log-in:before{content:"\e161"}.glyphicon-flash:before{content:"\e162"}.glyphicon-log-out:before{content:"\e163"}.glyphicon-new-window:before{content:"\e164"}.glyphicon-record:before{content:"\e165"}.glyphicon-save:before{content:"\e166"}.glyphicon-open:before{content:"\e167"}.glyphicon-saved:before{content:"\e168"}.glyphicon-import:before{content:"\e169"}.glyphicon-export:before{content:"\e170"}.glyphicon-send:before{content:"\e171"}.glyphicon-floppy-disk:before{content:"\e172"}.glyphicon-floppy-saved:before{content:"\e173"}.glyphicon-floppy-remove:before{content:"\e174"}.glyphicon-floppy-save:before{content:"\e175"}.glyphicon-floppy-open:before{content:"\e176"}.glyphicon-credit-card:before{content:"\e177"}.glyphicon-transfer:before{content:"\e178"}.glyphicon-cutlery:before{content:"\e179"}.glyphicon-header:before{content:"\e180"}.glyphicon-compressed:before{content:"\e181"}.glyphicon-earphone:before{content:"\e182"}.glyphicon-phone-alt:before{content:"\e183"}.glyphicon-tower:before{content:"\e184"}.glyphicon-stats:before{content:"\e185"}.glyphicon-sd-video:before{content:"\e186"}.glyphicon-hd-video:before{content:"\e187"}.glyphicon-subtitles:before{content:"\e188"}.glyphicon-sound-stereo:before{content:"\e189"}.glyphicon-sound-dolby:before{content:"\e190"}.glyphicon-sound-5-1:before{content:"\e191"}.glyphicon-sound-6-1:before{content:"\e192"}.glyphicon-sound-7-1:before{content:"\e193"}.glyphicon-copyright-mark:before{content:"\e194"}.glyphicon-registration-mark:before{content:"\e195"}.glyphicon-cloud-download:before{content:"\e197"}.glyphicon-cloud-upload:before{content:"\e198"}.glyphicon-tree-conifer:before{content:"\e199"}.glyphicon-tree-deciduous:before{content:"\e200"}.caret{display:inline-block;width:0;height:0;margin-left:2px;vertical-align:middle;border-top:4px solid;border-right:4px solid transparent;border-left:4px solid transparent}.dropdown{position:relative}.dropdown-toggle:focus{outline:0}.dropdown-menu{position:absolute;top:100%;left:0;z-index:1000;display:none;float:left;min-width:160px;padding:5px 0;margin:2px 0 0;list-style:none;font-size:15px;background-color:#4e5d6c;border:1px solid transparent;border-radius:0;-webkit-box-shadow:0 6px 12px rgba(0,0,0,0.175);box-shadow:0 6px 12px rgba(0,0,0,0.175);background-clip:padding-box}.dropdown-menu.pull-right{right:0;left:auto}.dropdown-menu .divider{height:1px;margin:9.5px 0;overflow:hidden;background-color:#2b3e50}.dropdown-menu>li>a{display:block;padding:3px 20px;clear:both;font-weight:normal;line-height:1.42857143;color:#ebebeb;white-space:nowrap}.dropdown-menu>li>a:hover,.dropdown-menu>li>a:focus{text-decoration:none;color:#ebebeb;background-color:#485563}.dropdown-menu>.active>a,.dropdown-menu>.active>a:hover,.dropdown-menu>.active>a:focus{color:#ffffff;text-decoration:none;outline:0;background-color:#df691a}.dropdown-menu>.disabled>a,.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{color:#2b3e50}.dropdown-menu>.disabled>a:hover,.dropdown-menu>.disabled>a:focus{text-decoration:none;background-color:transparent;background-image:none;filter:progid:DXImageTransform.Microsoft.gradient(enabled=false);cursor:not-allowed}.open>.dropdown-menu{display:block}.open>a{outline:0}.dropdown-menu-right{left:auto;right:0}.dropdown-menu-left{left:0;right:auto}.dropdown-header{display:block;padding:3px 20px;font-size:13px;line-height:1.42857143;color:#2b3e50}.dropdown-backdrop{position:fixed;left:0;right:0;bottom:0;top:0;z-index:990}.pull-right>.dropdown-menu{right:0;left:auto}.dropup .caret,.navbar-fixed-bottom .dropdown .caret{border-top:0;border-bottom:4px solid;content:""}.dropup .dropdown-menu,.navbar-fixed-bottom .dropdown .dropdown-menu{top:auto;bottom:100%;margin-bottom:1px}@media (min-width:768px){.navbar-right .dropdown-menu{left:auto;right:0}.navbar-right .dropdown-menu-left{left:0;right:auto}}.btn-group,.btn-group-vertical{position:relative;display:inline-block;vertical-align:middle}.btn-group>.btn,.btn-group-vertical>.btn{position:relative;float:left}.btn-group>.btn:hover,.btn-group-vertical>.btn:hover,.btn-group>.btn:focus,.btn-group-vertical>.btn:focus,.btn-group>.btn:active,.btn-group-vertical>.btn:active,.btn-group>.btn.active,.btn-group-vertical>.btn.active{z-index:2}.btn-group>.btn:focus,.btn-group-vertical>.btn:focus{outline:none}.btn-group .btn+.btn,.btn-group .btn+.btn-group,.btn-group .btn-group+.btn,.btn-group .btn-group+.btn-group{margin-left:-1px}.btn-toolbar{margin-left:-5px}.btn-toolbar .btn-group,.btn-toolbar .input-group{float:left}.btn-toolbar>.btn,.btn-toolbar>.btn-group,.btn-toolbar>.input-group{margin-left:5px}.btn-group>.btn:not(:first-child):not(:last-child):not(.dropdown-toggle){border-radius:0}.btn-group>.btn:first-child{margin-left:0}.btn-group>.btn:first-child:not(:last-child):not(.dropdown-toggle){border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn:last-child:not(:first-child),.btn-group>.dropdown-toggle:not(:first-child){border-bottom-left-radius:0;border-top-left-radius:0}.btn-group>.btn-group{float:left}.btn-group>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group>.btn-group:first-child>.btn:last-child,.btn-group>.btn-group:first-child>.dropdown-toggle{border-bottom-right-radius:0;border-top-right-radius:0}.btn-group>.btn-group:last-child>.btn:first-child{border-bottom-left-radius:0;border-top-left-radius:0}.btn-group .dropdown-toggle:active,.btn-group.open .dropdown-toggle{outline:0}.btn-group>.btn+.dropdown-toggle{padding-left:8px;padding-right:8px}.btn-group>.btn-lg+.dropdown-toggle{padding-left:12px;padding-right:12px}.btn-group.open .dropdown-toggle{-webkit-box-shadow:inset 0 3px 5px rgba(0,0,0,0.125);box-shadow:inset 0 3px 5px rgba(0,0,0,0.125)}.btn-group.open .dropdown-toggle.btn-link{-webkit-box-shadow:none;box-shadow:none}.btn .caret{margin-left:0}.btn-lg .caret{border-width:5px 5px 0;border-bottom-width:0}.dropup .btn-lg .caret{border-width:0 5px 5px}.btn-group-vertical>.btn,.btn-group-vertical>.btn-group,.btn-group-vertical>.btn-group>.btn{display:block;float:none;width:100%;max-width:100%}.btn-group-vertical>.btn-group>.btn{float:none}.btn-group-vertical>.btn+.btn,.btn-group-vertical>.btn+.btn-group,.btn-group-vertical>.btn-group+.btn,.btn-group-vertical>.btn-group+.btn-group{margin-top:-1px;margin-left:0}.btn-group-vertical>.btn:not(:first-child):not(:last-child){border-radius:0}.btn-group-vertical>.btn:first-child:not(:last-child){border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn:last-child:not(:first-child){border-bottom-left-radius:0;border-top-right-radius:0;border-top-left-radius:0}.btn-group-vertical>.btn-group:not(:first-child):not(:last-child)>.btn{border-radius:0}.btn-group-vertical>.btn-group:first-child:not(:last-child)>.btn:last-child,.btn-group-vertical>.btn-group:first-child:not(:last-child)>.dropdown-toggle{border-bottom-right-radius:0;border-bottom-left-radius:0}.btn-group-vertical>.btn-group:last-child:not(:first-child)>.btn:first-child{border-top-right-radius:0;border-top-left-radius:0}.btn-group-justified{display:table;width:100%;table-layout:fixed;border-collapse:separate}.btn-group-justified>.btn,.btn-group-justified>.btn-group{float:none;display:table-cell;width:1%}.btn-group-justified>.btn-group .btn{width:100%}[data-toggle="buttons"]>.btn>input[type="radio"],[data-toggle="buttons"]>.btn>input[type="checkbox"]{display:none}.input-group{position:relative;display:table;border-collapse:separate}.input-group[class*="col-"]{float:none;padding-left:0;padding-right:0}.input-group .form-control{position:relative;z-index:2;float:left;width:100%;margin-bottom:0}.input-group-lg>.form-control,.input-group-lg>.input-group-addon,.input-group-lg>.input-group-btn>.btn{height:52px;padding:12px 24px;font-size:19px;line-height:1.33;border-radius:0}select.input-group-lg>.form-control,select.input-group-lg>.input-group-addon,select.input-group-lg>.input-group-btn>.btn{height:52px;line-height:52px}textarea.input-group-lg>.form-control,textarea.input-group-lg>.input-group-addon,textarea.input-group-lg>.input-group-btn>.btn,select[multiple].input-group-lg>.form-control,select[multiple].input-group-lg>.input-group-addon,select[multiple].input-group-lg>.input-group-btn>.btn{height:auto}.input-group-sm>.form-control,.input-group-sm>.input-group-addon,.input-group-sm>.input-group-btn>.btn{height:31px;padding:5px 10px;font-size:13px;line-height:1.5;border-radius:0}select.input-group-sm>.form-control,select.input-group-sm>.input-group-addon,select.input-group-sm>.input-group-btn>.btn{height:31px;line-height:31px}textarea.input-group-sm>.form-control,textarea.input-group-sm>.input-group-addon,textarea.input-group-sm>.input-group-btn>.btn,select[multiple].input-group-sm>.form-control,select[multiple].input-group-sm>.input-group-addon,select[multiple].input-group-sm>.input-group-btn>.btn{height:auto}.input-group-addon,.input-group-btn,.input-group .form-control{display:table-cell}.input-group-addon:not(:first-child):not(:last-child),.input-group-btn:not(:first-child):not(:last-child),.input-group .form-control:not(:first-child):not(:last-child){border-radius:0}.input-group-addon,.input-group-btn{width:1%;white-space:nowrap;vertical-align:middle}.input-group-addon{padding:8px 16px;font-size:15px;font-weight:normal;line-height:1;color:#2b3e50;text-align:center;background-color:#4e5d6c;border:1px solid transparent;border-radius:0}.input-group-addon.input-sm{padding:5px 10px;font-size:13px;border-radius:0}.input-group-addon.input-lg{padding:12px 24px;font-size:19px;border-radius:0}.input-group-addon input[type="radio"],.input-group-addon input[type="checkbox"]{margin-top:0}.input-group .form-control:first-child,.input-group-addon:first-child,.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group>.btn,.input-group-btn:first-child>.dropdown-toggle,.input-group-btn:last-child>.btn:not(:last-child):not(.dropdown-toggle),.input-group-btn:last-child>.btn-group:not(:last-child)>.btn{border-bottom-right-radius:0;border-top-right-radius:0}.input-group-addon:first-child{border-right:0}.input-group .form-control:last-child,.input-group-addon:last-child,.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group>.btn,.input-group-btn:last-child>.dropdown-toggle,.input-group-btn:first-child>.btn:not(:first-child),.input-group-btn:first-child>.btn-group:not(:first-child)>.btn{border-bottom-left-radius:0;border-top-left-radius:0}.input-group-addon:last-child{border-left:0}.input-group-btn{position:relative;font-size:0;white-space:nowrap}.input-group-btn>.btn{position:relative}.input-group-btn>.btn+.btn{margin-left:-1px}.input-group-btn>.btn:hover,.input-group-btn>.btn:focus,.input-group-btn>.btn:active{z-index:2}.input-group-btn:first-child>.btn,.input-group-btn:first-child>.btn-group{margin-right:-1px}.input-group-btn:last-child>.btn,.input-group-btn:last-child>.btn-group{margin-left:-1px}.nav{margin-bottom:0;padding-left:0;list-style:none}.nav>li{position:relative;display:block}.nav>li>a{position:relative;display:block;padding:10px 15px}.nav>li>a:hover,.nav>li>a:focus{text-decoration:none;background-color:#4e5d6c}.nav>li.disabled>a{color:#4e5d6c}.nav>li.disabled>a:hover,.nav>li.disabled>a:focus{color:#4e5d6c;text-decoration:none;background-color:transparent;cursor:not-allowed}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{background-color:#4e5d6c;border-color:#df691a}.nav .nav-divider{height:1px;margin:9.5px 0;overflow:hidden;background-color:#e5e5e5}.nav>li>a>img{max-width:none}.nav-tabs{border-bottom:1px solid transparent}.nav-tabs>li{float:left;margin-bottom:-1px}.nav-tabs>li>a{margin-right:2px;line-height:1.42857143;border:1px solid transparent;border-radius:0 0 0 0}.nav-tabs>li>a:hover{border-color:#4e5d6c #4e5d6c transparent}.nav-tabs>li.active>a,.nav-tabs>li.active>a:hover,.nav-tabs>li.active>a:focus{color:#ebebeb;background-color:#2b3e50;border:1px solid #4e5d6c;border-bottom-color:transparent;cursor:default}.nav-tabs.nav-justified{width:100%;border-bottom:0}.nav-tabs.nav-justified>li{float:none}.nav-tabs.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-tabs.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-tabs.nav-justified>li{display:table-cell;width:1%}.nav-tabs.nav-justified>li>a{margin-bottom:0}}.nav-tabs.nav-justified>li>a{margin-right:0;border-radius:0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border:1px solid #4e5d6c}@media (min-width:768px){.nav-tabs.nav-justified>li>a{border-bottom:1px solid #4e5d6c;border-radius:0 0 0 0}.nav-tabs.nav-justified>.active>a,.nav-tabs.nav-justified>.active>a:hover,.nav-tabs.nav-justified>.active>a:focus{border-bottom-color:#4e5d6c}}.nav-pills>li{float:left}.nav-pills>li>a{border-radius:0}.nav-pills>li+li{margin-left:2px}.nav-pills>li.active>a,.nav-pills>li.active>a:hover,.nav-pills>li.active>a:focus{color:#ffffff;background-color:#df691a}.nav-stacked>li{float:none}.nav-stacked>li+li{margin-top:2px;margin-left:0}.nav-justified{width:100%}.nav-justified>li{float:none}.nav-justified>li>a{text-align:center;margin-bottom:5px}.nav-justified>.dropdown .dropdown-menu{top:auto;left:auto}@media (min-width:768px){.nav-justified>li{display:table-cell;width:1%}.nav-justified>li>a{margin-bottom:0}}.nav-tabs-justified{border-bottom:0}.nav-tabs-justified>li>a{margin-right:0;border-radius:0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border:1px solid #4e5d6c}@media (min-width:768px){.nav-tabs-justified>li>a{border-bottom:1px solid #4e5d6c;border-radius:0 0 0 0}.nav-tabs-justified>.active>a,.nav-tabs-justified>.active>a:hover,.nav-tabs-justified>.active>a:focus{border-bottom-color:#4e5d6c}}.tab-content>.tab-pane{display:none}.tab-content>.active{display:block}.nav-tabs .dropdown-menu{margin-top:-1px;border-top-right-radius:0;border-top-left-radius:0}.navbar{position:relative;min-height:40px;margin-bottom:21px;border:1px solid transparent}@media (min-width:768px){.navbar{border-radius:0}}@media (min-width:768px){.navbar-header{float:left}}.navbar-collapse{max-height:340px;overflow-x:visible;padding-right:15px;padding-left:15px;border-top:1px solid transparent;box-shadow:inset 0 1px 0 rgba(255,255,255,0.1);-webkit-overflow-scrolling:touch}.navbar-collapse.in{overflow-y:auto}@media (min-width:768px){.navbar-collapse{width:auto;border-top:0;box-shadow:none}.navbar-collapse.collapse{display:block !important;height:auto !important;padding-bottom:0;overflow:visible !important}.navbar-collapse.in{overflow-y:visible}.navbar-fixed-top .navbar-collapse,.navbar-static-top .navbar-collapse,.navbar-fixed-bottom .navbar-collapse{padding-left:0;padding-right:0}}.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:-15px;margin-left:-15px}@media (min-width:768px){.container>.navbar-header,.container-fluid>.navbar-header,.container>.navbar-collapse,.container-fluid>.navbar-collapse{margin-right:0;margin-left:0}}.navbar-static-top{z-index:1000;border-width:0 0 1px}@media (min-width:768px){.navbar-static-top{border-radius:0}}.navbar-fixed-top,.navbar-fixed-bottom{position:fixed;right:0;left:0;z-index:1030}@media (min-width:768px){.navbar-fixed-top,.navbar-fixed-bottom{border-radius:0}}.navbar-fixed-top{top:0;border-width:0 0 1px}.navbar-fixed-bottom{bottom:0;margin-bottom:0;border-width:1px 0 0}.navbar-brand{float:left;padding:9.5px 15px;font-size:19px;line-height:21px;height:40px}.navbar-brand:hover,.navbar-brand:focus{text-decoration:none}@media (min-width:768px){.navbar>.container .navbar-brand,.navbar>.container-fluid .navbar-brand{margin-left:-15px}}.navbar-toggle{position:relative;float:right;margin-right:15px;padding:9px 10px;margin-top:3px;margin-bottom:3px;background-color:transparent;background-image:none;border:1px solid transparent;border-radius:0}.navbar-toggle:focus{outline:none}.navbar-toggle .icon-bar{display:block;width:22px;height:2px;border-radius:1px}.navbar-toggle .icon-bar+.icon-bar{margin-top:4px}@media (min-width:768px){.navbar-toggle{display:none}}.navbar-nav{margin:4.75px -15px}.navbar-nav>li>a{padding-top:10px;padding-bottom:10px;line-height:21px}@media (max-width:767px){.navbar-nav .open .dropdown-menu{position:static;float:none;width:auto;margin-top:0;background-color:transparent;border:0;box-shadow:none}.navbar-nav .open .dropdown-menu>li>a,.navbar-nav .open .dropdown-menu .dropdown-header{padding:5px 15px 5px 25px}.navbar-nav .open .dropdown-menu>li>a{line-height:21px}.navbar-nav .open .dropdown-menu>li>a:hover,.navbar-nav .open .dropdown-menu>li>a:focus{background-image:none}}@media (min-width:768px){.navbar-nav{float:left;margin:0}.navbar-nav>li{float:left}.navbar-nav>li>a{padding-top:9.5px;padding-bottom:9.5px}.navbar-nav.navbar-right:last-child{margin-right:-15px}}@media (min-width:768px){.navbar-left{float:left !important}.navbar-right{float:right !important}}.navbar-form{margin-left:-15px;margin-right:-15px;padding:10px 15px;border-top:1px solid transparent;border-bottom:1px solid transparent;-webkit-box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);box-shadow:inset 0 1px 0 rgba(255,255,255,0.1),0 1px 0 rgba(255,255,255,0.1);margin-top:0.5px;margin-bottom:0.5px}@media (min-width:768px){.navbar-form .form-group{display:inline-block;margin-bottom:0;vertical-align:middle}.navbar-form .form-control{display:inline-block;width:auto;vertical-align:middle}.navbar-form .input-group>.form-control{width:100%}.navbar-form .control-label{margin-bottom:0;vertical-align:middle}.navbar-form .radio,.navbar-form .checkbox{display:inline-block;margin-top:0;margin-bottom:0;padding-left:0;vertical-align:middle}.navbar-form .radio input[type="radio"],.navbar-form .checkbox input[type="checkbox"]{float:none;margin-left:0}.navbar-form .has-feedback .form-control-feedback{top:0}}@media (max-width:767px){.navbar-form .form-group{margin-bottom:5px}}@media (min-width:768px){.navbar-form{width:auto;border:0;margin-left:0;margin-right:0;padding-top:0;padding-bottom:0;-webkit-box-shadow:none;box-shadow:none}.navbar-form.navbar-right:last-child{margin-right:-15px}}.navbar-nav>li>.dropdown-menu{margin-top:0;border-top-right-radius:0;border-top-left-radius:0}.navbar-fixed-bottom .navbar-nav>li>.dropdown-menu{border-bottom-right-radius:0;border-bottom-left-radius:0}.navbar-btn{margin-top:0.5px;margin-bottom:0.5px}.navbar-btn.btn-sm{margin-top:4.5px;margin-bottom:4.5px}.navbar-btn.btn-xs{margin-top:9px;margin-bottom:9px}.navbar-text{margin-top:9.5px;margin-bottom:9.5px}@media (min-width:768px){.navbar-text{float:left;margin-left:15px;margin-right:15px}.navbar-text.navbar-right:last-child{margin-right:0}}.navbar-default{background-color:#4e5d6c;border-color:transparent}.navbar-default .navbar-brand{color:#ebebeb}.navbar-default .navbar-brand:hover,.navbar-default .navbar-brand:focus{color:#ebebeb;background-color:transparent}.navbar-default .navbar-text{color:#ebebeb}.navbar-default .navbar-nav>li>a{color:#ebebeb}.navbar-default .navbar-nav>li>a:hover,.navbar-default .navbar-nav>li>a:focus{color:#ebebeb;background-color:#485563}.navbar-default .navbar-nav>.active>a,.navbar-default .navbar-nav>.active>a:hover,.navbar-default .navbar-nav>.active>a:focus{color:#ebebeb;background-color:#485563}.navbar-default .navbar-nav>.disabled>a,.navbar-default .navbar-nav>.disabled>a:hover,.navbar-default .navbar-nav>.disabled>a:focus{color:#cccccc;background-color:transparent}.navbar-default .navbar-toggle{border-color:transparent}.navbar-default .navbar-toggle:hover,.navbar-default .navbar-toggle:focus{background-color:#485563}.navbar-default .navbar-toggle .icon-bar{background-color:#ebebeb}.navbar-default .navbar-collapse,.navbar-default .navbar-form{border-color:transparent}.navbar-default .navbar-nav>.open>a,.navbar-default .navbar-nav>.open>a:hover,.navbar-default .navbar-nav>.open>a:focus{background-color:#485563;color:#ebebeb}@media (max-width:767px){.navbar-default .navbar-nav .open .dropdown-menu>li>a{color:#ebebeb}.navbar-default .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>li>a:focus{color:#ebebeb;background-color:#485563}.navbar-default .navbar-nav .open .dropdown-menu>.active>a,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.active>a:focus{color:#ebebeb;background-color:#485563}.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-default .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#cccccc;background-color:transparent}}.navbar-default .navbar-link{color:#ebebeb}.navbar-default .navbar-link:hover{color:#ebebeb}.navbar-inverse{background-color:#df691a;border-color:transparent}.navbar-inverse .navbar-brand{color:#ebebeb}.navbar-inverse .navbar-brand:hover,.navbar-inverse .navbar-brand:focus{color:#ebebeb;background-color:transparent}.navbar-inverse .navbar-text{color:#ebebeb}.navbar-inverse .navbar-nav>li>a{color:#ebebeb}.navbar-inverse .navbar-nav>li>a:hover,.navbar-inverse .navbar-nav>li>a:focus{color:#ebebeb;background-color:#c85e17}.navbar-inverse .navbar-nav>.active>a,.navbar-inverse .navbar-nav>.active>a:hover,.navbar-inverse .navbar-nav>.active>a:focus{color:#ebebeb;background-color:#c85e17}.navbar-inverse .navbar-nav>.disabled>a,.navbar-inverse .navbar-nav>.disabled>a:hover,.navbar-inverse .navbar-nav>.disabled>a:focus{color:#444444;background-color:transparent}.navbar-inverse .navbar-toggle{border-color:transparent}.navbar-inverse .navbar-toggle:hover,.navbar-inverse .navbar-toggle:focus{background-color:#c85e17}.navbar-inverse .navbar-toggle .icon-bar{background-color:#ebebeb}.navbar-inverse .navbar-collapse,.navbar-inverse .navbar-form{border-color:#bf5a16}.navbar-inverse .navbar-nav>.open>a,.navbar-inverse .navbar-nav>.open>a:hover,.navbar-inverse .navbar-nav>.open>a:focus{background-color:#c85e17;color:#ebebeb}@media (max-width:767px){.navbar-inverse .navbar-nav .open .dropdown-menu>.dropdown-header{border-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu .divider{background-color:transparent}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a{color:#ebebeb}.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>li>a:focus{color:#ebebeb;background-color:#c85e17}.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.active>a:focus{color:#ebebeb;background-color:#c85e17}.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:hover,.navbar-inverse .navbar-nav .open .dropdown-menu>.disabled>a:focus{color:#444444;background-color:transparent}}.navbar-inverse .navbar-link{color:#ebebeb}.navbar-inverse .navbar-link:hover{color:#ebebeb}.breadcrumb{padding:8px 15px;margin-bottom:21px;list-style:none;background-color:#4e5d6c;border-radius:0}.breadcrumb>li{display:inline-block}.breadcrumb>li+li:before{content:"/\00a0";padding:0 5px;color:#ebebeb}.breadcrumb>.active{color:#ebebeb}.pagination{display:inline-block;padding-left:0;margin:21px 0;border-radius:0}.pagination>li{display:inline}.pagination>li>a,.pagination>li>span{position:relative;float:left;padding:8px 16px;line-height:1.42857143;text-decoration:none;color:#ebebeb;background-color:#4e5d6c;border:1px solid transparent;margin-left:-1px}.pagination>li:first-child>a,.pagination>li:first-child>span{margin-left:0;border-bottom-left-radius:0;border-top-left-radius:0}.pagination>li:last-child>a,.pagination>li:last-child>span{border-bottom-right-radius:0;border-top-right-radius:0}.pagination>li>a:hover,.pagination>li>span:hover,.pagination>li>a:focus,.pagination>li>span:focus{color:#ebebeb;background-color:#485563;border-color:transparent}.pagination>.active>a,.pagination>.active>span,.pagination>.active>a:hover,.pagination>.active>span:hover,.pagination>.active>a:focus,.pagination>.active>span:focus{z-index:2;color:#ebebeb;background-color:#df691a;border-color:transparent;cursor:default}.pagination>.disabled>span,.pagination>.disabled>span:hover,.pagination>.disabled>span:focus,.pagination>.disabled>a,.pagination>.disabled>a:hover,.pagination>.disabled>a:focus{color:#323c46;background-color:#4e5d6c;border-color:transparent;cursor:not-allowed}.pagination-lg>li>a,.pagination-lg>li>span{padding:12px 24px;font-size:19px}.pagination-lg>li:first-child>a,.pagination-lg>li:first-child>span{border-bottom-left-radius:0;border-top-left-radius:0}.pagination-lg>li:last-child>a,.pagination-lg>li:last-child>span{border-bottom-right-radius:0;border-top-right-radius:0}.pagination-sm>li>a,.pagination-sm>li>span{padding:5px 10px;font-size:13px}.pagination-sm>li:first-child>a,.pagination-sm>li:first-child>span{border-bottom-left-radius:0;border-top-left-radius:0}.pagination-sm>li:last-child>a,.pagination-sm>li:last-child>span{border-bottom-right-radius:0;border-top-right-radius:0}.pager{padding-left:0;margin:21px 0;list-style:none;text-align:center}.pager li{display:inline}.pager li>a,.pager li>span{display:inline-block;padding:5px 14px;background-color:#4e5d6c;border:1px solid transparent;border-radius:15px}.pager li>a:hover,.pager li>a:focus{text-decoration:none;background-color:#485563}.pager .next>a,.pager .next>span{float:right}.pager .previous>a,.pager .previous>span{float:left}.pager .disabled>a,.pager .disabled>a:hover,.pager .disabled>a:focus,.pager .disabled>span{color:#323c46;background-color:#4e5d6c;cursor:not-allowed}.label{display:inline;padding:.2em .6em .3em;font-size:75%;font-weight:bold;line-height:1;color:#ffffff;text-align:center;white-space:nowrap;vertical-align:baseline;border-radius:.25em}.label[href]:hover,.label[href]:focus{color:#ffffff;text-decoration:none;cursor:pointer}.label:empty{display:none}.btn .label{position:relative;top:-1px}.label-default{background-color:#4e5d6c}.label-default[href]:hover,.label-default[href]:focus{background-color:#39444e}.label-primary{background-color:#df691a}.label-primary[href]:hover,.label-primary[href]:focus{background-color:#b15315}.label-success{background-color:#5cb85c}.label-success[href]:hover,.label-success[href]:focus{background-color:#449d44}.label-info{background-color:#5bc0de}.label-info[href]:hover,.label-info[href]:focus{background-color:#31b0d5}.label-warning{background-color:#f0ad4e}.label-warning[href]:hover,.label-warning[href]:focus{background-color:#ec971f}.label-danger{background-color:#d9534f}.label-danger[href]:hover,.label-danger[href]:focus{background-color:#c9302c}.badge{display:inline-block;min-width:10px;padding:3px 7px;font-size:13px;font-weight:300;color:#ebebeb;line-height:1;vertical-align:baseline;white-space:nowrap;text-align:center;background-color:#4e5d6c;border-radius:10px}.badge:empty{display:none}.btn .badge{position:relative;top:-1px}.btn-xs .badge{top:0;padding:1px 5px}a.badge:hover,a.badge:focus{color:#ffffff;text-decoration:none;cursor:pointer}a.list-group-item.active>.badge,.nav-pills>.active>a>.badge{color:#df691a;background-color:#ffffff}.nav-pills>li>a>.badge{margin-left:3px}.jumbotron{padding:30px;margin-bottom:30px;color:inherit;background-color:#4e5d6c}.jumbotron h1,.jumbotron .h1{color:inherit}.jumbotron p{margin-bottom:15px;font-size:23px;font-weight:200}.container .jumbotron{border-radius:0}.jumbotron .container{max-width:100%}@media screen and (min-width:768px){.jumbotron{padding-top:48px;padding-bottom:48px}.container .jumbotron{padding-left:60px;padding-right:60px}.jumbotron h1,.jumbotron .h1{font-size:67.5px}}.thumbnail{display:block;padding:4px;margin-bottom:21px;line-height:1.42857143;background-color:#2b3e50;border:1px solid #dddddd;border-radius:0;-webkit-transition:all .2s ease-in-out;transition:all .2s ease-in-out}.thumbnail>img,.thumbnail a>img{margin-left:auto;margin-right:auto}a.thumbnail:hover,a.thumbnail:focus,a.thumbnail.active{border-color:#df691a}.thumbnail .caption{padding:9px;color:#ebebeb}.alert{padding:15px;margin-bottom:21px;border:1px solid transparent;border-radius:0}.alert h4{margin-top:0;color:inherit}.alert .alert-link{font-weight:bold}.alert>p,.alert>ul{margin-bottom:0}.alert>p+p{margin-top:5px}.alert-dismissable{padding-right:35px}.alert-dismissable .close{position:relative;top:-2px;right:-21px;color:inherit}.alert-success{background-color:#5cb85c;border-color:transparent;color:#ebebeb}.alert-success hr{border-top-color:rgba(0,0,0,0)}.alert-success .alert-link{color:#d2d2d2}.alert-info{background-color:#5bc0de;border-color:transparent;color:#ebebeb}.alert-info hr{border-top-color:rgba(0,0,0,0)}.alert-info .alert-link{color:#d2d2d2}.alert-warning{background-color:#f0ad4e;border-color:transparent;color:#ebebeb}.alert-warning hr{border-top-color:rgba(0,0,0,0)}.alert-warning .alert-link{color:#d2d2d2}.alert-danger{background-color:#d9534f;border-color:transparent;color:#ebebeb}.alert-danger hr{border-top-color:rgba(0,0,0,0)}.alert-danger .alert-link{color:#d2d2d2}@-webkit-keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}@keyframes progress-bar-stripes{from{background-position:40px 0}to{background-position:0 0}}.progress{overflow:hidden;height:21px;margin-bottom:21px;background-color:#4e5d6c;border-radius:0;-webkit-box-shadow:inset 0 1px 2px rgba(0,0,0,0.1);box-shadow:inset 0 1px 2px rgba(0,0,0,0.1)}.progress-bar{float:left;width:0%;height:100%;font-size:13px;line-height:21px;color:#ffffff;text-align:center;background-color:#df691a;-webkit-box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);box-shadow:inset 0 -1px 0 rgba(0,0,0,0.15);-webkit-transition:width 0.6s ease;transition:width 0.6s ease}.progress-striped .progress-bar{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-size:40px 40px}.progress.active .progress-bar{-webkit-animation:progress-bar-stripes 2s linear infinite;animation:progress-bar-stripes 2s linear infinite}.progress-bar-success{background-color:#5cb85c}.progress-striped .progress-bar-success{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-info{background-color:#5bc0de}.progress-striped .progress-bar-info{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-warning{background-color:#f0ad4e}.progress-striped .progress-bar-warning{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.progress-bar-danger{background-color:#d9534f}.progress-striped .progress-bar-danger{background-image:-webkit-linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent);background-image:linear-gradient(45deg, rgba(255,255,255,0.15) 25%, transparent 25%, transparent 50%, rgba(255,255,255,0.15) 50%, rgba(255,255,255,0.15) 75%, transparent 75%, transparent)}.media,.media-body{overflow:hidden;zoom:1}.media,.media .media{margin-top:15px}.media:first-child{margin-top:0}.media-object{display:block}.media-heading{margin:0 0 5px}.media>.pull-left{margin-right:10px}.media>.pull-right{margin-left:10px}.media-list{padding-left:0;list-style:none}.list-group{margin-bottom:20px;padding-left:0}.list-group-item{position:relative;display:block;padding:10px 15px;margin-bottom:-1px;background-color:#4e5d6c;border:1px solid transparent}.list-group-item:first-child{border-top-right-radius:0;border-top-left-radius:0}.list-group-item:last-child{margin-bottom:0;border-bottom-right-radius:0;border-bottom-left-radius:0}.list-group-item>.badge{float:right}.list-group-item>.badge+.badge{margin-right:5px}a.list-group-item{color:#ebebeb}a.list-group-item .list-group-item-heading{color:#ebebeb}a.list-group-item:hover,a.list-group-item:focus{text-decoration:none;background-color:#485563}a.list-group-item.active,a.list-group-item.active:hover,a.list-group-item.active:focus{z-index:2;color:#ffffff;background-color:#df691a;border-color:#df691a}a.list-group-item.active .list-group-item-heading,a.list-group-item.active:hover .list-group-item-heading,a.list-group-item.active:focus .list-group-item-heading{color:inherit}a.list-group-item.active .list-group-item-text,a.list-group-item.active:hover .list-group-item-text,a.list-group-item.active:focus .list-group-item-text{color:#f9decc}.list-group-item-success{color:#ebebeb;background-color:#5cb85c}a.list-group-item-success{color:#ebebeb}a.list-group-item-success .list-group-item-heading{color:inherit}a.list-group-item-success:hover,a.list-group-item-success:focus{color:#ebebeb;background-color:#4cae4c}a.list-group-item-success.active,a.list-group-item-success.active:hover,a.list-group-item-success.active:focus{color:#fff;background-color:#ebebeb;border-color:#ebebeb}.list-group-item-info{color:#ebebeb;background-color:#5bc0de}a.list-group-item-info{color:#ebebeb}a.list-group-item-info .list-group-item-heading{color:inherit}a.list-group-item-info:hover,a.list-group-item-info:focus{color:#ebebeb;background-color:#46b8da}a.list-group-item-info.active,a.list-group-item-info.active:hover,a.list-group-item-info.active:focus{color:#fff;background-color:#ebebeb;border-color:#ebebeb}.list-group-item-warning{color:#ebebeb;background-color:#f0ad4e}a.list-group-item-warning{color:#ebebeb}a.list-group-item-warning .list-group-item-heading{color:inherit}a.list-group-item-warning:hover,a.list-group-item-warning:focus{color:#ebebeb;background-color:#eea236}a.list-group-item-warning.active,a.list-group-item-warning.active:hover,a.list-group-item-warning.active:focus{color:#fff;background-color:#ebebeb;border-color:#ebebeb}.list-group-item-danger{color:#ebebeb;background-color:#d9534f}a.list-group-item-danger{color:#ebebeb}a.list-group-item-danger .list-group-item-heading{color:inherit}a.list-group-item-danger:hover,a.list-group-item-danger:focus{color:#ebebeb;background-color:#d43f3a}a.list-group-item-danger.active,a.list-group-item-danger.active:hover,a.list-group-item-danger.active:focus{color:#fff;background-color:#ebebeb;border-color:#ebebeb}.list-group-item-heading{margin-top:0;margin-bottom:5px}.list-group-item-text{margin-bottom:0;line-height:1.3}.panel{margin-bottom:21px;background-color:#4e5d6c;border:1px solid transparent;border-radius:0;-webkit-box-shadow:0 1px 1px rgba(0,0,0,0.05);box-shadow:0 1px 1px rgba(0,0,0,0.05)}.panel-body{padding:15px}.panel-heading{padding:10px 15px;border-bottom:1px solid transparent;border-top-right-radius:-1;border-top-left-radius:-1}.panel-heading>.dropdown .dropdown-toggle{color:inherit}.panel-title{margin-top:0;margin-bottom:0;font-size:17px;color:inherit}.panel-title>a{color:inherit}.panel-footer{padding:10px 15px;background-color:#485563;border-top:1px solid transparent;border-bottom-right-radius:-1;border-bottom-left-radius:-1}.panel>.list-group{margin-bottom:0}.panel>.list-group .list-group-item{border-width:1px 0;border-radius:0}.panel>.list-group:first-child .list-group-item:first-child{border-top:0;border-top-right-radius:-1;border-top-left-radius:-1}.panel>.list-group:last-child .list-group-item:last-child{border-bottom:0;border-bottom-right-radius:-1;border-bottom-left-radius:-1}.panel-heading+.list-group .list-group-item:first-child{border-top-width:0}.panel>.table,.panel>.table-responsive>.table{margin-bottom:0}.panel>.table:first-child,.panel>.table-responsive:first-child>.table:first-child{border-top-right-radius:-1;border-top-left-radius:-1}.panel>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:first-child,.panel>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:first-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:first-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:first-child{border-top-left-radius:-1}.panel>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child td:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child td:last-child,.panel>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>thead:first-child>tr:first-child th:last-child,.panel>.table:first-child>tbody:first-child>tr:first-child th:last-child,.panel>.table-responsive:first-child>.table:first-child>tbody:first-child>tr:first-child th:last-child{border-top-right-radius:-1}.panel>.table:last-child,.panel>.table-responsive:last-child>.table:last-child{border-bottom-right-radius:-1;border-bottom-left-radius:-1}.panel>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:first-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:first-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:first-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:first-child{border-bottom-left-radius:-1}.panel>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child td:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child td:last-child,.panel>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tbody:last-child>tr:last-child th:last-child,.panel>.table:last-child>tfoot:last-child>tr:last-child th:last-child,.panel>.table-responsive:last-child>.table:last-child>tfoot:last-child>tr:last-child th:last-child{border-bottom-right-radius:-1}.panel>.panel-body+.table,.panel>.panel-body+.table-responsive{border-top:1px solid #4e5d6c}.panel>.table>tbody:first-child>tr:first-child th,.panel>.table>tbody:first-child>tr:first-child td{border-top:0}.panel>.table-bordered,.panel>.table-responsive>.table-bordered{border:0}.panel>.table-bordered>thead>tr>th:first-child,.panel>.table-responsive>.table-bordered>thead>tr>th:first-child,.panel>.table-bordered>tbody>tr>th:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:first-child,.panel>.table-bordered>tfoot>tr>th:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:first-child,.panel>.table-bordered>thead>tr>td:first-child,.panel>.table-responsive>.table-bordered>thead>tr>td:first-child,.panel>.table-bordered>tbody>tr>td:first-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:first-child,.panel>.table-bordered>tfoot>tr>td:first-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:first-child{border-left:0}.panel>.table-bordered>thead>tr>th:last-child,.panel>.table-responsive>.table-bordered>thead>tr>th:last-child,.panel>.table-bordered>tbody>tr>th:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>th:last-child,.panel>.table-bordered>tfoot>tr>th:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>th:last-child,.panel>.table-bordered>thead>tr>td:last-child,.panel>.table-responsive>.table-bordered>thead>tr>td:last-child,.panel>.table-bordered>tbody>tr>td:last-child,.panel>.table-responsive>.table-bordered>tbody>tr>td:last-child,.panel>.table-bordered>tfoot>tr>td:last-child,.panel>.table-responsive>.table-bordered>tfoot>tr>td:last-child{border-right:0}.panel>.table-bordered>thead>tr:first-child>td,.panel>.table-responsive>.table-bordered>thead>tr:first-child>td,.panel>.table-bordered>tbody>tr:first-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>td,.panel>.table-bordered>thead>tr:first-child>th,.panel>.table-responsive>.table-bordered>thead>tr:first-child>th,.panel>.table-bordered>tbody>tr:first-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:first-child>th{border-bottom:0}.panel>.table-bordered>tbody>tr:last-child>td,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>td,.panel>.table-bordered>tfoot>tr:last-child>td,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>td,.panel>.table-bordered>tbody>tr:last-child>th,.panel>.table-responsive>.table-bordered>tbody>tr:last-child>th,.panel>.table-bordered>tfoot>tr:last-child>th,.panel>.table-responsive>.table-bordered>tfoot>tr:last-child>th{border-bottom:0}.panel>.table-responsive{border:0;margin-bottom:0}.panel-group{margin-bottom:21px}.panel-group .panel{margin-bottom:0;border-radius:0;overflow:hidden}.panel-group .panel+.panel{margin-top:5px}.panel-group .panel-heading{border-bottom:0}.panel-group .panel-heading+.panel-collapse .panel-body{border-top:1px solid transparent}.panel-group .panel-footer{border-top:0}.panel-group .panel-footer+.panel-collapse .panel-body{border-bottom:1px solid transparent}.panel-default{border-color:transparent}.panel-default>.panel-heading{color:#333333;background-color:#f5f5f5;border-color:transparent}.panel-default>.panel-heading+.panel-collapse .panel-body{border-top-color:transparent}.panel-default>.panel-footer+.panel-collapse .panel-body{border-bottom-color:transparent}.panel-primary{border-color:transparent}.panel-primary>.panel-heading{color:#ffffff;background-color:#df691a;border-color:transparent}.panel-primary>.panel-heading+.panel-collapse .panel-body{border-top-color:transparent}.panel-primary>.panel-footer+.panel-collapse .panel-body{border-bottom-color:transparent}.panel-success{border-color:transparent}.panel-success>.panel-heading{color:#ebebeb;background-color:#5cb85c;border-color:transparent}.panel-success>.panel-heading+.panel-collapse .panel-body{border-top-color:transparent}.panel-success>.panel-footer+.panel-collapse .panel-body{border-bottom-color:transparent}.panel-info{border-color:transparent}.panel-info>.panel-heading{color:#ebebeb;background-color:#5bc0de;border-color:transparent}.panel-info>.panel-heading+.panel-collapse .panel-body{border-top-color:transparent}.panel-info>.panel-footer+.panel-collapse .panel-body{border-bottom-color:transparent}.panel-warning{border-color:transparent}.panel-warning>.panel-heading{color:#ebebeb;background-color:#f0ad4e;border-color:transparent}.panel-warning>.panel-heading+.panel-collapse .panel-body{border-top-color:transparent}.panel-warning>.panel-footer+.panel-collapse .panel-body{border-bottom-color:transparent}.panel-danger{border-color:transparent}.panel-danger>.panel-heading{color:#ebebeb;background-color:#d9534f;border-color:transparent}.panel-danger>.panel-heading+.panel-collapse .panel-body{border-top-color:transparent}.panel-danger>.panel-footer+.panel-collapse .panel-body{border-bottom-color:transparent}.well{min-height:20px;padding:19px;margin-bottom:20px;background-color:#4e5d6c;border:1px solid transparent;border-radius:0;-webkit-box-shadow:inset 0 1px 1px rgba(0,0,0,0.05);box-shadow:inset 0 1px 1px rgba(0,0,0,0.05)}.well blockquote{border-color:#ddd;border-color:rgba(0,0,0,0.15)}.well-lg{padding:24px;border-radius:0}.well-sm{padding:9px;border-radius:0}.close{float:right;font-size:22.5px;font-weight:bold;line-height:1;color:#ebebeb;text-shadow:none;opacity:0.2;filter:alpha(opacity=20)}.close:hover,.close:focus{color:#ebebeb;text-decoration:none;cursor:pointer;opacity:0.5;filter:alpha(opacity=50)}button.close{padding:0;cursor:pointer;background:transparent;border:0;-webkit-appearance:none}.modal-open{overflow:hidden}.modal{display:none;overflow:auto;overflow-y:scroll;position:fixed;top:0;right:0;bottom:0;left:0;z-index:1050;-webkit-overflow-scrolling:touch;outline:0}.modal.fade .modal-dialog{-webkit-transform:translate(0, -25%);-ms-transform:translate(0, -25%);transform:translate(0, -25%);-webkit-transition:-webkit-transform .3s ease-out;-moz-transition:-moz-transform .3s ease-out;-o-transition:-o-transform .3s ease-out;transition:transform .3s ease-out}.modal.in .modal-dialog{-webkit-transform:translate(0, 0);-ms-transform:translate(0, 0);transform:translate(0, 0)}.modal-dialog{position:relative;width:auto;margin:10px}.modal-content{position:relative;background-color:#4e5d6c;border:1px solid transparent;border-radius:0;-webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);box-shadow:0 3px 9px rgba(0,0,0,0.5);background-clip:padding-box;outline:none}.modal-backdrop{position:fixed;top:0;right:0;bottom:0;left:0;z-index:1040;background-color:#000000}.modal-backdrop.fade{opacity:0;filter:alpha(opacity=0)}.modal-backdrop.in{opacity:0.5;filter:alpha(opacity=50)}.modal-header{padding:15px;border-bottom:1px solid #2b3e50;min-height:16.42857143px}.modal-header .close{margin-top:-2px}.modal-title{margin:0;line-height:1.42857143}.modal-body{position:relative;padding:20px}.modal-footer{margin-top:15px;padding:19px 20px 20px;text-align:right;border-top:1px solid #2b3e50}.modal-footer .btn+.btn{margin-left:5px;margin-bottom:0}.modal-footer .btn-group .btn+.btn{margin-left:-1px}.modal-footer .btn-block+.btn-block{margin-left:0}@media (min-width:768px){.modal-dialog{width:600px;margin:30px auto}.modal-content{-webkit-box-shadow:0 5px 15px rgba(0,0,0,0.5);box-shadow:0 5px 15px rgba(0,0,0,0.5)}.modal-sm{width:300px}}@media (min-width:992px){.modal-lg{width:900px}}.tooltip{position:absolute;z-index:1030;display:block;visibility:visible;font-size:13px;line-height:1.4;opacity:0;filter:alpha(opacity=0)}.tooltip.in{opacity:0.9;filter:alpha(opacity=90)}.tooltip.top{margin-top:-3px;padding:5px 0}.tooltip.right{margin-left:3px;padding:0 5px}.tooltip.bottom{margin-top:3px;padding:5px 0}.tooltip.left{margin-left:-3px;padding:0 5px}.tooltip-inner{max-width:200px;padding:3px 8px;color:#ffffff;text-align:center;text-decoration:none;background-color:#000000;border-radius:0}.tooltip-arrow{position:absolute;width:0;height:0;border-color:transparent;border-style:solid}.tooltip.top .tooltip-arrow{bottom:0;left:50%;margin-left:-5px;border-width:5px 5px 0;border-top-color:#000000}.tooltip.top-left .tooltip-arrow{bottom:0;left:5px;border-width:5px 5px 0;border-top-color:#000000}.tooltip.top-right .tooltip-arrow{bottom:0;right:5px;border-width:5px 5px 0;border-top-color:#000000}.tooltip.right .tooltip-arrow{top:50%;left:0;margin-top:-5px;border-width:5px 5px 5px 0;border-right-color:#000000}.tooltip.left .tooltip-arrow{top:50%;right:0;margin-top:-5px;border-width:5px 0 5px 5px;border-left-color:#000000}.tooltip.bottom .tooltip-arrow{top:0;left:50%;margin-left:-5px;border-width:0 5px 5px;border-bottom-color:#000000}.tooltip.bottom-left .tooltip-arrow{top:0;left:5px;border-width:0 5px 5px;border-bottom-color:#000000}.tooltip.bottom-right .tooltip-arrow{top:0;right:5px;border-width:0 5px 5px;border-bottom-color:#000000}.popover{position:absolute;top:0;left:0;z-index:1010;display:none;max-width:276px;padding:1px;text-align:left;background-color:#4e5d6c;background-clip:padding-box;border:1px solid transparent;border-radius:0;-webkit-box-shadow:0 5px 10px rgba(0,0,0,0.2);box-shadow:0 5px 10px rgba(0,0,0,0.2);white-space:normal}.popover.top{margin-top:-10px}.popover.right{margin-left:10px}.popover.bottom{margin-top:10px}.popover.left{margin-left:-10px}.popover-title{margin:0;padding:8px 14px;font-size:15px;font-weight:normal;line-height:18px;background-color:#485563;border-bottom:1px solid #3d4954;border-radius:5px 5px 0 0}.popover-content{padding:9px 14px}.popover>.arrow,.popover>.arrow:after{position:absolute;display:block;width:0;height:0;border-color:transparent;border-style:solid}.popover>.arrow{border-width:11px}.popover>.arrow:after{border-width:10px;content:""}.popover.top>.arrow{left:50%;margin-left:-11px;border-bottom-width:0;border-top-color:transparent;bottom:-11px}.popover.top>.arrow:after{content:" ";bottom:1px;margin-left:-10px;border-bottom-width:0;border-top-color:#4e5d6c}.popover.right>.arrow{top:50%;left:-11px;margin-top:-11px;border-left-width:0;border-right-color:transparent}.popover.right>.arrow:after{content:" ";left:1px;bottom:-10px;border-left-width:0;border-right-color:#4e5d6c}.popover.bottom>.arrow{left:50%;margin-left:-11px;border-top-width:0;border-bottom-color:transparent;top:-11px}.popover.bottom>.arrow:after{content:" ";top:1px;margin-left:-10px;border-top-width:0;border-bottom-color:#4e5d6c}.popover.left>.arrow{top:50%;right:-11px;margin-top:-11px;border-right-width:0;border-left-color:transparent}.popover.left>.arrow:after{content:" ";right:1px;border-right-width:0;border-left-color:#4e5d6c;bottom:-10px}.carousel{position:relative}.carousel-inner{position:relative;overflow:hidden;width:100%}.carousel-inner>.item{display:none;position:relative;-webkit-transition:.6s ease-in-out left;transition:.6s ease-in-out left}.carousel-inner>.item>img,.carousel-inner>.item>a>img{line-height:1}.carousel-inner>.active,.carousel-inner>.next,.carousel-inner>.prev{display:block}.carousel-inner>.active{left:0}.carousel-inner>.next,.carousel-inner>.prev{position:absolute;top:0;width:100%}.carousel-inner>.next{left:100%}.carousel-inner>.prev{left:-100%}.carousel-inner>.next.left,.carousel-inner>.prev.right{left:0}.carousel-inner>.active.left{left:-100%}.carousel-inner>.active.right{left:100%}.carousel-control{position:absolute;top:0;left:0;bottom:0;width:15%;opacity:0.5;filter:alpha(opacity=50);font-size:20px;color:#ffffff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6)}.carousel-control.left{background-image:-webkit-linear-gradient(left, color-stop(rgba(0,0,0,0.5) 0), color-stop(rgba(0,0,0,0.0001) 100%));background-image:linear-gradient(to right, rgba(0,0,0,0.5) 0, rgba(0,0,0,0.0001) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#80000000', endColorstr='#00000000', GradientType=1)}.carousel-control.right{left:auto;right:0;background-image:-webkit-linear-gradient(left, color-stop(rgba(0,0,0,0.0001) 0), color-stop(rgba(0,0,0,0.5) 100%));background-image:linear-gradient(to right, rgba(0,0,0,0.0001) 0, rgba(0,0,0,0.5) 100%);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#00000000', endColorstr='#80000000', GradientType=1)}.carousel-control:hover,.carousel-control:focus{outline:none;color:#ffffff;text-decoration:none;opacity:0.9;filter:alpha(opacity=90)}.carousel-control .icon-prev,.carousel-control .icon-next,.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right{position:absolute;top:50%;z-index:5;display:inline-block}.carousel-control .icon-prev,.carousel-control .glyphicon-chevron-left{left:50%}.carousel-control .icon-next,.carousel-control .glyphicon-chevron-right{right:50%}.carousel-control .icon-prev,.carousel-control .icon-next{width:20px;height:20px;margin-top:-10px;margin-left:-10px;font-family:serif}.carousel-control .icon-prev:before{content:'\2039'}.carousel-control .icon-next:before{content:'\203a'}.carousel-indicators{position:absolute;bottom:10px;left:50%;z-index:15;width:60%;margin-left:-30%;padding-left:0;list-style:none;text-align:center}.carousel-indicators li{display:inline-block;width:10px;height:10px;margin:1px;text-indent:-999px;border:1px solid #ffffff;border-radius:10px;cursor:pointer;background-color:#000 \9;background-color:rgba(0,0,0,0)}.carousel-indicators .active{margin:0;width:12px;height:12px;background-color:#ffffff}.carousel-caption{position:absolute;left:15%;right:15%;bottom:20px;z-index:10;padding-top:20px;padding-bottom:20px;color:#ffffff;text-align:center;text-shadow:0 1px 2px rgba(0,0,0,0.6)}.carousel-caption .btn{text-shadow:none}@media screen and (min-width:768px){.carousel-control .glyphicon-chevron-left,.carousel-control .glyphicon-chevron-right,.carousel-control .icon-prev,.carousel-control .icon-next{width:30px;height:30px;margin-top:-15px;margin-left:-15px;font-size:30px}.carousel-caption{left:20%;right:20%;padding-bottom:30px}.carousel-indicators{bottom:20px}}.clearfix:before,.clearfix:after,.container:before,.container:after,.container-fluid:before,.container-fluid:after,.row:before,.row:after,.form-horizontal .form-group:before,.form-horizontal .form-group:after,.btn-toolbar:before,.btn-toolbar:after,.btn-group-vertical>.btn-group:before,.btn-group-vertical>.btn-group:after,.nav:before,.nav:after,.navbar:before,.navbar:after,.navbar-header:before,.navbar-header:after,.navbar-collapse:before,.navbar-collapse:after,.pager:before,.pager:after,.panel-body:before,.panel-body:after,.modal-footer:before,.modal-footer:after{content:" ";display:table}.clearfix:after,.container:after,.container-fluid:after,.row:after,.form-horizontal .form-group:after,.btn-toolbar:after,.btn-group-vertical>.btn-group:after,.nav:after,.navbar:after,.navbar-header:after,.navbar-collapse:after,.pager:after,.panel-body:after,.modal-footer:after{clear:both}.center-block{display:block;margin-left:auto;margin-right:auto}.pull-right{float:right !important}.pull-left{float:left !important}.hide{display:none !important}.show{display:block !important}.invisible{visibility:hidden}.text-hide{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.hidden{display:none !important;visibility:hidden !important}.affix{position:fixed}@-ms-viewport{width:device-width}.visible-xs,.visible-sm,.visible-md,.visible-lg{display:none !important}@media (max-width:767px){.visible-xs{display:block !important}table.visible-xs{display:table}tr.visible-xs{display:table-row !important}th.visible-xs,td.visible-xs{display:table-cell !important}}@media (min-width:768px) and (max-width:991px){.visible-sm{display:block !important}table.visible-sm{display:table}tr.visible-sm{display:table-row !important}th.visible-sm,td.visible-sm{display:table-cell !important}}@media (min-width:992px) and (max-width:1199px){.visible-md{display:block !important}table.visible-md{display:table}tr.visible-md{display:table-row !important}th.visible-md,td.visible-md{display:table-cell !important}}@media (min-width:1200px){.visible-lg{display:block !important}table.visible-lg{display:table}tr.visible-lg{display:table-row !important}th.visible-lg,td.visible-lg{display:table-cell !important}}@media (max-width:767px){.hidden-xs{display:none !important}}@media (min-width:768px) and (max-width:991px){.hidden-sm{display:none !important}}@media (min-width:992px) and (max-width:1199px){.hidden-md{display:none !important}}@media (min-width:1200px){.hidden-lg{display:none !important}}.visible-print{display:none !important}@media print{.visible-print{display:block !important}table.visible-print{display:table}tr.visible-print{display:table-row !important}th.visible-print,td.visible-print{display:table-cell !important}}@media print{.hidden-print{display:none !important}}.navbar{-webkit-box-shadow:none;box-shadow:none;border:none;font-size:12px}.navbar-default .badge{background-color:#fff;color:#4e5d6c}.navbar-inverse .badge{background-color:#fff;color:#df691a}.btn{font-weight:300}.btn-default:hover{background-color:#485563}.btn-sm,.btn-xs{font-size:12px}body{font-weight:300}.text-primary,.text-primary:hover{color:#df691a}.text-success,.text-success:hover{color:#5cb85c}.text-danger,.text-danger:hover{color:#d9534f}.text-warning,.text-warning:hover{color:#f0ad4e}.text-info,.text-info:hover{color:#5bc0de}.page-header{border-bottom-color:#4e5d6c}.dropdown-menu{border:none;margin:0;-webkit-box-shadow:none;box-shadow:none}.dropdown-menu>li>a{font-size:12px;font-weight:300}.btn-group.open .dropdown-toggle{-webkit-box-shadow:none;box-shadow:none}.dropdown-header{font-size:12px}table,.table{font-size:12px}table a,.table a{color:#fff;text-decoration:underline}table .text-muted,.table .text-muted{color:#4e5d6c}table>thead>tr>th,.table>thead>tr>th,table>tbody>tr>th,.table>tbody>tr>th,table>tfoot>tr>th,.table>tfoot>tr>th,table>thead>tr>td,.table>thead>tr>td,table>tbody>tr>td,.table>tbody>tr>td,table>tfoot>tr>td,.table>tfoot>tr>td{border-color:transparent}input,textarea{color:#2b3e50}label,.radio label,.checkbox label,.help-block{font-size:12px;font-weight:300}.input-addon,.input-group-addon{color:#ebebeb}.has-warning .help-block,.has-warning .control-label{color:#f0ad4e}.has-error .help-block,.has-error .control-label{color:#d9534f}.has-success .help-block,.has-success .control-label{color:#5cb85c}.form-control:focus{-webkit-box-shadow:none;box-shadow:none}.has-warning .form-control:focus,.has-error .form-control:focus,.has-success .form-control:focus{-webkit-box-shadow:none;box-shadow:none}.nav .open>a,.nav .open>a:hover,.nav .open>a:focus{border-color:transparent}.nav-tabs>li>a{color:#ebebeb}.nav-pills>li>a{color:#ebebeb}.pager a{color:#ebebeb}.close{opacity:0.4}.close:hover,.close:focus{opacity:1}.label{font-weight:300}.alert{color:#fff}.alert a,.alert .alert-link{color:#fff}.well{-webkit-box-shadow:none;box-shadow:none}.panel{border:none}.panel-default>.panel-heading{background-color:#485563;color:#ebebeb}.thumbnail{background-color:#4e5d6c;border:none}.modal{padding:0}.modal-header,.modal-footer{background-color:#485563;border:none;border-radius:0}.popover-title{border:none} \ No newline at end of file diff --git a/doc/website/css/font-awesome.min.css b/doc/website/css/font-awesome.min.css new file mode 100644 index 000000000..ec53d4d6d --- /dev/null +++ b/doc/website/css/font-awesome.min.css @@ -0,0 +1,4 @@ +/*! + * Font Awesome 4.2.0 by @davegandy - http://fontawesome.io - @fontawesome + * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) + */@font-face{font-family:'FontAwesome';src:url('../fonts/fontawesome-webfont.eot?v=4.2.0');src:url('../fonts/fontawesome-webfont.eot?#iefix&v=4.2.0') format('embedded-opentype'),url('../fonts/fontawesome-webfont.woff?v=4.2.0') format('woff'),url('../fonts/fontawesome-webfont.ttf?v=4.2.0') format('truetype'),url('../fonts/fontawesome-webfont.svg?v=4.2.0#fontawesomeregular') format('svg');font-weight:normal;font-style:normal}.fa{display:inline-block;font:normal normal normal 14px/1 FontAwesome;font-size:inherit;text-rendering:auto;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}.fa-lg{font-size:1.33333333em;line-height:.75em;vertical-align:-15%}.fa-2x{font-size:2em}.fa-3x{font-size:3em}.fa-4x{font-size:4em}.fa-5x{font-size:5em}.fa-fw{width:1.28571429em;text-align:center}.fa-ul{padding-left:0;margin-left:2.14285714em;list-style-type:none}.fa-ul>li{position:relative}.fa-li{position:absolute;left:-2.14285714em;width:2.14285714em;top:.14285714em;text-align:center}.fa-li.fa-lg{left:-1.85714286em}.fa-border{padding:.2em .25em .15em;border:solid .08em #eee;border-radius:.1em}.pull-right{float:right}.pull-left{float:left}.fa.pull-left{margin-right:.3em}.fa.pull-right{margin-left:.3em}.fa-spin{-webkit-animation:fa-spin 2s infinite linear;animation:fa-spin 2s infinite linear}@-webkit-keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}@keyframes fa-spin{0%{-webkit-transform:rotate(0deg);transform:rotate(0deg)}100%{-webkit-transform:rotate(359deg);transform:rotate(359deg)}}.fa-rotate-90{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=1);-webkit-transform:rotate(90deg);-ms-transform:rotate(90deg);transform:rotate(90deg)}.fa-rotate-180{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2);-webkit-transform:rotate(180deg);-ms-transform:rotate(180deg);transform:rotate(180deg)}.fa-rotate-270{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=3);-webkit-transform:rotate(270deg);-ms-transform:rotate(270deg);transform:rotate(270deg)}.fa-flip-horizontal{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=0, mirror=1);-webkit-transform:scale(-1, 1);-ms-transform:scale(-1, 1);transform:scale(-1, 1)}.fa-flip-vertical{filter:progid:DXImageTransform.Microsoft.BasicImage(rotation=2, mirror=1);-webkit-transform:scale(1, -1);-ms-transform:scale(1, -1);transform:scale(1, -1)}:root .fa-rotate-90,:root .fa-rotate-180,:root .fa-rotate-270,:root .fa-flip-horizontal,:root .fa-flip-vertical{filter:none}.fa-stack{position:relative;display:inline-block;width:2em;height:2em;line-height:2em;vertical-align:middle}.fa-stack-1x,.fa-stack-2x{position:absolute;left:0;width:100%;text-align:center}.fa-stack-1x{line-height:inherit}.fa-stack-2x{font-size:2em}.fa-inverse{color:#fff}.fa-glass:before{content:"\f000"}.fa-music:before{content:"\f001"}.fa-search:before{content:"\f002"}.fa-envelope-o:before{content:"\f003"}.fa-heart:before{content:"\f004"}.fa-star:before{content:"\f005"}.fa-star-o:before{content:"\f006"}.fa-user:before{content:"\f007"}.fa-film:before{content:"\f008"}.fa-th-large:before{content:"\f009"}.fa-th:before{content:"\f00a"}.fa-th-list:before{content:"\f00b"}.fa-check:before{content:"\f00c"}.fa-remove:before,.fa-close:before,.fa-times:before{content:"\f00d"}.fa-search-plus:before{content:"\f00e"}.fa-search-minus:before{content:"\f010"}.fa-power-off:before{content:"\f011"}.fa-signal:before{content:"\f012"}.fa-gear:before,.fa-cog:before{content:"\f013"}.fa-trash-o:before{content:"\f014"}.fa-home:before{content:"\f015"}.fa-file-o:before{content:"\f016"}.fa-clock-o:before{content:"\f017"}.fa-road:before{content:"\f018"}.fa-download:before{content:"\f019"}.fa-arrow-circle-o-down:before{content:"\f01a"}.fa-arrow-circle-o-up:before{content:"\f01b"}.fa-inbox:before{content:"\f01c"}.fa-play-circle-o:before{content:"\f01d"}.fa-rotate-right:before,.fa-repeat:before{content:"\f01e"}.fa-refresh:before{content:"\f021"}.fa-list-alt:before{content:"\f022"}.fa-lock:before{content:"\f023"}.fa-flag:before{content:"\f024"}.fa-headphones:before{content:"\f025"}.fa-volume-off:before{content:"\f026"}.fa-volume-down:before{content:"\f027"}.fa-volume-up:before{content:"\f028"}.fa-qrcode:before{content:"\f029"}.fa-barcode:before{content:"\f02a"}.fa-tag:before{content:"\f02b"}.fa-tags:before{content:"\f02c"}.fa-book:before{content:"\f02d"}.fa-bookmark:before{content:"\f02e"}.fa-print:before{content:"\f02f"}.fa-camera:before{content:"\f030"}.fa-font:before{content:"\f031"}.fa-bold:before{content:"\f032"}.fa-italic:before{content:"\f033"}.fa-text-height:before{content:"\f034"}.fa-text-width:before{content:"\f035"}.fa-align-left:before{content:"\f036"}.fa-align-center:before{content:"\f037"}.fa-align-right:before{content:"\f038"}.fa-align-justify:before{content:"\f039"}.fa-list:before{content:"\f03a"}.fa-dedent:before,.fa-outdent:before{content:"\f03b"}.fa-indent:before{content:"\f03c"}.fa-video-camera:before{content:"\f03d"}.fa-photo:before,.fa-image:before,.fa-picture-o:before{content:"\f03e"}.fa-pencil:before{content:"\f040"}.fa-map-marker:before{content:"\f041"}.fa-adjust:before{content:"\f042"}.fa-tint:before{content:"\f043"}.fa-edit:before,.fa-pencil-square-o:before{content:"\f044"}.fa-share-square-o:before{content:"\f045"}.fa-check-square-o:before{content:"\f046"}.fa-arrows:before{content:"\f047"}.fa-step-backward:before{content:"\f048"}.fa-fast-backward:before{content:"\f049"}.fa-backward:before{content:"\f04a"}.fa-play:before{content:"\f04b"}.fa-pause:before{content:"\f04c"}.fa-stop:before{content:"\f04d"}.fa-forward:before{content:"\f04e"}.fa-fast-forward:before{content:"\f050"}.fa-step-forward:before{content:"\f051"}.fa-eject:before{content:"\f052"}.fa-chevron-left:before{content:"\f053"}.fa-chevron-right:before{content:"\f054"}.fa-plus-circle:before{content:"\f055"}.fa-minus-circle:before{content:"\f056"}.fa-times-circle:before{content:"\f057"}.fa-check-circle:before{content:"\f058"}.fa-question-circle:before{content:"\f059"}.fa-info-circle:before{content:"\f05a"}.fa-crosshairs:before{content:"\f05b"}.fa-times-circle-o:before{content:"\f05c"}.fa-check-circle-o:before{content:"\f05d"}.fa-ban:before{content:"\f05e"}.fa-arrow-left:before{content:"\f060"}.fa-arrow-right:before{content:"\f061"}.fa-arrow-up:before{content:"\f062"}.fa-arrow-down:before{content:"\f063"}.fa-mail-forward:before,.fa-share:before{content:"\f064"}.fa-expand:before{content:"\f065"}.fa-compress:before{content:"\f066"}.fa-plus:before{content:"\f067"}.fa-minus:before{content:"\f068"}.fa-asterisk:before{content:"\f069"}.fa-exclamation-circle:before{content:"\f06a"}.fa-gift:before{content:"\f06b"}.fa-leaf:before{content:"\f06c"}.fa-fire:before{content:"\f06d"}.fa-eye:before{content:"\f06e"}.fa-eye-slash:before{content:"\f070"}.fa-warning:before,.fa-exclamation-triangle:before{content:"\f071"}.fa-plane:before{content:"\f072"}.fa-calendar:before{content:"\f073"}.fa-random:before{content:"\f074"}.fa-comment:before{content:"\f075"}.fa-magnet:before{content:"\f076"}.fa-chevron-up:before{content:"\f077"}.fa-chevron-down:before{content:"\f078"}.fa-retweet:before{content:"\f079"}.fa-shopping-cart:before{content:"\f07a"}.fa-folder:before{content:"\f07b"}.fa-folder-open:before{content:"\f07c"}.fa-arrows-v:before{content:"\f07d"}.fa-arrows-h:before{content:"\f07e"}.fa-bar-chart-o:before,.fa-bar-chart:before{content:"\f080"}.fa-twitter-square:before{content:"\f081"}.fa-facebook-square:before{content:"\f082"}.fa-camera-retro:before{content:"\f083"}.fa-key:before{content:"\f084"}.fa-gears:before,.fa-cogs:before{content:"\f085"}.fa-comments:before{content:"\f086"}.fa-thumbs-o-up:before{content:"\f087"}.fa-thumbs-o-down:before{content:"\f088"}.fa-star-half:before{content:"\f089"}.fa-heart-o:before{content:"\f08a"}.fa-sign-out:before{content:"\f08b"}.fa-linkedin-square:before{content:"\f08c"}.fa-thumb-tack:before{content:"\f08d"}.fa-external-link:before{content:"\f08e"}.fa-sign-in:before{content:"\f090"}.fa-trophy:before{content:"\f091"}.fa-github-square:before{content:"\f092"}.fa-upload:before{content:"\f093"}.fa-lemon-o:before{content:"\f094"}.fa-phone:before{content:"\f095"}.fa-square-o:before{content:"\f096"}.fa-bookmark-o:before{content:"\f097"}.fa-phone-square:before{content:"\f098"}.fa-twitter:before{content:"\f099"}.fa-facebook:before{content:"\f09a"}.fa-github:before{content:"\f09b"}.fa-unlock:before{content:"\f09c"}.fa-credit-card:before{content:"\f09d"}.fa-rss:before{content:"\f09e"}.fa-hdd-o:before{content:"\f0a0"}.fa-bullhorn:before{content:"\f0a1"}.fa-bell:before{content:"\f0f3"}.fa-certificate:before{content:"\f0a3"}.fa-hand-o-right:before{content:"\f0a4"}.fa-hand-o-left:before{content:"\f0a5"}.fa-hand-o-up:before{content:"\f0a6"}.fa-hand-o-down:before{content:"\f0a7"}.fa-arrow-circle-left:before{content:"\f0a8"}.fa-arrow-circle-right:before{content:"\f0a9"}.fa-arrow-circle-up:before{content:"\f0aa"}.fa-arrow-circle-down:before{content:"\f0ab"}.fa-globe:before{content:"\f0ac"}.fa-wrench:before{content:"\f0ad"}.fa-tasks:before{content:"\f0ae"}.fa-filter:before{content:"\f0b0"}.fa-briefcase:before{content:"\f0b1"}.fa-arrows-alt:before{content:"\f0b2"}.fa-group:before,.fa-users:before{content:"\f0c0"}.fa-chain:before,.fa-link:before{content:"\f0c1"}.fa-cloud:before{content:"\f0c2"}.fa-flask:before{content:"\f0c3"}.fa-cut:before,.fa-scissors:before{content:"\f0c4"}.fa-copy:before,.fa-files-o:before{content:"\f0c5"}.fa-paperclip:before{content:"\f0c6"}.fa-save:before,.fa-floppy-o:before{content:"\f0c7"}.fa-square:before{content:"\f0c8"}.fa-navicon:before,.fa-reorder:before,.fa-bars:before{content:"\f0c9"}.fa-list-ul:before{content:"\f0ca"}.fa-list-ol:before{content:"\f0cb"}.fa-strikethrough:before{content:"\f0cc"}.fa-underline:before{content:"\f0cd"}.fa-table:before{content:"\f0ce"}.fa-magic:before{content:"\f0d0"}.fa-truck:before{content:"\f0d1"}.fa-pinterest:before{content:"\f0d2"}.fa-pinterest-square:before{content:"\f0d3"}.fa-google-plus-square:before{content:"\f0d4"}.fa-google-plus:before{content:"\f0d5"}.fa-money:before{content:"\f0d6"}.fa-caret-down:before{content:"\f0d7"}.fa-caret-up:before{content:"\f0d8"}.fa-caret-left:before{content:"\f0d9"}.fa-caret-right:before{content:"\f0da"}.fa-columns:before{content:"\f0db"}.fa-unsorted:before,.fa-sort:before{content:"\f0dc"}.fa-sort-down:before,.fa-sort-desc:before{content:"\f0dd"}.fa-sort-up:before,.fa-sort-asc:before{content:"\f0de"}.fa-envelope:before{content:"\f0e0"}.fa-linkedin:before{content:"\f0e1"}.fa-rotate-left:before,.fa-undo:before{content:"\f0e2"}.fa-legal:before,.fa-gavel:before{content:"\f0e3"}.fa-dashboard:before,.fa-tachometer:before{content:"\f0e4"}.fa-comment-o:before{content:"\f0e5"}.fa-comments-o:before{content:"\f0e6"}.fa-flash:before,.fa-bolt:before{content:"\f0e7"}.fa-sitemap:before{content:"\f0e8"}.fa-umbrella:before{content:"\f0e9"}.fa-paste:before,.fa-clipboard:before{content:"\f0ea"}.fa-lightbulb-o:before{content:"\f0eb"}.fa-exchange:before{content:"\f0ec"}.fa-cloud-download:before{content:"\f0ed"}.fa-cloud-upload:before{content:"\f0ee"}.fa-user-md:before{content:"\f0f0"}.fa-stethoscope:before{content:"\f0f1"}.fa-suitcase:before{content:"\f0f2"}.fa-bell-o:before{content:"\f0a2"}.fa-coffee:before{content:"\f0f4"}.fa-cutlery:before{content:"\f0f5"}.fa-file-text-o:before{content:"\f0f6"}.fa-building-o:before{content:"\f0f7"}.fa-hospital-o:before{content:"\f0f8"}.fa-ambulance:before{content:"\f0f9"}.fa-medkit:before{content:"\f0fa"}.fa-fighter-jet:before{content:"\f0fb"}.fa-beer:before{content:"\f0fc"}.fa-h-square:before{content:"\f0fd"}.fa-plus-square:before{content:"\f0fe"}.fa-angle-double-left:before{content:"\f100"}.fa-angle-double-right:before{content:"\f101"}.fa-angle-double-up:before{content:"\f102"}.fa-angle-double-down:before{content:"\f103"}.fa-angle-left:before{content:"\f104"}.fa-angle-right:before{content:"\f105"}.fa-angle-up:before{content:"\f106"}.fa-angle-down:before{content:"\f107"}.fa-desktop:before{content:"\f108"}.fa-laptop:before{content:"\f109"}.fa-tablet:before{content:"\f10a"}.fa-mobile-phone:before,.fa-mobile:before{content:"\f10b"}.fa-circle-o:before{content:"\f10c"}.fa-quote-left:before{content:"\f10d"}.fa-quote-right:before{content:"\f10e"}.fa-spinner:before{content:"\f110"}.fa-circle:before{content:"\f111"}.fa-mail-reply:before,.fa-reply:before{content:"\f112"}.fa-github-alt:before{content:"\f113"}.fa-folder-o:before{content:"\f114"}.fa-folder-open-o:before{content:"\f115"}.fa-smile-o:before{content:"\f118"}.fa-frown-o:before{content:"\f119"}.fa-meh-o:before{content:"\f11a"}.fa-gamepad:before{content:"\f11b"}.fa-keyboard-o:before{content:"\f11c"}.fa-flag-o:before{content:"\f11d"}.fa-flag-checkered:before{content:"\f11e"}.fa-terminal:before{content:"\f120"}.fa-code:before{content:"\f121"}.fa-mail-reply-all:before,.fa-reply-all:before{content:"\f122"}.fa-star-half-empty:before,.fa-star-half-full:before,.fa-star-half-o:before{content:"\f123"}.fa-location-arrow:before{content:"\f124"}.fa-crop:before{content:"\f125"}.fa-code-fork:before{content:"\f126"}.fa-unlink:before,.fa-chain-broken:before{content:"\f127"}.fa-question:before{content:"\f128"}.fa-info:before{content:"\f129"}.fa-exclamation:before{content:"\f12a"}.fa-superscript:before{content:"\f12b"}.fa-subscript:before{content:"\f12c"}.fa-eraser:before{content:"\f12d"}.fa-puzzle-piece:before{content:"\f12e"}.fa-microphone:before{content:"\f130"}.fa-microphone-slash:before{content:"\f131"}.fa-shield:before{content:"\f132"}.fa-calendar-o:before{content:"\f133"}.fa-fire-extinguisher:before{content:"\f134"}.fa-rocket:before{content:"\f135"}.fa-maxcdn:before{content:"\f136"}.fa-chevron-circle-left:before{content:"\f137"}.fa-chevron-circle-right:before{content:"\f138"}.fa-chevron-circle-up:before{content:"\f139"}.fa-chevron-circle-down:before{content:"\f13a"}.fa-html5:before{content:"\f13b"}.fa-css3:before{content:"\f13c"}.fa-anchor:before{content:"\f13d"}.fa-unlock-alt:before{content:"\f13e"}.fa-bullseye:before{content:"\f140"}.fa-ellipsis-h:before{content:"\f141"}.fa-ellipsis-v:before{content:"\f142"}.fa-rss-square:before{content:"\f143"}.fa-play-circle:before{content:"\f144"}.fa-ticket:before{content:"\f145"}.fa-minus-square:before{content:"\f146"}.fa-minus-square-o:before{content:"\f147"}.fa-level-up:before{content:"\f148"}.fa-level-down:before{content:"\f149"}.fa-check-square:before{content:"\f14a"}.fa-pencil-square:before{content:"\f14b"}.fa-external-link-square:before{content:"\f14c"}.fa-share-square:before{content:"\f14d"}.fa-compass:before{content:"\f14e"}.fa-toggle-down:before,.fa-caret-square-o-down:before{content:"\f150"}.fa-toggle-up:before,.fa-caret-square-o-up:before{content:"\f151"}.fa-toggle-right:before,.fa-caret-square-o-right:before{content:"\f152"}.fa-euro:before,.fa-eur:before{content:"\f153"}.fa-gbp:before{content:"\f154"}.fa-dollar:before,.fa-usd:before{content:"\f155"}.fa-rupee:before,.fa-inr:before{content:"\f156"}.fa-cny:before,.fa-rmb:before,.fa-yen:before,.fa-jpy:before{content:"\f157"}.fa-ruble:before,.fa-rouble:before,.fa-rub:before{content:"\f158"}.fa-won:before,.fa-krw:before{content:"\f159"}.fa-bitcoin:before,.fa-btc:before{content:"\f15a"}.fa-file:before{content:"\f15b"}.fa-file-text:before{content:"\f15c"}.fa-sort-alpha-asc:before{content:"\f15d"}.fa-sort-alpha-desc:before{content:"\f15e"}.fa-sort-amount-asc:before{content:"\f160"}.fa-sort-amount-desc:before{content:"\f161"}.fa-sort-numeric-asc:before{content:"\f162"}.fa-sort-numeric-desc:before{content:"\f163"}.fa-thumbs-up:before{content:"\f164"}.fa-thumbs-down:before{content:"\f165"}.fa-youtube-square:before{content:"\f166"}.fa-youtube:before{content:"\f167"}.fa-xing:before{content:"\f168"}.fa-xing-square:before{content:"\f169"}.fa-youtube-play:before{content:"\f16a"}.fa-dropbox:before{content:"\f16b"}.fa-stack-overflow:before{content:"\f16c"}.fa-instagram:before{content:"\f16d"}.fa-flickr:before{content:"\f16e"}.fa-adn:before{content:"\f170"}.fa-bitbucket:before{content:"\f171"}.fa-bitbucket-square:before{content:"\f172"}.fa-tumblr:before{content:"\f173"}.fa-tumblr-square:before{content:"\f174"}.fa-long-arrow-down:before{content:"\f175"}.fa-long-arrow-up:before{content:"\f176"}.fa-long-arrow-left:before{content:"\f177"}.fa-long-arrow-right:before{content:"\f178"}.fa-apple:before{content:"\f179"}.fa-windows:before{content:"\f17a"}.fa-android:before{content:"\f17b"}.fa-linux:before{content:"\f17c"}.fa-dribbble:before{content:"\f17d"}.fa-skype:before{content:"\f17e"}.fa-foursquare:before{content:"\f180"}.fa-trello:before{content:"\f181"}.fa-female:before{content:"\f182"}.fa-male:before{content:"\f183"}.fa-gittip:before{content:"\f184"}.fa-sun-o:before{content:"\f185"}.fa-moon-o:before{content:"\f186"}.fa-archive:before{content:"\f187"}.fa-bug:before{content:"\f188"}.fa-vk:before{content:"\f189"}.fa-weibo:before{content:"\f18a"}.fa-renren:before{content:"\f18b"}.fa-pagelines:before{content:"\f18c"}.fa-stack-exchange:before{content:"\f18d"}.fa-arrow-circle-o-right:before{content:"\f18e"}.fa-arrow-circle-o-left:before{content:"\f190"}.fa-toggle-left:before,.fa-caret-square-o-left:before{content:"\f191"}.fa-dot-circle-o:before{content:"\f192"}.fa-wheelchair:before{content:"\f193"}.fa-vimeo-square:before{content:"\f194"}.fa-turkish-lira:before,.fa-try:before{content:"\f195"}.fa-plus-square-o:before{content:"\f196"}.fa-space-shuttle:before{content:"\f197"}.fa-slack:before{content:"\f198"}.fa-envelope-square:before{content:"\f199"}.fa-wordpress:before{content:"\f19a"}.fa-openid:before{content:"\f19b"}.fa-institution:before,.fa-bank:before,.fa-university:before{content:"\f19c"}.fa-mortar-board:before,.fa-graduation-cap:before{content:"\f19d"}.fa-yahoo:before{content:"\f19e"}.fa-google:before{content:"\f1a0"}.fa-reddit:before{content:"\f1a1"}.fa-reddit-square:before{content:"\f1a2"}.fa-stumbleupon-circle:before{content:"\f1a3"}.fa-stumbleupon:before{content:"\f1a4"}.fa-delicious:before{content:"\f1a5"}.fa-digg:before{content:"\f1a6"}.fa-pied-piper:before{content:"\f1a7"}.fa-pied-piper-alt:before{content:"\f1a8"}.fa-drupal:before{content:"\f1a9"}.fa-joomla:before{content:"\f1aa"}.fa-language:before{content:"\f1ab"}.fa-fax:before{content:"\f1ac"}.fa-building:before{content:"\f1ad"}.fa-child:before{content:"\f1ae"}.fa-paw:before{content:"\f1b0"}.fa-spoon:before{content:"\f1b1"}.fa-cube:before{content:"\f1b2"}.fa-cubes:before{content:"\f1b3"}.fa-behance:before{content:"\f1b4"}.fa-behance-square:before{content:"\f1b5"}.fa-steam:before{content:"\f1b6"}.fa-steam-square:before{content:"\f1b7"}.fa-recycle:before{content:"\f1b8"}.fa-automobile:before,.fa-car:before{content:"\f1b9"}.fa-cab:before,.fa-taxi:before{content:"\f1ba"}.fa-tree:before{content:"\f1bb"}.fa-spotify:before{content:"\f1bc"}.fa-deviantart:before{content:"\f1bd"}.fa-soundcloud:before{content:"\f1be"}.fa-database:before{content:"\f1c0"}.fa-file-pdf-o:before{content:"\f1c1"}.fa-file-word-o:before{content:"\f1c2"}.fa-file-excel-o:before{content:"\f1c3"}.fa-file-powerpoint-o:before{content:"\f1c4"}.fa-file-photo-o:before,.fa-file-picture-o:before,.fa-file-image-o:before{content:"\f1c5"}.fa-file-zip-o:before,.fa-file-archive-o:before{content:"\f1c6"}.fa-file-sound-o:before,.fa-file-audio-o:before{content:"\f1c7"}.fa-file-movie-o:before,.fa-file-video-o:before{content:"\f1c8"}.fa-file-code-o:before{content:"\f1c9"}.fa-vine:before{content:"\f1ca"}.fa-codepen:before{content:"\f1cb"}.fa-jsfiddle:before{content:"\f1cc"}.fa-life-bouy:before,.fa-life-buoy:before,.fa-life-saver:before,.fa-support:before,.fa-life-ring:before{content:"\f1cd"}.fa-circle-o-notch:before{content:"\f1ce"}.fa-ra:before,.fa-rebel:before{content:"\f1d0"}.fa-ge:before,.fa-empire:before{content:"\f1d1"}.fa-git-square:before{content:"\f1d2"}.fa-git:before{content:"\f1d3"}.fa-hacker-news:before{content:"\f1d4"}.fa-tencent-weibo:before{content:"\f1d5"}.fa-qq:before{content:"\f1d6"}.fa-wechat:before,.fa-weixin:before{content:"\f1d7"}.fa-send:before,.fa-paper-plane:before{content:"\f1d8"}.fa-send-o:before,.fa-paper-plane-o:before{content:"\f1d9"}.fa-history:before{content:"\f1da"}.fa-circle-thin:before{content:"\f1db"}.fa-header:before{content:"\f1dc"}.fa-paragraph:before{content:"\f1dd"}.fa-sliders:before{content:"\f1de"}.fa-share-alt:before{content:"\f1e0"}.fa-share-alt-square:before{content:"\f1e1"}.fa-bomb:before{content:"\f1e2"}.fa-soccer-ball-o:before,.fa-futbol-o:before{content:"\f1e3"}.fa-tty:before{content:"\f1e4"}.fa-binoculars:before{content:"\f1e5"}.fa-plug:before{content:"\f1e6"}.fa-slideshare:before{content:"\f1e7"}.fa-twitch:before{content:"\f1e8"}.fa-yelp:before{content:"\f1e9"}.fa-newspaper-o:before{content:"\f1ea"}.fa-wifi:before{content:"\f1eb"}.fa-calculator:before{content:"\f1ec"}.fa-paypal:before{content:"\f1ed"}.fa-google-wallet:before{content:"\f1ee"}.fa-cc-visa:before{content:"\f1f0"}.fa-cc-mastercard:before{content:"\f1f1"}.fa-cc-discover:before{content:"\f1f2"}.fa-cc-amex:before{content:"\f1f3"}.fa-cc-paypal:before{content:"\f1f4"}.fa-cc-stripe:before{content:"\f1f5"}.fa-bell-slash:before{content:"\f1f6"}.fa-bell-slash-o:before{content:"\f1f7"}.fa-trash:before{content:"\f1f8"}.fa-copyright:before{content:"\f1f9"}.fa-at:before{content:"\f1fa"}.fa-eyedropper:before{content:"\f1fb"}.fa-paint-brush:before{content:"\f1fc"}.fa-birthday-cake:before{content:"\f1fd"}.fa-area-chart:before{content:"\f1fe"}.fa-pie-chart:before{content:"\f200"}.fa-line-chart:before{content:"\f201"}.fa-lastfm:before{content:"\f202"}.fa-lastfm-square:before{content:"\f203"}.fa-toggle-off:before{content:"\f204"}.fa-toggle-on:before{content:"\f205"}.fa-bicycle:before{content:"\f206"}.fa-bus:before{content:"\f207"}.fa-ioxhost:before{content:"\f208"}.fa-angellist:before{content:"\f209"}.fa-cc:before{content:"\f20a"}.fa-shekel:before,.fa-sheqel:before,.fa-ils:before{content:"\f20b"}.fa-meanpath:before{content:"\f20c"} \ No newline at end of file diff --git a/doc/website/css/highlight.tomorrow-night.css b/doc/website/css/highlight.tomorrow-night.css new file mode 100644 index 000000000..a486af9e9 --- /dev/null +++ b/doc/website/css/highlight.tomorrow-night.css @@ -0,0 +1,45 @@ +/* Tomorrow Night Theme */ +/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ +/* Original theme - https://github.com/chriskempson/tomorrow-theme */ +/* http://jmblog.github.com/color-themes-for-google-code-highlightjs */ +.tomorrow-comment, pre .comment, pre .title { + color: #969896; +} + +.tomorrow-red, pre .variable, pre .attribute, pre .tag, pre .regexp, pre .ruby .constant, pre .xml .tag .title, pre .xml .pi, pre .xml .doctype, pre .html .doctype, pre .css .id, pre .css .class, pre .css .pseudo { + color: #cc6666; +} + +.tomorrow-orange, pre .number, pre .preprocessor, pre .built_in, pre .literal, pre .params, pre .constant { + color: #de935f; +} + +.tomorrow-yellow, pre .class, pre .ruby .class .title, pre .css .rules .attribute { + color: #f0c674; +} + +.tomorrow-green, pre .string, pre .value, pre .inheritance, pre .header, pre .ruby .symbol, pre .xml .cdata { + color: #b5bd68; +} + +.tomorrow-aqua, pre .css .hexcolor { + color: #8abeb7; +} + +.tomorrow-blue, pre .function, pre .python .decorator, pre .python .title, pre .ruby .function .title, pre .ruby .title .keyword, pre .perl .sub, pre .javascript .title, pre .coffeescript .title { + color: #81a2be; +} + +.tomorrow-purple, pre .keyword, pre .javascript .function { + color: #b294bb; +} + +pre code { + display: block; + background: #1d1f21; + color: #c5c8c6; + font-family: Menlo, Monaco, Consolas, monospace; + line-height: 1.5; + border: 1px solid #ccc; + padding: 10px; +} diff --git a/doc/website/css/main.css b/doc/website/css/main.css new file mode 100644 index 000000000..5f35644fe --- /dev/null +++ b/doc/website/css/main.css @@ -0,0 +1,98 @@ +body { + font-size: 19px; +} + +main { + margin-top: 90px; +} + +section { + margin-bottom: 50px; +} + +h1, h2, h3, h4 { + color: #df691a; +} +h3 { + font-size: 23px; +} + +li { + margin-bottom: 3px; +} + +img { + max-width: 100%; +} + +header.navbar { + opacity: 0.9; +} +.navbar .navbar-brand { + font-size: 28px; + height: auto; + line-height: 50px; + margin-left: 20px; + color: #df691a; +} +.navbar a.navbar-brand:hover { + color: #df691a; +} +.navbar .navbar-brand small { + font-size: 18px; + font-weight: 300; + margin-left: 10px; + color: white; +} + +@media (min-width: 768px) { + #sidebar { + position: absolute; + } +} +@media (max-width: 960px) { + body { + font-size: 17px; + } + pre { + font-size: 12px; + } +} + +.page-header { + margin-top: 0; +} + +#sidebar .github-star { + margin-top: 20px; + margin-left: 50px; +} + +#sidebar .text-muted { + color: #859AAF; +} + +pre { + padding: 0; + border-color: #3D5166; + background-color: #1D2B3A; + border-radius: 4px; + margin: 15px; +} +pre code { + border: none; + background-color: #1D2B3A; +} + +code { + font-size: 85%; + padding: 4px 4px 1px; + margin: 0 4px; + border-radius: 3px; + color: #c5c8c6; + border: solid 1px #3D5166; + background-color: #1D2B3A; + white-space: pre-wrap; + white-space: -moz-pre-wrap; + word-wrap: break-word; +} diff --git a/doc/website/default.twig b/doc/website/default.twig new file mode 100644 index 000000000..7a1db87a1 --- /dev/null +++ b/doc/website/default.twig @@ -0,0 +1,80 @@ + + + + + + + {{ title }} + + + + + + + + + + +
+
+ + {% if menu is defined %} + + + + {% endif %} + + + +
+
+ + + + + + + + + diff --git a/doc/website/fonts/FontAwesome.otf b/doc/website/fonts/FontAwesome.otf new file mode 100644 index 0000000000000000000000000000000000000000..81c9ad949b47f64afeca5642ee2494b6e3147f44 GIT binary patch literal 85908 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 uQr82 literal 0 HcmV?d00001 diff --git a/doc/website/fonts/fontawesome-webfont.eot b/doc/website/fonts/fontawesome-webfont.eot new file mode 100644 index 0000000000000000000000000000000000000000..84677bc0c5f37f1fac9d87548c4554b5c91717cf GIT binary patch literal 56006 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�<^?u>Q{42JLrwuTYxyMKSr<(b06ndn)vd52hUM!% zo+=6@Asd2Mt*`H2sR1R`U2HTIDK{QgFI-sf_w#=Hc>2)O72x1WWGjJwy|G3;8Lo3I z;fA?8FdLIbD*-wjw7xejv4gDku$%G7c*#@sPfhc-n!AO>OuF%j-?XwXUS7ykNX&3? z!u)Z6Q>3L<*X>O%#A3T!QDBA_=0F5x69h#-#eNU)Cyy(c?O%ASv4n_;a`Y90#cL_D z(_;K&7BdBS`J_nWZ_JL5DA0W?m~FeDOb;1CL-`_tHz28nc6m`SQQE6yLCA~WRrufi ztUuACikW)SJ5Y4^StEqFw?m;Gvd#t`Lh;r{4h2nmXn#Bpmj<%X^mBSvCtqR~(=H_D zeIfuZQY56zYsSffvzGA1J=vJY14|~3Aotir_OVHV8KjI$T0RSb){Cx=vS-xgKhz>* zL;lI5b{q)SVMqwPr;*W-;znYr7J+s0NnUbQq5R0zB{nMji2e>3-D&B?2q4GYMEj7v zKFX$+)S{)1LN%w=dVpGo_XyD-x0vN|DUwuAODoPzAo>oV+F-|=sv$T~&m!(ntMxj~ z@DMj&coe2m!4aj2`$psp8tyFqRu9=*_e<#$qy&!;{%LUPC4bEliFJ5`3j1pl>Jdy6 zN|N5I{R;&z{aZs|sJ0KLvA89L^sC$##Tu|{3rOeS6#~8IVwMEMNkUfx4~>P(%^Mnr z1daO_0S0*45?yX9N;^zDp}l2fTgr(X8h2-D@Kh@h1kt0e6q<~tR%~<_?4xhPZOcB- z2IlV598vw70#5ga9J|LJ>8Vlm|Fzl_{OON4Nu9^OpV}t#oyJ9lF@399@#JsCfb^7E ztdo;YeIgfr#TGhyQTa>{!fXK6Bst>H;2f|Ca4&RWK%`Yy5G$gdWv zNQG%s?rJm*hiGdIPQQ6Ffuw^O+O)|gKCjCxH!5WoX0lr)nJ?Um%IFZkPXI~Hc%5-+ zC$mgDJLJyF=EPNviXh(qiW)b50a&07Tzgzrdl!HU9TM>`(GY6r8%o@$_jv?LTJ>a? zh`8r{la`Qa@cqS$u7DGvMm2pWPWmXF*GoKo(KCylN~w}lz$DQ1?Y6dZ&g1P;+lFn6 zk=oK=GJ%|CQ596!-m5pbaZ3%>@?;SrFNuKu(c;kk)2yeVwcZ3E_V6uCwvbxs!tBd7 zfU@>bxjO%R4JL1j1YXv@>b?vPR4`@@832~)B&^F%Wi`Kqa5ex(aoigbix#I4iS6F7 z2ceAACyyvn%6edB7BVznRiNUc@S7(|d3y$R;tywo+K?;rnELw}Szgm^x+u`mlx6mI zMqgj8MUP_P9hLehpk~wKe?(+TsNTPKC`N*X(Gif2-jfrkncE4|1n5>~O3}LGLZP6a zf}SW*gHPJ}#rt8P_+WhB>xFI%bO^YCBVj4AE%H6~?gPhE>!ppnF53O69+(p%WR z(KgL8sZ9?e`9x=UMQAFem(LPV>pNhb>n0!7Ii67*1;ymR4Pd8bqmf$xaRtrLX!y(# zN&&+fwWeHWKg;-n;n-!NO)h_khtF?0E!XO_c>X&_+J2aA?Yy_^0hQ0+CvAa--EdBl|+HaenEjw)O-AJKya{G zH)C!2b}($wfOO*Dd$8D1c}OqixgW=X4-Y9R3ZTJiO8C?8_fNb&Z~{VgxgaP+bv|RE z9O4t+ENy|tMN82C`r%R%N-0VnY8W;KFDqSuh}9GUn<($h@XGVxabgfT~ z#UxysSn0e*IoA2Fu*^IoW6aS&r#qWcrIXfcpyhrka%lvVshhufjcnExd@9f4bD0iM zT~s4fpy(fG_&#z}%KaX#Cb<94H{N!rEE(()?dxTAsLo~e0}GZpIt)otg7@&)2N5AD20|Ij`&7E>~l+qec~wv z3TWXDff|6P4qZP2fVYjiT=0R}X83&&B_F*H#qoz`^P%@zjciPA@G>I;eY|p(d-Poo z+SKXJYe}e!nQ{sZ-Q14@$~qRh3BKh#r`lSK5Z5EA_57X1S_&}fq*Sy?==X0 zfZ+wW1m%v1F3!!Tgwld|k{|a$Qq1Uv`1e`x%AFXtQSe1MhmyYMh!Fvr#c*}legb3p z4c?HEY%S4h$k(+;eb;yuxp+fEHFH6=mv*WiVQ5UXb+q*AS_7md*3lph9o8w)7=(fO z(@0$-0s-OEo1A&|kN{Nf1Lw=abN_8z@!W`*Vjfiwkvf4&wiNqT4R%I`D)O?xLwd@YD?Bh)s zWVQVs9y(yq4o#EK2gtSrb#V|#LsnZ3p7h1=%nkPY&KiA54KNdM%j7eYSey8{R24HV z6c%2izaZ4w&M|*iP>8}f!m7{Pk4c^8I$_`eUtYi&<1o~Gx~Uet(^CruO=GxMelaT< z0r&WFdYWvul}nS=ESC?rsL%`WBt(kJtAauKvQm*{Q-m=D@td1Y#orGyU)u89dsQi1*<)Frv2U zW>geM7&K@C6mO*==pC4lFd;oR@-<$ljPG*j&2@7uWV!xoO|Q6ep78;xak#4Lg3%hv z9NxP=d{avX>miQ>I@B>LXi~htsUSevh{y+<=;%~pa>gRjuz4T)8_>1sIzGFLmjf&? zg3u~4VfZr$lENgw&;$xTgu+Ld#usKsU|euvK2b=P_(%UOOX_^9E7p!o$xLjS*Vdga zT=pVc(jB)Zz9~A?R~Re6vWWO}l@>p3QY9u$)ds_=+KE@UoT29mMJquRl3g#A2MKvfXb98&%GJF~V zSqVkC&abwDLPbL6=;kI(>WZW|e@pIp*0d#+Mkx?C9fB{>-&^I?Fo}K!Sf?pvBIX@; zfvY@xW}^1!i~8YnmEv1Fl;~oBVNkI0lz8gQKP_R?l%l<- zbAur*jYkVF!dfbr5h0+X#Ffn`gW9dDZVXe$0<*fLe)r`%eB-7e1KU?zZ~pyya(cfv z6NuDaM@8kFjUX@r^K=RLfpJG6v|LL?La+IU&UF!Ga2!(3V*3@7lK^VoZaHlphyDmG z-ng2m=yd1vzOBm;0rCQ{JCHrV4j&oCCe}QNct+hPEc_l)i zTeyXQM;Ud>6Pv@)L>Wu2a9_11&K@?Yy&t_S8VJ)faI=LsHnG zE&nGahOQ~<<^XHu?o(@C#tStK3P?1+PAkPdzF}zb>T%S1XsCJ@2Kybk+kUtAiuOu= znHeOU$0-2LT>?pD5VP zp7zhW9ZW(@66lmB22PrFs@SMNo`5$z+o8oXcmb79e?F#iqxlJNvPq1O3bX1k>%@jE zs0kypki=GEcJh63BCy(YR##SZW{x*<#V3(DkLnFILTU!AX!5$3YD1L1;|6_!qtO@g z)pir7gG57~H67fMaky1>Iv^IsPf@I~bxjJ>&~(7S&lvUA9n`IDl-T6fZLtxT-czQ? zg@iA@mbo^`;T*z=G3%hLVmhEzvay&B-rfzG3=$EF#@BR&;E(vh4LEAGw?Co1-Rg9v&%5FvOJ_@awz$&0by zyA!sDe&9hu+v*Rn-ET2Y6~mv)Um^vqCD(-9+SpB@7g`tYt-AePTyL?d^k>JFR^FVfw!-Zx+DAVGejcyXbR|uod zI7$sT4Y<0=zpruv&m`NaR1|a{SFb?5NtCP-MWq50y$Pd{gwU*uwTF!n)y%{`Q#{_p z^aRJP1WC&-xveL=SO+PFA>sXfQ~y4ofYE&ys=Q$ny6Ls@T}RTw@=WF2a25q-1nS^J z)bog{OB8g)$hO7?FuT}_W*Mq{dqBUji+AFMGK$USZSjny46-Au-(iO-E{!T^lzUm% z^#c~Xn(%d?&{_ATTr`lgX_|2vd-QWiaq*_Bi6gplBrhrm8nc7977n)gT{ZzDreScgHwG^T~2CSPY?!Xp2!B^;a-qld~G5h=iFq0!TqwUK5P{rgF#fL_(4L$(l}u^ggms47>)abIL2?mYa7 z{4IDQuCBHus14%Ug)nW$U7z?j_aZ5HTOsyh+#Neu!JK}NNrGgMR;AoVWPWbhxevU>@uYL#`!_-}n#i>gk52K|3CG+<*#-kxkzgf%_j)6XQ^M6<1pq_t1CRB)Uj>xTJCHo$~`F! zO2f*RDhYh8!e}g>rJJ9dnFuO&TVO3+Kix;x&`c^3JnFcA_dnEy&6BGKi25DTuH=A# za|Y&#+-39O&Y!l-+CvjDTJh*S{c>5%Z3&$t2Bz#7fJ*`u2T%|l|!47ormqORgAm_1c{ zOR}0L1k7Pf^hI=gHz>fert6I!5n|mC2K+)F8QP@-(lD@4r2O)?DMqTj0-<@F{Lr0a zYREA++GlC&oY>tMEB%C6GYS_sQji262-`+CPzmKaL54@0=~PYd*0CJ~(H-Sn5c?pv zwxIOKbtA%4>;lu>W!Zyh1KsQN_y2H0qAIIdkWEGZ$&i$qN{pK!FlV+ezGpKJhdcBIHAd6I%iIC+b_$uHEC5kD*HYi32aRt--#lIKYZsye%0+dUg|>f31Ka z`KG>#I1z=MGUR;+Ed~)Yv_1ZK`oil8z9!IUs_ni0iMp@RRizIjXjTJ_>J;g}4S*6U zDDKcbd59HOoY`QYh>qJ6!8LvpyTQN)(+<6B9d4_@rn17iQ>Om5VSAgA!OMyHakc%3 z7%#?mV@sNFMIBHIU|ls*>05&GfbBM6>{3`Sv+CKL0}Naa6X0e3aJ3dIk+Ax}-hDG*;k81elad=!j}+H@5>2DiZJM2@jvhoB~6UyZ_s448?3< zP?c|sx=eeaXhy{Xr*CqC4-mwm*?efHtaud%kQFN>Dejop=qCrN^~_NiX@f$&UhM|A z)C4S#TsXF@8f9>1nB|wCM=W{PG-vM3m<~36^;Jm@7GVkwZBDV!&92>u+fl!Ey*G+E&ycNh@Xa+ES2eFP+>c-KCLb+l4Icu2wj9W< z^5T$b+aKZssNo0+i=>#u1|;FV*p9lc_ zX5J4*NrN-&ZruD)nN%^tl!+3oZyMRm`o!aZY^z1xGh=195WVYnDfmt{T9Xz_mXAGe znCapUf5uulvNJ9-5O-nf!nl;nvSn4xm_e@_4!uNs1mjen)`cICTyaw>5f3bKVARfx zqk!lT3}W`Q^H%urOtz`JB9hiO(}s8}-9d>U>)Yx1*vhrYXw#=hbPJLpwY?`l+;;R3N_52R%LcRJ!b4*2(YO+oI1gGWqY!7D`=7^0mDkD$|0YaZeeeGv%cQ(+`#E1 z;qt#Z*?1)Gw{R|)zB_{cjGv}qQ&$TNMPItibTrEWKvAM6G)j!KsJU-g$lZLzUmq;V zM8pX_)7(Inbnx*}efGx#!)OiHvvv5<_!#cwXt8!PdO<_rRqQ15`qA{%duOa8c0>GA zb^hH}RC>`tnoe%B?=LVuUc5WGVHM&(Q6dweYhHBUA{g~B;IQ=AtsN&=SHGT@qXw!+ zP5%Ha3)(bHnAQKef*Y`_&A0DTtN8x3yt!2lDoEh8Q9v8sSxf1*!mtftSP5GoXczH2ppazABD~$0o2C zTc5Cq;z*hqa@f;|o$czp%KO_{&N@7#C&U8q|AmLc%OstvqPK?2|C2i37=sN4k=BUI zPu4{tHQKvzbJr97G!;+!2PdCX=td}5WLIlWcP1Jvik{E7U%ByUgnxy)R)cFF{u~HW zG1s`WBc??#3WuF(B(zcUrS$gjhVS^Igx95-mS8$h#n}}^X!Gau3C}=A!gJ-cXOHiP zrbp!O&L3eA66jbpRcxGpY7_nE)y1#^l%x#B?1Yj+mIF2^EXF;|?KZcqv!waJ;@Ooy zWB*DUe4w9|;zw`y(tW(g%XjiO6hZ5=?ZudbUE`xwlK0tjjK@av@nK=L#nWGgn^;8@ zT)hEg5)v+#r3263l*cU1ess$&MuUfFyakRG5k7wHZas+uzL_hX=n681($`E{uut(5 zZ+$X)Xl-g?YgtZG9OWX`{M7u}M}!dijHd6eJPCbhOd4KXDm7?z+-5oDCu`!#ioad` zK+-q#nD7Ob$1zNDS~u&elvahQZ6{w}l%Ty#-;#Muo0fPu<(aNU@vdXpAfVLUz%X>2(=X*`O$HaB&RAi3zcRGaxm@J;WR9dE7jlFBz}*X zsC#z(or&u&Kkx~h=7fxzcP~TJMufE7SP+IqDK7v0^t4rlzgAW)e;1DAk3VxBtXT!EE&AS`_g# zfeSZsr-M&G-dhk^fw3|~6n}9ieV$aOx%c7g%Qf_1K-9Vr|DcKhE47^cs;A!@$-s5` zmwin@dZD>+T@1e6+bQ=Xqr)+pGn)cPNP6=z&N9uJJ#meQsg9y;)`#}6xCx~^kok!q z4vG)>kvXSd(hoyiY_%>JXwewzu8_xE!Xr{;ZvQO=Btx7vAS`&t@08iR>6zRkKz~X_ z8IBBG9jMybK9$ZDY9MPSOfFsVT`7+_Zu~+5%2^YmM_}&os=^l&EZy5zk*Eqd6F7Di zw=|>@dwaAiin^d6{+C4*H>v`9K(Cf?Bb0wF|Ie;PV$$&Q@5^*fd|v|KPThv;{q1Y$ z11q#kjY{o465t~K!oX%k{en-aXw%B-XFrRVpqx(9pymg2>@h-=q|@BDdjT>lyN6c%h7m7Q?gEAu-as5r_TPWUrzvsw5*aN>(CvMUomr!X- z#sB_s^YR_eV$Z_rR!}yx*nF&+;Z}^xcI&#Zg2G9qv4&v2ck%%wh$HzuYfCaE|7oX1 zQlv02;_?jKO7X+sBfv}XxekESyT2aashP{FvMF0%pO3F(n$&CT{mWrf-xQ^Fbj>(4D-@F9}oYR zuan#HY7|YdNOK@rSA}CzSF`@8fe%q{mcRAp3VClfD4b7DN^rHCA@?am?5IsbM?6!Ho+xkJE z-#52u5@c!?1#0)w4Y_dcY2*idt4ZLJm-vZK%?e$<46H(L!`c)qmW@PAwumc{zLMJ= zBsX%UA*z0!(zM4EHU#K)2mZa*O|!(6BG+*>FZoJtKiGck87_DY9|YyNfbjIZP>!S_ zT0-ag0Lfd_pH2yU-#T$=b2I6E+~E=L$v5@BMBO2cNiBj4MkYyyT6xLw>Wn?6a_XHk zsvt)I==&j61B_VEUj(V@W?PTw0XENe5P6&zG_a7Fu@DKjz=28uYBki9NLpF)0~Dib zJ6aQta$L6y-J`vKalrD}ph?Qy&`McV#qtOJ@_Qy2F{Fq!Q9>ZxVQ<5VR<#}rl5IIp zi1Hx%#qbm7G`M&?kc0qAKUp1;)F;iZVoHU>>-pvd9ohn%{5|FvMD}~omEmn3z+u!i zx>DQ~FftNtYAJXryMco$rE$%>tSOXa+r_Db&M?p!gJsksi6_FH>pz!+=yK4=9#@dU z;O6JYBOkOh_Gd|a3+LZIQ<^yVf0Wc}2v(t;MPw#6F>>7!ONIDE4mNQG*fEwU=IqHx ze4f<(*KLOL&(Lvym(^qiIA8$AElK$iWP5tc=>z{w7YA1CqK*4(cj(y|^;Iq|za#{I z`0{J%?e0U#b65*w2)vymR(=^8v`8JnXD}RZtd0Kd3dZ|e!ew^xT6$=w-t`fX(7#ld z_O#nwSgMrHHu!oINXTwjU>P8R#L3^MiVf zpNitY8Dwz}279StlC^gK)}8pe+PLqH?T{+p&+&4qOCFXZnH=fih!T3SpQq7RT&(bA zA3&|c(XU$cjS7>h@9|x=(vsX^H#CAyiQO7xpf76dq zEcwEp&TU;vuBWSafwqqa;n(S$liSo;O=cLoWnEUB(9@6`HAwz&^0)e5Nk9)oju*!* zbX-5|$pREya!wAqY@9+HtWxsYe}56Vx$QCiOtEgb#&esDkfn;l#cbkBb}Kw{05vi$4E!j+E>Qv|X-L5$8+8@VdmA2zjGisS zyQhW-?U5YKJgo@plau#52|%G+YZix1O~C)mF>vq()r&0?2)T~RB+fYm3}bA$TAEO1 zf~nA3Ut0@wy=>TC~Xckr3cT@VYyS0EeJ|o zKkYp62hm~tsbm#nXJ>fAA+#PsBReMMYU8AI06uvJ{f(n)T9}}%8`r2KdAje93QH1vW5@!eL zF%^?9G}a}8Pf;>=Ki5&8^|~3ORi>uDEixuGj~qr#Ay}nuPR&tddEjIAMxW!fP6(6k zT$eA&)pTdTF_=nlCRgsx2RfoWZW^c$mkjpG<3i3vk!7S8S=LuVfnk<)vvWJBA+P|Et z1Vq;tBI$D>Fcs(>giAqfc~9wbe;zde1L*mz*Z>%KdTNX3+%WUHMCa^3Li+s2Leh~o zpU1{a=xbY<3G|OiJQG#X&M3_ z64?haImy)MSkZrj_RQZmyd+Loar$^@%gaSU!Riq4BX!}fn+@Ow!q!O%(ms^g z;z?Rq7NXcXG8X_)c-L4a2?dbyjKC6LF~Tr-^IFmd`>SY9TSiZwn=nX<>)tzgo(mb- zbUdH%#`&@W{GIikP9+jImhGsWr=g8cO-||o-Ed9lVsx0MN*)!i1D6*_--C7^~WZZ--uocYg z`R9Fw7B`nE*$5-aAicV1pgCSX_&ba1m$_1`Rh%v~3K=>-<8zb7I5j%8vM6x&6Z9mi zx>kGtRGEZzJV>ECt~kJfwnCc9*QDW5jsh#}-Co}G0P#qFT`7+NTgb;oJ{j-Kl&meW4jzzCQMa9$y zAzu>VV%=c$kY#wbSp28B_dN6b-o zFue70f6a#{n3zfDO@amwi6N11prToxEB2pklJ#@6LTd)ZEVNN^Vg_Q`e(0kI?_9K5 zMb-N|-oIvf;gpw1m0bZFn^wI&!$^3WF7~hlSi|6~w_&4^Z~_g<2He`EP75R4vNv=k z8rcTRqiE8-H}U7*OM``B`QZ9t$|#ps>Gobl+7plwj|*SkGwG+V62gSZ<=|mY?{3~; z&3^)Ro!+nZCFF!Zu#d}5);ac|Kue)1_@u|VB_~Xi7$~V_7`Nv9_|{j#jqgq}B1Ij& zJv{(P)LGC*Z4kP2K?WVG8Z5!)#W@ugIVDqZt&;`8b$RtbQas1Gd2(@*(USfc$6_md zG6EQjnVNZOEwpxUhBv<2aJ4w~e zm$0g<`IT1g6j~j4i66&}#Cxp!>xYgp{!sU?eaeT}l;+sh26B%XFaCYoTfcab8k{pSfOBf%}P8L~6 z8&3fiO*?xe>f}fcgHpQnWj$G<=gJ(gRuWelv zK(P%x5^PRc^d3)%>=^|1$OS|f5KA4EI@#DF%n1gcq&H`RV^BUA&8c=J`x#JM$v~ht z;Im>?+-bO+%Yhi=84#NtjWZo<4zg-RK%_>&M&aVPm@B{YChDR;7M7kun&Yu2v6EIg z*m{yFw;@!b-s`rn7RhY+s@$*vam=XkX66a`tCY+CttMqcP3Y^Ru0ltO266{EDmE2I zpL!CxgAHx6o?8P83)46Ov8JM6zgex8e9=SKbb<@#jh0CVvQ%GUDlnK0aLMig*eYaM zmc4tRx92<l^on%u^Q%JusNoNNdcuW0GSvj4=*rQ z=>baP8r0ej>Dn|x!f3IA-h60LMn~XIz>mJJ-ISD0G^0l+aA;m~%PZz1;9Q3dkp&K8 zu5dYBy6$~$eCY>fY#j)VLFUZ5f52&fd+DEGNImx7g`99I8CyNvRvA(3v*5GTZy3Na z&+thZX$pGfTKlGFvtEc$8>&G!;=*kC;fRSF4rX4)->f<=Y-S00Ysq zfG#n3z@6HTCF4+goN~lajh$%8U|7zJe4Pk&<28a7KWZ%acm&x_JU|%2t@kIwq;PWU ztAwA?0)ekIu0`tkb<$ORyTk2guymZu?fffJ@Fg2m>p_l>s^5_vSoP|24uA26I*nfk zD31(-NxdurhLEO{m`BzP`iY()PvR> z)E6AW*oZA-ErBSq@~RKE$Pa{Jp2;!E&uWMZWtNJ*6G=bGS?Ftfqw1atI5-4pJaCb( z>ORFM@EE^+lHUs!p}biPsmUchK%Pa!&yqhA%5u9Gv4L0H#AtPmrYxj?0?VfoxL6w= z0&QZSMCr@?Z8YXWlOKStQ^NPwq46>m6WN9|C>sfXa>Q;N>?n`iw%1u3>z*&EpBY4K zg@m`l@sNnR8H}WlF?kj3qI3!CValmGWg8;vyDnwLnorHP_LLps0ORdHZy1&D(ZE>F$*Xci(1_@;z` zBGVO|S9?ZBh)NQ}B`RVRy%4nvw?$t3E2br$R`^7#;Xw*KGgw9!#X83r0E5Jh4rKn| z0c``(A{<&x$_BZSKYRjMolFE*O@N%f!F0cnMn%i4EV`1K3wp!r>x1DakjbJDc|`)T zm+buTLj8ya0R-yK0AVEx3J-=37R8<5n=gpRsf#T4^wPH_cz~euy@A-&8~9BWAMcnI zcpL%{4y1iK9_O4=RRKMgPU_8+F~bs&f+&=WxEbEF@cLP^xtg^Nsvlz_wL3jUn3)dd zD7c<6VlawguycwP1hee$xD*Oepe=4<+;=e4D}TVC8Pae>C>pHv{WmDB{>K6a7=%W@ zX<9^SC2SGQ>JSvk;b}{tUW|GX_O?9xEHktvS3!nR%Pi4s zgC0G=?y>%M0GLQkD7p&QX|5(hvAr3y4cWkjYC$|@V(MtA`e?Z{NCKS@M-7KFEW({3 zwEl=V;^${8Jl^Rl-nt{0q-`S*0O&;H_>)lsvlcEv>oqea8}(176_(|hi!lc*QlV0z zpjHXLk>~u~)W%S{bPf~`u+E6WW zEzC@!KKuzluwXOp^9!UAnLC7RiC(920U)12x6rPN+j0UYl#oTT?}BD5(rUm8{{S!V zpBQ1wkr2C2M3RZ((h#naVBMgynlLH?HfGXHU*a^9rTt5Ef2igGJdSCb{@(|9FM19$ zJI|u(GSy|(fgUg1nag60sTK*|;1CU#m!NS50fWi-_k6mkD zqYX4^?=+RwYPS@E;mbah@3V=MuxG_4vDVNCv;hLdUWc9h@%1Z~vWoA6@r19)c%%Z@S`AO(sg(bQp+cki{k5is+?UY_Bsni zO8X%Tt2|M$y`?~g|Ay$i^%_kQ9F>&MKd}xIt^1TXm927fZ0b( zipysPIQ1v{TK*xgOGAErpT1~NuzuO`;7fLU(^UX6HX6~^nn=$DFMrm z;KV?)qVc-fEV~*E>-F}8E^FX)bRjm67Hu6j!_5*oPdiVs^pXg>fM*lexBtlM-*hOH zR&w{uHa|}>b=*T;9uhRui~8iurg@jKY|%>~{Z}CGYoG@WkxY2J8q&ie0uQX}AYURQ zG&GZIb<9{gc?l{>MZDd9$gjC^=35eBhLHo%6IUk$U))yS>tKxIqd<9a&v+q@)QBIi z)5f9^$~Gw;j~ZXnKv1E)__1ynwBR5C_paK(nmKS^7;w>i#U(KwP-G5-Qx=s;vUnkp z9A%`0opGON8SoK~TqV#eC1=DFQK=8cs7TL~TqH{4dI#`O$0MLg`NauI;El>;hVtmt zL1(a&aq#TDtfZpm-Oo6h&H}A8O0sw95LOttzGNeh{o^|$B@*_ww!d6dqk?m{ZDGNm zhu<^&h?_F4*0%+?GqBmeT4D^1NrM_DYFoKhl^}@#7P;HvjzukjjuPRYm^LFPjs4EC zN+d`{vR5$C8x;yEjZ|b{|3f!A_Qau z5Rj${?afaVJ_eyo74d^2z+B z4S&Dxs^#*ygC1rFr>o17inTcYmY17IuPiZbCmnZYn9ZOp2=`Zyg0PH|2KNA%-nx7h92@FG~>^2DK(D(K{vi76O10j992BN;GJ0Z3~|)QZ>_f$~d7h`vOQ1 zXJ8&_it&IcR-NK_m2{LiHbEJ%60QRYM#27?EC7R}AcjE{DFUuGh5^T?(?OvOEg6Ia zxxt_x5Ai4=0NLU$Y4Bo4rl)+qG_T@E;CALfU@M)vUM*BCOB6Bb8y>IlVPP3{uVX>D zopehr28KfI(HMxJY3!Zv60JsD!c?(T!D(k3Z5XdvRVKtoT~C_ghvu&3=1>rLofdc) z5=LjT;Zp^NmW*@l97*KcwzP1!>n0nEZTBYT zE*ABUI;GNZ9L9iHWhVpJuThwQS3lUvYaWh^N~4(qW~P!$M@r(X5e28oDskQY{m3E| zHvw4IyVuEQ94>H#F4>lw6c!n-!P}ulatJmxB=)7G&smoI_p2!W*xV$j58M-N%mJ3I zUS)knRW;WkN|eK6`7=Jl{8Cv9Ly2sm_q(%%F7iCfC_1wbtEkX{qOC=T6UkutMf6CE z#u^UuY9t&V5y-$EQY2bDK#$N5SzH;P5c%5y@!>lt7y}=UON>fa$VyL_#|RO2W@;xeQ?# zUr+>hF|5o17x~t*5(aJo|D=F0mXR9IgOqhQ%iCis(3LGz@fnhn9Zd~2>psCl2*~4) zg-1uMQP&7g7Ap56UQ+ak3<@JIm}F9zu}8SU!?cIOPa zUhHF!p1PMM1B47Rk`CR+ta0oi0CClVQ|S;$eUf3dq$Mzm%A~7koN0Yz#&P2=w8^1|UAj_hA?0;Yxj*Zbz^p2r?S_w@esD zI5Q8}CfH#LLYL&yy5N38U|znmtp>x`(#_n^UzqBEdiU`BDP}BG&s!A4F?HAg&=dYS z0}1Ych<8jN1tLl|<~IG8nL%a;h)9r#Y<4QvC67}wQnj|OEQTV)I$16}@5`nzW4Mx% zx69Dy1`^JHV73b^er5&s&C47YBoG(MceFaehX$!1Q@2Q=K?M+i9oc}OIY@05G8r%O ztlB*wh{oP|ick@2|&9L1EbYi786XOf3EG$mmz%PYA4Dvh8ZfkXQ|U)47JML+ZRlz?#VrR`(~6veGg z$VWVz5nBikj*2hQTeu0RCIBbwzZ5b(3_gDm@aYo61F26*1>VonRLUaWNROESQk{c$ z_*35_Ft^>Ih#?8FYL->(*K9-|yV4(;{a=(H(p*0KQbc}w5w#@~{Rx{zUJ`9=lsHMX z9uG~QH9|WU5}QSC5sDxr9y1$G`DMQN&^82kU4fi#8yzdT27o$LQ(!$*M|2Y1R^lG; zE)F0B3GGXVhKDbL#z5|-5~=|)NT5k@8DsS>(AQmJ144rmi^<$zpn%cC7NQ@$hDv+{yx~YH zc>|26w5ggCTMV2V2C-eVl64NpjK*>#}n`0Zqh^$rm6Y`v?3)Ca0;Rh(`1@=+E zfNG3V7@p}P7>wuwohQBu1@g`$gy+FhIzZY)oX{FV)T~cOtL~pyqJj^M>QT^gfXS;M zS(PUhGuo)=daZ|ibamcm5uD&N1h!%wF=&}rI1Pjgnrw2Lvz??A0&AM*85P9L_b?2! zVJDXvB>#;r3V5=V40I4*u}Qyv_uvu>1UdZglEM&f{_F!9gu$Q|<|jT)^SE7u^5brx z3S$(G&VDgWg#q;G33e9p)=yvpWG#FjVkEg@VfO?kx`$B_O0 zJNqom6~yq>SQKYK+fE2dL?6nRf=p+Mj^Ta$d!M%0x9~Uo;JWFgC{N(PV60R46D!6* zEE8l8kPH}XC6kHT_WUH+1357qqwSW1f?xgJ`=3mpka+?JdhV;XuUQiZMB=0#1P2wD za0_e*I%`1&!N|{M;tfDGuX5sGRf3U-^00h599AQm8e*srkOKZAQbqpKY#m=m?Bq~acvp*b zt`4tXaACw?rr6Wd1;blqlTK&_(F!R*{#c;vSOB+Rg}sWJ*j+gP0s{!7jeV08EBll; z$K6(qFuh~5g$q9G@HjPmU8#xcP|)Ui$<}5umb;x#r^2NOy%-%b5XSl6!yc(Jq>m-vdKUG^-9+*GT&oMbPQ+7v(b7 z3Z@CBsD$6Tk25P;jxI}pnD-}QFgAiQ`(9Z>#Qg%EKA)(TWk-r>75W_dxf@v5iFocfin5ow8U8{#; zL=kSw%8=k(nXYq!e;+}NrYt(eoyuoXSe!!jd{p7o^5jxrhs@d-_ge%(BwSQ^&gB~f zQkYk%H8vxPCxNg!P(h{~15Rp(66bV;xC9RKaxK9F=8&Uu#im5ox>se17eg?x6AD^piQ@t+QUX42Np`s042e@}Q?+a1 zoz=D7<3nIzd1i$uc_DZ(-$HC3R<4ITI8dtuEtZ&s3>|F12WtO-S}`d-B7&Z3E~LW5 zTgqTjjy7yN5WV~XbnO#zO2Y5KEm|(q;=h-4N=a}qybpInV@bTKHjgAo|Cgy43AD$^ z&)$^)<3NUW~~eBqi;)rGQ}OmJnFl z#{pe~kxo%6KruL&@zRf(v_v)1nJr_2l~H6xX`l^)Mv`4h04FdJ8W%H;yWa93G#eDJ zqJ@?uKnxmH^9LQ1F)CZP0I_@lQJKU64 zyLy_E2*^uac1mQ(`p!T!Ro5c6?`AV4B!q-_jwyFwjkuJj0Q`Tbm_-L_jI&^6PFAQpsYcr-Vp94!JV6c$86Bxxy7#zmDB$deN%pQ zxe~-rwv~tCBs@&Mo95aOPN~sh?wEwQsGm>4PhDcur?@k%#rA4RdTcw2Mh$84NK*`x z&1KY_2*g7-eeejxLH&+GZqhL9y`Iwk+(3+yNDOio2u?0m%qyaht>h(}Qr=-G9Re_D z`Ag9R{I+f3;G|R%R%T-hr)Ab?Bo#nd*rX4QM)a>IVeFpwd|h$*xY4lzKv{aA1o11?1ly zrh*TYxQ>8|+Q0xRWX*~acpL@Z3mCzLV4=0t^~5xj=PrsscZZP*mgkA!xR~}OW&;dP zSJPN-#F<2qXg2GV_(?ulj1Li*L5Rc$DYj7Ag=1|D`M9{824y<{+{e|iuK3u5=xiZo zU8P|om%R#phRIgiG_jVc0-roY!;1?nii91iO{c@H)vVI30SyYn#d&CrbQrM4x(2<> z1hLo{e_MH#vijkx3)wc_7md^kVy6*4uiP{3%gjCUq{&R$M-B%8UTkS}OFd-!SZPb| zhX;7LOux}4k#H-U(}g^5C*<6CCl{(|>it!5K@wtGwXGF~?ooQUXH|UazHJlN%iVWH zf3-dB9DNiA!BCOwRfMfD5u3yIO9&X7XtWYW-@g1M=DK?XmhzGXl!$C4XZ?pq6Bl^7 zshFlK_O#+RdajBl-fO(gta2Cz;cl2#x&$q^#)r1T5pL{8_ z=5`eK77pe0FF{R8M;%3r1Cl*pcS*3VO=Fq>E?6-*+|GU&U#Doq1Oq-1bE-m=i)i{d ze4f$?KAhU}B!Na|V~90NI1)l(7T3tpxC|6CGK5UeWk7CsjEeZ#M)g9!w<7)Q5p*{P zK@h9{NCF7|8JGW{9FHyNp>E~tV>3*_8^{6QJLkwfVzKR-Y$v47F^7NCP^(KL zfvC}wJ|?GiD2PEJb-ncH*%knJWllyBBhrB}QlT~_g%%EG$KgGWlth{DbUy)lqd+X$ zeH-~T;5b}0$?wxs{oKiu$Sj1;k(r$uy^!`#bEJc1r?V-LDuY0xR<2Z_l|r}$?2>ei znp(7^kV6o%K1aD}Px_-ks~_PCJdTrX07#{feN*iR*L}r)x26a~PaCp@YkQNw> zS@Q!OY@qxoSh-sY2%YO6qS!od;63xzJ1RmQQn55_{Rc4-Y{eTFCfUJh9^)7t+RJ-KV7(DQJy&IS|c@3~Nu!6JdWm!3Q9dp2Z~= z(#j58VwGU=HjVQIb#b8tStcs_x}R>eBk^300#Hd{0CA2JDXa@zdj^FRG;6ToD0^T@&}9F7?HBRp19su+koEF!^XMr;h1G6LVj_ZcM`+?Csp zX>z~{Sea@J&8|8)3kuiiKuyM1L>{}gM;D{PytV% zVgRR^{MIt9==6gJ%z}dhGh5HmB?D^A#`Ieo{B|d8cm#+^ zN%L^63gK@n9cUCK-Z-%h zZ^0YjTC5P^n2E=S40q2JZ1`h58RJkb zqH8-ubXi683MNaDZQIG%g?#ksZCz}{XhLp9IzO$N8+RW5+A$r7K|Pat!Ht1PQn8xd z(sL6*9<#IBhicFJiaVEf+Vn!t($Wgdu8%+!h@+dSDyS2w29tG3;B=Q)^W`rywH;j= z8~44y1wFd*u?up7;;QO_)9^g;3@&IQdxTE@c#2K_-ZKoiMewQ_{KNiAHfZ2(y045a2{QT`py)No(w zxG+zkhgu2i3ZaC$i5uVI_iQ%#n3L~gaE!E0yx&Ct_6tf zxs;D-Xkt$Mw6rzqq;btDUl5Wk2rXc(Shu+39me*;&tFN&w1zh%Po0vr)G-mMiY3*mXYM*Sru&%jQZfX-&#c6XYq{)}sa`;NeKVU3TgCW2m~nLA~OY z{<$nBFA^~M!q^@oHCPxc&Rl4A7m3&u1RXK^eelH34@BA`Acz1ai4trbgZB!l98RUx zn!}-E9jwuK<}IXuB*~_GvRgH$Ef@L3yl8KlnLP;a1kEJKs0iqTuR$*vU( z@9@?IBHc^s9rmy>7Y8;sdEx&HnX$)bdjjblg3he+(&WToRto?C5hk11Cj#JK-HoS@ z6b+6PTLS_8qkj@ov)lzfe2!dQjCL>hoel(Vf(3@s@obk(`koJ9FXBPE0Hp=OG;9N% zc6c0w@$7ZVJ%u4^?2w_Ef#w_E`4jDC`@CaNXmaC0@tFB5VQ&5`m9ln zhwd#Uhn-ssT((C}=u8!2Lc@zR5m8zN07V&b+%`!rd4J4{+p|pe< z8;p%`?F|!yrmvRm)&Jp5C-`|MaXk@(=)ekOYE&;!jdM zPJ1p7a0&e2zl_lQ`5G=1Or9-Bq|B<9l<1nY550k1=E{u$%PZUslyWh~5Z^^l#4#cU zTT+Z?ejL9S4+Ef6c7vtCeAbB5oI;4UXq&4Vx`dXg<99T_8X@jJpf+imo6va$;y5Rb^6#)C0OC7}Sf2s9v+8*~r;LnTA~GCF2vxt1yz9H0V2 zF@&8VAyId&N&+R4Y%AI&EyXuIG;`E36Y>W+wLz-t7WSyc0RH>Skpx2y0H{8!#S%MA zi%*VJ)H2H1_DTrgBk)>%XdHJPGRAtecjZ@{JK?4c)WFp80+8fWpj3&CwJZ-5KC6q& zBMLK9Y!BWr77pay$(!-IJF`XX6_gBbPI+msL;wC`kbB9k2CC4JfvpD$-0Mb5+NXE=0thr{dCO$r$Dwn`4I|J9)!~ z@gjjnS$GkPXrU14`ge%?FMOuM%J>oY^DFXRIswoYaoX|Qp7M`@CJ6C^tyuuw$zEP^ zUK@BupQy{wZRx5;k8s^R^S7Ty1_sewzd_H!-bpplU)0g?&K^%_&LA|>_k_i!@Ko)2>b)+{)qjf0UoN0@dZJ@80R1gpQ4Ci2-FQ6xvJ**isD z{4|~brK8>_?E=?p34=DX`GS_NR>N$Q_&m=w1}+U{gADs1LnhRbHs{&r&uFk*!wI+s z{foudT2a_K)Jq+8c6^Wi4m2X=L#W`+O=xsN^fJ(Oynwig;279`_z6*9Z;)^V2?dX) z?by1q_5`9IWOO8%XsC@CqT+P=S(vO9b?OwpK4bK>rlk9p6#!q#=s$il5tb#?*Va_VSs)A`jm{$Q*>FOLZ49VU zK8+TIbpgh`hLMNJQccAeuGzWg?_yOb55r7jJTQ@J@R0eTLe3#BX~HDW>oa?i-}ej8 zgCAVNZR&$+Y!G_!WM49vE?ZBC`K2yKP_%xEQG2Bqz~n&36(Ul! z{WB+H7PKcXY(@D?NC78$ksX-`QXb30^9%@x*t6SiFfs|yPH`(2kq{!FQkwx#qZUL7 zz`X3=)%gnTx_LAUWOLfum2HfT~R zgEfpdvZs~tp#->st2sot#FG_17~Uj}kAm@L36T~8*%BTf%XR19jW2oAkvg`LE!Tv~9y1B+wi2+P!rS~>?>S}fZrr@aw#Jevc=0GMiO4+HPH*+1cV)!z&h zZAyWWo=5AWAxS^92O-n&?1L<uwrmSkjL*%T9qW?9hStDUPlY?}R; zTp56E??|z}Z)FQ;2Nj}sF#^kR!-NQ4JNP(wfa~JWv9k}iBNm3(8<7;+2Y%34>!hRq zC-gxm{y|c_>Wb2wm-`w`lLY@Px1gdG=H!A6$S1Y}J=cyJCE0iNJwf_L*`{;hp1tJm^TkY08f9%kzz|k(yO&WIw}U+mA=hO*_8T(!^tu* z)!ZteZ5`*r6t3>>q79VX(U5XYEk2nbk*Xv5J2@$RwZjEKri1Nrcj5Sv@S6GqX>#3Y3fzrg?XfpkiZ|#>Tsv3PL@GaAmZ=hg32Y}l3LBTxIP&z(6*Ek~D zx==L+!2IwQu!X=D$*Tl<{9r{1v%G)T%cxwi#*u{{M&Whd>=BZp!iR`*hG}al+C#R> zV5g9OiEjApkuyPa@BQd=@3dZ1RxoWKy$|a7OM>zdVEV`VSq3pxj6~<2Q z^pN80(q%0m9O56XP`rZjx7XouR~m>T6{?e^McqAuY-R*En3~%|XuHueV(sA}7;sc+ z2Q__DcvyM2oa)bR_pRJ0HU5~Zdt}&`kD-GegDT6ORoQXT+3QKFkId~Qp&~$OIU+%e zH3?#x_GfeEQVTTqT4N<9;1rJSq_(6|NXs7^lwXk;PUoB`;6C22ia`}-DLK-{6HCJ; z5N%OWTEn|jFl46~SD?k0Yq(Z7ESH z$YTB|0zB_&cOdYB6>XiIT%o z{6`5hPi^c^Z3zZ$3n^vqsAvi6^;*_643?Ca3rw*!j=Qsz7Ld)K(=7&p4@`EBGe*sq zbAv8^M|M!ylDI5cw`nAT$|-PxoC_A9vqL%{r?8=c#{@9{D%$djBaOR9*UJ8!E`LN)fyjyj?z>30$BSuct_8edw}fp_BJ9& zO?+t7Fs2prO$1mYX;hGek0rghtO`+sgX%NVr zdQj{_ju?cLN>5ah?wVZ~A;DWLV zkwy(wMmD3uzlOEw6vNyoL^uPSOiCC$DSRZ1#^owF=h@^idVW^0=aUzX(u)amN#q!c zJameU-$J{lfJq`EiHK(TQL>XauogfCK$4=g{GF9u{3LbAWk#C8XT+#S5ZC!ZzMI|# zC;DM_Ru_FycWRg2;DmOX*{RnDUBNQT|B^f6aZ`cV+3>dJ!BkR&vsW}d6EBTC_@<(i zAcI+{Uyy8L2{LzJ7uE(Lgux(YPa{_33X%fNI2%)HC!$^fl{NgsR$}G^*UqhjC-spr zZ2E4q^rMM2?J5rw`TyTwRzwBBd=gct%a&bB&R^-J5y659uiiux2BtH2#*)ZBawx$km-)hcKsw{-6&{+ z0)vZA@R8a9GB_c(d8BdsceA!>-vffT2*E00q|=|k5hR(cxW2)E6G68j!~fD59qI$> z$v}}Lr!y$R;bIb&>gXN_$Vkdr>v(?a%HXA<6tQ3)5iNo%Gn7E_j0Rv*82Zyr(hvuI z)ZkHT0qwvs-6q>=L^+?O?`ehk00oJ_Mf8C`)JmgV5t@|(qMD{JAJ)UxtEu*a zqMf40xNZgj?i^sof-)O*W^)PDLSR3%r~uk{pfu3waHBI6G7piz3jin&5}BO&vjHH@ zb_K8i?8yZ2lf7_{Q%oWAI^_pBu!!gS0BVe8VFQ8!dk0Am-b8+2_xOf3`b@+ID|)%B zO(N{y$PqI$&d?|Wq4~JDdv4k_)_n2VrS5buC97hNsa!hfs8S_+HRXW&u#Os+`>nRd zFk(6i9%Hf5;bPcAX=W7)5sVAC31wy^^aHZi8AMf)_L+8!qjz|$MBFpL^(ipPoo zgAhpf=E{&nItGmXYY`1H5-^brO~%@rw)Oo~c8-czO6*E;mo~}W-%HFY_-^2IpL(d_Tm-`x;I1RxmUn733>^XqTJZul)`Kqv(_&@g_;43ze8E z2d2A=n`OS?dSs@FnVIlEK;az**ExcUWjO`5X2U9Zl-HiqkOtA@lx4u48&o!V79m*r zEL|$Yxj1-KBtIh_3`h*S#3L^qPrC97CGtZXCM7fB>MA3I+k%CBef%+Hx$r#Um{^yN!i(#^CHN-#Y z01#sWO72evGPYvqI7og$`!ah*?`138&{L}|aKI%yHsdp2;`#=UnQ0w_$5UnaY|u&X zVF@VtVrz^d^Gv@(N6=90$6$QHRENe_*Y~tRd*b*2f^GoiJUT7m9KAWV@F*f;=OJ2}??1L<2bzZ105(a58BN3z&2jgKl1XC-0+*M?Z$0;mg zdF-mqM!f^^S~*bK!3WG(QGbU$x=e+YL_~kdt;Z;q-rDHNIZks-yaSIeCnn|EypMK| zncaXnycgho(4)sTF<>#rh~`c`NtErq@0M_J-V*q+=r?h>> zM3S@u^n|^$5E9X`I^#Y=Qc?c&P{#U@OYv#ZVmy;Q-+_OF+N56Lc#n}U@3_s<{%kyN zxj}@Gad(ab6KOk=2?r0k0#oE-{f7U7fuz#jk*RHb0LUGTfKrD00%?p zCwcH<)FeqKGE0y7!9BIIv{!ynVS!)3+xKxKc_tpac7fu#w z#v~1N*umDVPXsK$SrSei)|+ygK{Ce!P9ZdnpxM{rxO!1U**x@VRePk)()r9lzfDdd z@#-xIT-P1T8gq=b5kyXTgA7Ssl3@Rc>)T3Am00+^ToN_dur!qyPdC zKt8E9`Yixo`(Ed1YC-=GA)0cg5f{l|#ZD0dMkFNmpXBBRTS;CDsG}U+^Yq7BQ?Mcj zyXoL6K)nq#3X$)U9{lS5Dyu2mN!Nc3&7l*^q>ohAXr`}->>cXbEBNw39 z#V*>^KLpI4VgEXSZcPe})e2gIdNDZ;WhEE?zK}=7jiFO;00cFZL|8x9kce%_cRQ&> zG@XF$L#@`i1CRG#MmFpyi};k7AjJ5jo9SP7U3`IX3l5<(6owtz+LuWta2BfA^-g`M^*N?P7zM z>l8GRg6PClb5g;QqJ)e@O{fQ|I(!K<+`mvp6K)Q1viK8Bh{&>sQPaL1sQge!cBLe? zKpz1#r7aG`P|%9el+*UBQoJrF4MZq}G*+d6Sp)WWOb11YVXApvtER6p|a_?6ld{FM|GO`ctg#x5TI>F0}APj_y zObML>OmdlsV7%6<>cr`XDd?BBTypKdWg3Wjk7JUZBcrqnW$<4EOHAW2FkrD~CYGSh z_iW;G0B)XMNx}k`g9Q0cZ!-aTNpsbOPlHIGZ&X8?Qn=rKq?!2j=<|!T3#y=CReg>DI*!o@M8f_ci&O?tD#maiv!?Nnu zuZaJfKr&I6yj9&Gk2^uFSBGanjIY23qbVkdSAutiO-8rv_o4a97(K$d<3J_Mx=80K zigLT0YXJC;ycB2$!cX$)1T4s>D5>g#bv5MBG-`?rNS!n+=I5Swn=4PYAxcI!@UBA7U2$)vqF2TV?!WE8ooy2)Hu9Gii7V30 ze0!v()NhW2;FT+ zj*m3$#hXzPS`5JXr;vR zTa6?_`1+R4C+Avt(H&w3HGs$~ikux7hvqkMs|19DN?TdMnbdX?J%VWr2eD6oTb@~s z{QL*X%pVr>6b>1Skp^4(cNDrdjr;tKf@KsaQv@<>Ce9E96irUW-`w|in26paNmRDF zMxfAb4w1cnW3aqyE6TYp{oN&u;?+rTa!!!EKTT6jw!?M6N@M6R97OMd2DAr(+Biue zMT3BD#|nyQIH47iO$^u!NVP&>h|<7=j~>7gWT1mFD>68Mn)tbu_4?VK>r} z3ug-iRDT@lk>VJxzqjrkkWIh9k+6|t2c9*0qjX+q%S>bpyiA~&B~z5077-mw@u-RU zlW_QTIGaW^Pf;=2pKr|I-e*OvOnD(@TkZM)4QYTvs1qiqFD7Wp*}6sH)*BU}dtf(( z39uUS0K_jj(a*OvuZF(AqBh5L8M3r0dfHL5^3D z)u4+sv(-O0Dli!%MyulKM&wl<#WaR_XMuAzD1=y$xqD%nTF0h|ZD3|6Zc8S4_LkKw z0aT;X##3uu{8kByB`h}>v}C*(JOA;EWp9;!>)qWfJwy~uoDyc zM%#hqDu~=U!g}wEp)8bCl`$9)bFfVcA63wQKZ6an_#1)f2s7}A%EgL}YXnph2VS|5 zAM*q$y?!d~1l#-J=5=KuKCJ2yP`8r}7il?$iR#jV_~bT96y9S_(?l#W4#U^rBlV$H z(HU9z{H75p^NEj6wD#65JYVyzQdwWPT{sBhCco?j+~LiG``d%vcP`G%r6jW;NBoDq z<(?)JX+$H~B_mR&;Dgw#;Rp?O4i$=>bA6d^!YBiQ~WS7iA3~u`~Ao zK|sF0_jt0rCjjZ)zyxfnfUQ%Hi3ZzY!C*7R@h${S-gE;HmT0g6G834OT3F;RmFSkp zlK5{87^Ebb`t_1hwU)7H5I&b`;Qf%waR8dtm%a7WrI=k9ex$k3_Q?k}^SII&lT8E{ ztEu4GtQ|n#aRvjA?5d-E zxt;Tl*AOH~u+F*gsv#7EXfqQDIDfNBNi+gzq~DPMjh4oXCSD(JX_UAuZf@qhGLvF= zi;MHwpdXc#Xzdpev{%Q#XEmd>_3>ha&{&8$Gal-wrVfQhcJIOa`$5!$BLV7N)iVYx2AH760^t?YpEnLIL0RbY(uqbMX zi@6hM4l&qj=)}@@2Z_CI@#bPs0a;MA{hx;eXKH+g2{^K2jL3A03%vkN&_M2f^CLYkFnGWe;KiVdfIOG08)heok2;#3&i7@C%K zZQ)FKa=Cl3&g?2Dj6mVjRC-b~=aHt$g{Ul$zH99bRbszIGUjYz`9KyoyaU%ndy$)I z%;1&GYQcsVlSD!)uqzR%YiuYSA2!@tjBAC3fYD<#DPv8?deDFnnQ=X^GV$Fg*D;6JWEBJ=5fMF08~s8!jRL z?S2Ow2w>$y#+L98wGo&57-D!T?Y$iN&zY}?XyUuRRUK<#mD;LRQ#DZSoX#tE)1X#V$&D0!o3S1v>9ca+er~)^?3_c z-7)$v$8v_S5GV?k0Ajtueu}g2RU|8%$4gPd-OkF2`}IZ94zPeB9w>rs3kj2-`>P0L zUj~JtYzydd3Ut~vSm@0ulR;urVbj!Rmkg{PD(W!l*&OzCWqfdJz2b>D!pHcRnuCRaBG&cnL|$w~ zNUeclUIiC&Fi~9FYhUY(zR3?CZS9?fn`(DauK4Z5e)ih=*f;`#SOF&pV|Q)-$q62A zl41di7RN*ZGY?_Wn{bYa5dnBO295@V%pJs~mQc&O9S4IL>)<1zoURRoMz6R-BajAg z*4p5o;5m1}&ZfV=?FdFg@Mp5FbT|mLg2W~4NT!2&XXqF+K*I8M#t#Wh@G>o?2~ISc zV3yjclZ2l8Efa`0%&y?)QZ0oe$uG9EI5iMH)PK{{8{5MflgXwkEPu^898;IjkC+s= zf5}1FEml*42$ z<2+f7ko!3-S@4;lKuQQjRl*6QP5f-&#Y{XqfqKcJ4=0{?kCNd*!Tt10UX)`BNa%za z2zhu0knMPbCmxXUO!*5`cJAi;1fk(>57`%iCkH!nh) zrsZHA2|y!twijw$_d5Ve6Sn;08EII&63HMdp##V~4-(Ku&i)w*Q7$;C`MwSrO(4CP zl7$B}iEliPZh6_}O7x{H5$O1S17@Io1s>2Xsd@>|bMxs)O9`iKAJD@);PSwpM!12F>9M00!*xj7l zsZxDC-=M-wfyf%DZa^|vNpmRsSnSWtw*pU%IMu<0(%7NX2Pai=m|>)Zo&9m@wgcvv zq1_pxPKecPy$SgT32KJ8oM{3%13wrRW4B4KQys3<2!4@36G&tNUnc5I1t>WgKxtKZ zbiXn41Lq$=JwPXp)^!&%G%pjw)RZQdn!fp#*A|XdfOSWeLGj{8&H=%>7#R?nqnAJg zdTAQwMF0r2QL^=N0F{FGV40d?&0E7@R*DwKGSezic|7M6@!EG`*D!<5Av zh1IoczWf+H`M)6-&p^8vs4y!ukx&l0)0 zYpt$76N zSoL@KgfikWpNd50pm#y0bH>8)O#%8WwR(M<8u+)F-g-i-)qgZaV8WHND0bSTovDwY zexZZsB9|4O3*Z5&z}H*Z3Qra6$G9D0n>MLcIc2DLRHD3yP2c8j;7&Q>zQO z9L~apakGV8RgpYXHBsUlYy1}A1+8mFMk88~q-IrI_re>=AG7JTBk~SP9IS{yS*?5p zFk(Oppst`L(k0M<(>RHM!E3%w8v?kxyC+H51UbxXMY^eUmZ3?6<7^;nI;Z-*7LSg; zTReuGe|M`;?8E^p_LV%=y}E+SXU%0Iy=%7KWO;9Iyaq+3nAanaT?7q{&VddTDFA{6 zVTfp&7$dlYaTKtG{f8i*Y!tL^dMdu>S2^k>L%Yp-Y3{?_+MzMt0~Dku(C3rLMOdQC z@kgYJ_3t790g3lBgAqANv&y)t*$5Hpak(va|}!Wo-1$? z)=tvmAOuf0e(@h^PU_ZPfFoojzkhL=UD2Jq&zu0ixRD7cgZbh`8o?|EsfGq5DcaU# z)jwQM3dmHu*kmxATzeStL2-4bkp%`@XvVS=i-Mr7LN(VkT_R; zC5W&bg_z|4fEwvK9hOKtLfY<+cF(^R-N`B4jvsQkZ%B%jjs#Hr6_f6KQVW~XvNYPi zrNfpKh2x^yT9rzu#y1%k@aDC$W9>r|j2(pPssNP-e#@nTP;t7uU%B}*DnCZO+Khm8 z{S`Os7OjJ1aQJNf5I){V^3pCr-3j49V&XDOK^D?nV1}O!H?VVy&LmX_1TBM5$0v$S{;b~i4StUS0Vr&A0qbRs%f7}Xh*LQe zPOt(JdI^+$b@9i5;}9XMG#49#ZZ&5Xp;cM2PQoRvt#0`s%?fUK6b@#{u}i}-eYwl` zVg>8yXwQlbs_k4TbcB)aQP2tDiOP;^GV(Ti$&8>1-6L{ z`z)S|bmkU5#J+unFaH2jf+aE}`4O@l5Jc+LpypL1{;DacRJ_cI`$HT=-;|6P?fc@b zVdD)L!+~MH=63x3KWxhYssOB3Uk6X?xojs$Ku5xNt?0xIHw5^`$l=$(cF6YmdM z@ss>$&7x!cIrW~A0A|=>J{>a{DuOE%+ol?t)k{B1WDhc%mchql@aPJVeHqU0>6S6i zVaJ{z796IJ4CIwMdTe?-Q8#2y`SVlwc+IH^#mL%XmrbGvLC?M{H)BWQo*V9~8H_V0 z1~=lwlcRVvtl6#|1Z&baMokvAqguOhb435!dsR`K+DJx6mvdCn8 zjd1YsywzdL`eX(jInJGUBCH~jL@33O;#k(RS?c18#X0A3uO-D&A)8#f*prykOolB% z8n54~pVtKtWAIBN(yUMTsYt>hz6 zrUlm6!JOj7mxe$NkSvoWxlwp7Gl$$>w}|3rmShO`-WN;s2#ksZJmQrKk7DK&@YYzB^6JO^`(49l6aHXL20I+6~YIwxXu9OJ38b+Nn5TVAsP*BdG(TOl~ zV%{)9Bv~dP3^e+S4CMl)9cg3989cwUO7`H*Z-Ppla@of) zSZS})u-!S-?4m507#))q7}WUPL_17sFv!BDhe;_|Hu6PphAi>P_K71%(FS1+;pT~w zvjynf2VilLP{W7tT#`~liu51njPxJ<-5yY)%xK>T$cFLS^Y<1?46U;oJ4Q!0(!)0W z>=s!&A{^FHl_8E)<7(r+X65B8Dh71*0h>J;dQ&FYRW(bkNeFbAN>9mf#2{nX~6@fq<*~ z^Hmc;0}Rt26kT(wCZ^_xS}m$GRZKp|z)2|AbneRCOUhal=?e>3sj7cgrBF#iMd^=Z zm2ALZ85D~R4obeVx*oeu6+d%QuqDvs z=JM(?MW-hS2g(1RDX!5OlQP$yZHS-!#2M;&xaY-#WX6XQKeXiv9iCqb#-XSb6FB65 z+^L}O?`5*K(McNSP0rIKVE|%M7J#)%7gbZ@)PQLZ zUmJ5ipdlxff&~N&ZP7qUY=|s-&`OdH*Ks2gTK2=Ut=l>uIk=(Wi@sdK2qV1*a0U%w zwS#}YoG8&Cj&f*MZyYL$Db*Mwnc11Nd(}5W|0v0)FK67MZxKyJWk1_mn*6^qp}EBSf2_Yi?tmetC3tkn`}H4 z0~xbRcDd~Eme#}lnXe##d_u1584|(dz?70)19#wp^N-&G(s@j%>=dH7()!!j99x?l zg}5?=PT(ld4CI+(kHz*_q_|XIyziN%ddl}Rfhmq~Qk8kz2ZoUIx{|}{5V2u=PxV1a zxdkq$iKJU*@3-FLFi!jp3sd`m3>$+I!Dt7q03);Jc3>IKV?3U$TO54pXLIH=N2!a# zCPVLO0s|ia$BKTeg+1&esR7XPcZ5m!Mw{}{#&8#dx-HKsyP2`*BsZu~0!qgwA_fia zl+rl?#;`hFsr;eB^S}iF$S;_|l+KUs!KZJ%u36fag>lFOSDL_dIKafrs_z(XVPGL1 zY{V8iO2RGx6Y)4MyoQ11%RXT$FG z516DUaad~+n_&zycj2IQV5K2Eblw%STu)6^k)<3}@A3U4K@mBm9xJiG#Mwpf(E;zm zF)v<aE4)eNVAU&C>!$r_R+p3y>^Nep|@&nX0fl6 zl)y5E!(C_Q`cckjaX+H=>|>Mqw4eEQ2K$ji5rYX(tmQiN{h#W51DA@aqlN?1X{5w&~Y)3Qb{rj~v>LxPvr=DsP;_R{My zR2ERnv=MT+TowI^>#W3JxG8iHUSTmo1WUDEA)Eu)iAg;ofhK$rq~h_o%BZaY%V+}( z4-m3N$Omb}0w{f5=oq7`shNT;}r%KPz6$^f(+9(q3KcrcjK_>kd_#~Xxezy?8+rhj0XuiJ7j0R+BTU7 z%`rr)h2$eAW4$8PSfZg-b#FVxNo5w7{MJeOhL$2wjpFW;ih&nm)7=6>gBUFD^M;`IbHyf?DPsed`+}UD3{~k zP{X_i4`+MZeE3WXc{uaJwv?-tMZ)w+Vy+w%=Ui0Z z`6)Sxv7doG*Jv->zDao&URHf1fbmNvYI)w}m&Rxqe-jw<{~!Wn;u^WCp6cY74SviTSD(nV= zO!A9XYaTaMecQN}@>O9&Zm<};U-|lXh+yEID?SRvObF4Vcf;_01hXhaTNG(KS2NI; zOL6kI$APNqPo|a1^aG(W1xy@HAf7=P^I=~_8eY;>@kY8C|Hs>+FJ8>0A76ApAJ0vPoJr9S;UW{M>7-@+liwT?^r$n4)w2d=4sUr%kYNE2|Zu;Z#skY;{Tk zKOj+s^%Kdd!L3Kl#=O0Moj)l(Bb814O-0v zF-VJxQNnOuVF_-Ju)#pKduf}Ba0l1P80s@pUZH5eV0490lw!9sY&uDPHw`PpLoYSe z5LZ{Jx1~hBWbK-Ty&_eSjJdSaA8%1HlriRBEt1q1%6z#vg51}-7syqrdnu#X1Si&- z3HHQ>W}rJG<$y$H%4oYjCK~~GHaWcjE|3L7P|eCkFaSZ31KAM$nT{(R*@7Sml&Fup zGhBSuwtK8500>RhCLnw5&~b ziskSrMF%Tk58bx|f=C_=CgJRuAvZWvk#w~+eiI?!0ZKK5GiNGPiHIT&`B6#%YYGj6 zDLMqZ^`8c&Cf4va)0S;R0nlr9JL(hn60c9sg{Pq-O;~dTB(p;Mj>R)LNffA5OzT5Q$!`L3+G|ELcCcb#pvywG5LZ?^#iWeN$3x03f@Th``CSorK zWV~$bZ{nfHkSt7N)CV}v#gc(s;h%Xdox^*(?M+fBA;d^U!I|TOeAZ!$@?`815&k#Z z1{@jolc&7gWsqqRrs+SmA5qUd1LKLkk0j+(RX(=WXZZX(9^XvaVU-e`?v`;mIbieB zB+M%-1mcOV7Pf`-4KJnVNtWvHPFgd$nUhee*Iu^bKokZ?l_sneNM4@P=in!uyN zmL~c+0Huw)MTMd88K}fFzztpESdM0vc+;R^4vvWG*`!O&V@HO`8D?Zsr^pLpbaQcgv}%OOs9qzn1@ z@UIP_M*f(>1^bfLoET3=rKgPG3k|J-87wcCQ^}8a3a?v1Bd?>LPB+(U&zauw0L%^4 zsh7s>U1DQ6__O1Dt*S;rkC7;5HzM3*f%~;8m|N)oFn8PK(WF7++sEgbh6iL^_{Rq2p8@426Lkf0#2ivN%DWC~fViR_TQrJT z(i|i((4g$cw3Tg(o6&=uhJcaVi?*91rA3me_5?#fbAnWe5!%ZPUeM4Cr)nx=uV++d|4D1B|E%>-mBSs@WX&`OC$wE!2sYa)|E*ddW!8nGu@AUjU7?uPANzm!Yz?F%bw?^${nbb*m|8r8 z5EVsUwzGLg5iJ8@HVr21b(}S7NM-{h17A=YV%DtQWSnSUHG?j>OlhRjuOzP&X&#MR zq_tCii`2kqFS}3ICPDk~zxOM8nplKm;suOzMC;AF!v!vj zQ3y+1ev5bbN*fFYS(H+tiDRMt(&#p8T9i|7q^lSAFL2lXJjzj<_ax92vPr>2s!BBL zTHJjr@L|S{9{A~P7*19hGNRKZP;R3xLd5tP0!sgYtH68IojR1V5zfvfpQK05srm*| zd}wVoaRar^Hn5?Y7N}S1FC)Nybq+1a0bl_&3tPyPIlB1vhycLKKt%^>SZ1g_iDbQm zr8$luQXZ@(ejYU7UFW0!0skzKTr9zXpAHa-gU&fY6>Gc6iz1c&ncn*Q7Y4Y5dt_!_ z8O5*(0zfWPZ1S8xU{UL4gFV!rBa46m>*QS{Wq@)|2WS}5hnBhSmAgUsb~eK23>P=3bTLDXr+`Ai?RpM}#0x$cBO92)O*Htt@$o)wn!xnzNK$@N6CRvzO zr8qCejETMDO3qb5h`eW^2$`LB8}cvcpY zpwN50h9#7IfY|LfjF68Y7<2NFe2|%{3}>iof?&ZsKwL;7o)AbdJxh;Qn2~ghNb!7vfyyM78^EH(ni~&Ao3ko2i$VgzmX4~dFWE8^4+YoLR7ziGU6vZqZgom-@9f}%c zEE|w69tR)Oc9H@pAp@q7daQhQYFl-zjL>b_jGOF=$4^F-d~?hpTo15%1CLR_;83?W zvkw&S?XH&Lg%RXJBb2yRbucmxuilv?Uo9+ZU%dbtArmT&>}Az3Q$w{N1~h%m7M5}$ z8vk$EZn)>|?jc!+oGX8%BmYD1iUewC09!C9gaGx3K_0#M23VzMfOxqa`sy zw9~jIUv}1D04voFVxo5sDqM8r5f=~>b^cJlNN3CoM+C^M^2$wfVOs>=Gi z!GNf+V|%v{o6GWp^%O3Lg34ykXcUiHaV96Iu{`QggQr6xa~};R!To>O37E40Z6uyO za1p5)a>P1~2Vh82ACGXXw27 zv>F!Z8M-bX4GX7`mj#qasTNrkc)xPVFD|aMLkAsAhZGQ!y>1pnlA!E6q!e9VoEuqY=t#R z6QV<)0~OK$xuF7)F0hW6CG8T@R$Y8t)R7hHPmg@U5Wxm+KX5ianZ2=;N!1vN>bmI8 zWvjP2jRb>HLX;JKOtC)kWG94kAP9C=cE+);tpz)2uYVDLb&m|&Ilx}%Qmo_xJAWv6 zI0EM7z8r&&bm1hIxN*>;ky{fofZPD8;H>6bJZT%{-5XqEey~@}Yc+e5t5*TIlzu{Ihzvo_(qgd%f9p#M8$r{V3HFvl3aO{HdZFUzjCy zwL*+2A(WIPX=LI};Nq-~s8RvCHxeUPj1CszVEP}Z5S+gTQ(PBQ<{8^V#p$d|esT*- zi4&yQ>rIW(Y7y!wZ^?<*-u^QtI&}4Q!^(ea|TK{(Gnocwqq}rhW5NW}d__ zFP(>}RnL+4JfQj1_=Tlg#B;0UXnUAhC^@~z##O9=v=T?g zzdgsievjHz@Ja76qpWz5Mqk~H_k@KWEc(`NKGx(7g@Q$m2A zLd4F=pnagm^#~JU7~fOt{XgqRC;_{-$Azi%I-8WM*FCYo)zZD&KnqUDu^58|*)r3y zE3d173^)^NeC_K2XkU{G2S;4+hy;TN0$Q47-LS2HrS6sI;pZ=OxJaSsmp#yHfF?DW z67lOFQroasZbLD_>j51y!!ZMZ&2X=RmZGVk!AbQoP=%k{@L@Jx4Xw2sT(5!4q6Sz* zqYX=B%}KbD<$|I#pfxEkT&}&Lq0?rL;vL>`#&%Z?T5RZ&&(w}=Sch}$ zAsMB;9Rk5C2pHp(-S7QKKz(H2yr6JrN1d(6r~OMd^qmwSPl!FVJV$B50pS+jRfZTR ztD7O(Q6ftkMDn2i1bp+*Wg1Lk%tgYyX}7Hd<%5`7Vw1Jp6p_AI4q!J&lsB;;uvW*W zys=tNwyo)huRtPKXLU%Sj;38nb(DyRtfa(qTvSYz9)iQlIh&(zWF9^euf~qFIV1A0 z3XK~!cgp?ID^qg=G3ZE8vN;*#Cek^seb~Xe+$=^zXv!edeDiu6Berew=L3UhWC+iH zB!b&K4N5mn-xPwRlYz?lC*2(|;FWi@;?n82p(6D)4G(0T&6xZXM`g{;y!Fn#52Mjq zAX-qR`Wg^325(?d0-O$hhQi$3VfHdjF~%iH-GuNH6m=qyAFT+#W$>Jd_L>Y%RUvlq z<6H?WcWc!?J2A=wEJOcATfq?QLKj9Lk8sMAfXtCf1I)5X%P!NX5~dtA(Xe!&Ib{LM z13*hT;to9ns0e62Q>jNv77zEgS2@rtE6|*Zb=BkOOBJE27q_(8o1IjH9)e%83pbGj z!X#LM^a0=wRG7S;1rDdNPE~LOz)PR_dDb8Snlt-fB5R-@Lnll{^nLu7YsiF?8K*HT zKcD>|cU;rI@n-kNTAePC1z%Mt9G4*Jj^6irRt(IxXfZqe!uLsw89W4H+}RaBp^qA3 zV@#wE6_QBF*qVy^GFcf8o4FMLofqHYzcF2cIjiqN#wTT&#dgEQMKYly8et3nqX(i` z3lwZ?Mr7980_2H9#-&8?pub`&N=_LzdjfU37tIGU+*Iu$v11zQy+g5(BhFen=x`tSQHDvJ<8U>bqgxialCK7|~VJpILHhdAh8SN4*h zRMp)0c8UgBbh&I&In-J zmd&Bcn=QWxh2bgfBPMIw;a*~nxFizV(65DQM}WaC=olu-%xP6teSyH_SPIyu*Li~Q z1FZXEFXhD4EdjOWdxPx(b`OvQ%%yM_C*oNI%H0}7=aQuFxoa*&2e?rZJBj?3uw`9l8PHH zsFpiOFuRG)SSPOi)z$>*e~ZwL-2wp2bq`zag%(93abmcG*7=O7iUN@#2^KIjN*js` zgZ3`qodI5G0!~;Gc<_8PVJ>D0Kjw>Z%0kx%fFtAtwY8c-UY<5n#X>t{4!xdib^A^tU1R0)c4;D5{dFWYDCB0SbIHWE(k&_Oz5v zxNS2k)l3<}$`>$}!3bR9m%LKAIWIr)eGV){HNWp1wD*Uy*<6-~N)69t@SP{*bgJ8= zE+zv&F?=UT1Uv;KEPWFfA}2CUOGF`YOR!7y1(oi4G2!QUM_vHz)dfQv8gpFZ!?sFj zJ}YS)foYh?rtSdbG#E0XBby|#CAv!ERgZvP9eaXFP~CpY5tdJOu{CKM+=n~;f}FVF zHBipugd&5mxzy6kcp`2l(w#lI;GxzR5vwAYTY>D7hg>P!IQ=jHdlm|c4hNS3`#ARS zI7?!Lz7QS&jN0nhq?*Zn4`S%rP^^gagXRIQe1c|go}z77i2{}Fz&@i=DHl|(21E&p znlRCxaD`tmdOQ+Rii%Uz}Ab~k^!~mo5*vM zzYb^@+_uhuUVwm>O$V(7v+R$tX$+k3H5jy1$Jws_ZEqCDgQa^NVYC2K7s zdNi7I<`JzeQj`LJdj3xu2741=9B&L8dlGa-I2u-z&UhZNI)iPNjsY&c)sXDtydsY5 zZOF=^egZ2>80tmr%q*147s&UPC)3Y6AZxO$ScpXoRlk{C-1$Wn;OL@7p@O}5a}%-< zBB3Q6YN(7#1;&P0D>6LG&|Zfm#$1}h#(?(f*gI}MEb6HMc3J`1btP5W=DcG8*#afR zEY}C;IbBEpdVv|MRS^2mpNeTf^c;O-)+_<8(r`Cp!2-Wi%y3PqV-${9wC~h8y99d9oqsR%URDyZU@X*5PZ(qQikq#*RD7ubM7XgD! z1-FsLv8|s8^VIV7MLh}Wz+Rr;Stg#@e={XPAd(fUtH;syB3>)<_3!?NZm&RdRJAD~ zgt@?FST@JaAp1zERInK}0)PPEPwX!rZKC0W&I2|rP|z5u3NOQbgoCtni@wN8HB7o| zFd6kQ^}<#-VmL~krmij{Siw=@h5YC_VZcpZVc{YCHlL+rL5?lIz@MXuI~R2NKF68) zjvUoFGU*Sv+#F0e_M_gq*P1r5}?7DK0H59GC9BXF~0 zuEu}Tc!x=N4et~zMB<`*>E;+`cTdlIHInU4UTQKJuGe)Ih01H8@E%FzF7nCUXR=UF zs5LA&_7fh)*H6AMy394hh!ToXsSqm)Qw@SDZGTsuvg6(r*lDN7s#x*h9qI@iccP^O|E*Aeo8b84xwA8J~NOK3>pec(7mPE)kydix2DWW*E zcKo33a`w3(>?dbDvh!dJD@@8tdXp;%Ps3eHWBxv7>qa+SuzI}cE43eY070Uq zhWQsu1gFC1)**)%$5!=556Q$Utbv>!Kf1kH>dFRQD3cdzzw6oT)E~(K!nupfUn^z< zL-F%ACoZYfkDJjOo8%0;8q4hmdk~H&rEtlRQx!WKe?>Tm#pIM`21;t2k$rqtj#JY|6k?)W_oOsX?Z9wt zGg%&s$=rP$BF;eD(iw)4?vErXrLUF-`Kt5K80OE8L3ti9PmZ#H z5S!y~kd^JDx&Zowb*x~02KGerfC*HhOL=Ri=!l-XQKX~#n8OL_!b!zLSqO@D&|@4W z{(c(6w=S;o^lwMw~+5=lUu3=s*bX6eMtJ-&uu@`Ix!N!szj`hZ1LD zLG=6_R~1c4`N^_;DX0X>))Q_fDB(zxT4V}O;zhcN>7x*A z!w)vLg8!nV8{^Iq=ADV;-G9F^C+xgpK?P^PGXP1N;pD(b0J01`UIvO-r!>cV!twJJ zu9miebb782&{L2oK*vXy#HJgP8NjTWQ&2WyJFLr>KQ&4DK-~&Am7P#iI41m&X*wEo z7xV1zUWh5Twt-=BUHDNVsAI#@lM@~!t#~5k;eBE2=yV=V6@RTnYJ6z&BV}QFMv3yo zo7}E1YZDaC)|P=u9O|poOnSJ@Wf$TFKTi#*juC!cUl}5T9|^bU7LuPU;EE$8+m}L+ zZxQ=WEj2lV#k(d^3575isq0GFgY}M;EjHbMQapg=R_$_*MMG({M_j6F#?PbT*qVKl zka=<6R)BOm2!F|~7?;ZcFIJ@gEeeGW1zxH+hiZ%QiM#7^su88OU}r2C#+xH5y< zR%^q`T3A`i0Y;@+p??~r1NamHlnZ@|ymU0V-8bVh)2q9au3X%jCw zzyT2hd;_(1AhRlNJh$7skDL*YEw%;dyubyRs`YIOU38jyCqR=G z8V=G6SaLztWJ-0sX4|CYgA%qtMwoG6$^{T)BMjk<5-{~S(9-Laj2xbjPtroHMeyKn zkyUPT%yk?X$2jrbo;#Cb06DyzAfLG2ak#I@v98Y4hM+t#(}PLP<{!p`h0?b-2wRxPcjk{h1-aX>7xUp5BX9n7H+ONInNqA zgX74B$G)DKv6oy*kVyq6x=Ew!0QG0+M=sF&Ji6BKUu4qj}3@-YG}l*1|5QrvqbE-w!J2$;8r+m3h87^Qx822FZf?#WW)fD|Vp_z$R?g!KAXUNIHf3^!Ds>#(K)pQ8=!L8u@)^(^ zN?G9KPCzPA`%M2}#g>wTA)O;ji8?1hD=eC%VzLQ~9#xcw-N+-X*-MXnq$Hex!kKt} z#inU3&hwK-?9Z|R0!(a8+}1q+kWR|H^O&AL65RqsKsHU_bq4H2$ z3NFC-9_e#iqh`)?PDS<&Cy)e&(Dl~!#;k0P(DL8}=^IFK9%GR7A)#coCB^(%PVRME zno&?3rlz@G5Enu}F0$x^&WfGso33;X$W*EaxLMm0wN6(p_{(BX-=gQ`nbyX+I7KVy z+`=;Do!o%ZsrSlBn# zpd5}qOt6G^=SQVrigrNso>Sm9!>d370tvG!kiJ1XrV$(%9&p{Zt6h>ZSXff)V-A1a**04RpU80n9}^s9u~(xK3!QpqS0I zwcMSv14|^0cRh|l!H818lrz^f#nSTb)P4=7l|cq4M@pD|okNCp@wZaETCNpbjJeE< z@(V3D`yY3g!1S;F+Nds2bU_B4Y()h`!!M=29Z?x64w!drlObey0{rr?3XadLR3 z8tWuzFv)9~T_YnIGLcFxMGi5YKiH-+ zCQxP^qgJR=lVOKV)U|HSBBx^6FhF!sKv1+XlPj~byzS0SHUe~uISyX^C~#|%vK^Fa zkdi;VH+7!{t~!gJVadG23+!;DOc+01#!*dUG@!pE)2!p%f z0jbTig@`P##wW6?k5r@ZJtlcbAm>Z!}=!o57Kc-X~XB7_mcyV#I(C zSoj9m-53-A9j${NH%!u#m0-r$W}yA`)l|Rontjlj=EdnDdBhqf(J6$ttkmee z*>NG~hzBAY#-=RN;tdi86*9LH{@8>4G1Cml=0oFCKsr`P0W~e;M?Xk5niJLYoi`Pi zJ6O)NfRk}i;y5_OWGj^;h!D&l2XIrY!Z9luwCK*!+3)5n#Saz5nYznx-G`{yrE%6% zp^n4@y(;nTf}7<>v-Z+7P6ha(KNof}^+#8q+&yRgA=)!A;XsIWB-uqM5p)pVc2fX8H=ME68ag`O?zY7P>Ono=a~?12E?nfhiqk$hQX+ z4X8#$d0Zp!?@-+q2mn*6K_Helkf3P?ijvO^?=7p(g=1xGB1V0Z&r}}AX!T0Yny5aL zmGDZ5(;XwBB@pN-N)6O^683v6RU(v7?sPNgtXH5(sadKiiYfMc!5R>S zC0fT6Td!`;pE($a{CH+ovd(Wxz9D^nJ`1(cV2_g*)MEJbl8^%pR-QnB;BXzx-jxhx^@A+lbug@zt zRuzSqR3}owEu3DNmJ4QF*#OLuNYbe3)u6Sy(W5r;tnou#(-Rq0;&+UM3N#kDF96u^ zIlH~Pq8alhcmH~Vu%d{SnqN#EXPQRDQb^iRut?IN@_!u(C@2YPT9FP48mK8vZAmeq5@wcbV@L}FkV$0j6jox#jGNcGPROfdqTV` z#|=mnw=p>$h@Tp8U4k0}@^nCoeZXc~-7yE@f2`()9w>?}5T;LsXeS3D&k+cTPY46GnB^NB zO)Gi{#^c?zFnpGnK_D6k5Jb6rNk*}Zs73HAmuVGqvH)e>Gcn5fz~)WADg|N5?qX9~ z3Oh__(jaL{*1`t%bX8Iwa~H-|Gz_>j7zJsolB_psphW`FKE^UdYM4}q&41u>Gm&O4 zEddz%cTD(LWH{ga94u7EH=yhWuq+N0sRq*+A>W~K-bDtPibU4pf5)-oSZqcQmFP@i0vce*KVj9m)jV~w z^m_<`17a@tV1d0sX;8$i#DQwOBx3c&Cd$(m8(@~6W-HXdOn1bTwD`P!Gd-RV91ang zoVI(5E5esYgIg7%*>6^L;UFK++c!4&i*XiF<%+C0oTctSa>Amcz%@cs9;&F2Cra;PGnn`bVJ3Bj7(Iz1Vlspo zcpQY!EYYsEFA^2{!?FxGYscu19XDU9fd#bc)NK(6 z-&xk|z_qo{@l{JVavVNt${|-uW(Gnk+F~az3wYBc^Nh1_xd1CHl(bK4T#yEN4)|?P zq_|d);N+xQzVFRjt>#?t1*M6N6G-y0%vdO(>sm6n@?Gl(wihdRX0(8{2`tM{qn+hE znbch3m? zAcO+?`?a!bF>*AtPgv49UtrXo!EA?;}_l#z-)f8KuT) z6k*dRgyomCDcf6#MadUfJK2&60A~>f#VDwSo-q<{nQ`x!5V{;n=R_~=B7j+Jk(2KV zNAP@ia%H_{g~qTc3te(lJc^xN1OW7||6Fi!lajC)~AMz0j7w{afF~z;A3m-tPSHFxn;p6qMOi9Wr@xF-W>Fz&a?kA!k zAzOY=uM!CW%M7^@gCzQhj1{l&<64qEz-&NoGCH3`gfm5a(^kW#AzTAw&g>aS{5n(C#%`1$MvzY~7@)KRU^OfP zVZO2CL132%Ml-eBEmng84!r|MwY)RxZ&A==Vt{C%@t1Zlj&Tn-s^o_iIPOLk*es45 zq2Tb=EgA_0T8=Cq3qd*quZ{Udv77rjYn;)hN|PdteHdg%pC6v-T(_}SVME{;JbfC} zWbzHTxx*P?Tn^eki~~vZcL7ss9_2kUxeuaHt2%rm@X;ipsa00{zYsZI9NBS??lyW^ zlD^(Nr*dpz!+zNZ`%+Yo0m`mw1<^X3!#nQQAtE0_fc)uo+CBQVDo!HAXF8Oc(`ysil_e(0)r`lG_O35}*sDWqb?5|E*O5Vq zcoLI}Og9-IKXW1vfi)P}^0@{Sn&zul-x-^OQz{a0HeSADQW|Rm^*s#g6B_@iMPe5; zpc1a#8glu}5R|yJvl;24gMZJH9rv>^#BO((7=LDZ4E`xhZmt6i;EG9M(&Wn<>8UnJ z`hB}%$Ze8_PMgPkpf}`SchXep{9vM7+%eY2|em?Af7*t2w_0=CA@9!JwIJ^kF z@a0O)Odu~=f(u7pM%HvV8RKjkY?SZvW(a@356uu}99MtXg(PTJJaz4~n@>t1p3-4V zr9rp6J;RY)dxa*}fv9d}>vzOjjg!!c7x0XM0ipy!b)oq^e=fBo>C_fgC!>i(SS<#x zuy;pbMKR5>jx?@P9Y5U?3-P)G9X{Owj)s1T_G6eDi*7K@5CRfSQi1&vl1*xbuC_sJ zNboY2Y$_JTfv#i>LnRhUGU%8|upLS4GImnL0dQ>5avwpC1I-*6TnA_jaUSZtwVa1K z#1}5(lEh|Px_pqoZ7bR~c}s&p(v*m#cedi6DSnG?#1#r;vP^Y)6ki8z;2JjQ=TS;} zEnZ;PYJp@CHxqW^Q5WCL3s*n^7-cyMC#D2X%z--`hDHJ=)=x$WX^8VuviKJ~R6=$) zlhoGI#9%@v^_A)i;mZoMziay2ZxO{q zRk*HD8ATApPF9v04dVwPB}{Cg2t+T=jKDM8VBTP8DO&|VxZc?$kzc0%7Jw6!7@B}n z35%hEBn0RYoTE)8DK!&-uaUrPu;9lkCx5jcGn3-kPeheE(oHC_M34UH<=2tz*<|3}>QFthLb{jq=HK$zaxs<`-)gUcHN8?^8KD26{y8qLjxxG;WYKn+f7 z{1D0*m)j?Ro(#>j694cj;x!-=zSydVs-Vw*L9!PKM@!R)(6ExEkDIWV50J zEH?*417c>1=sb@%Ik*+D6=h7ez&J|LAvbAqx8H&1Xvpp=-*5z{H7N*uJ80A&ki=q=nx84GM};s4Q3ixAq68&)B~luA zt{$ViRF;Sy({h7Dt#t$ov^#+a1DW$vC)gvNFXx2BazW&8BJ*Sz=fWwYM^^yJvA<=0y_&-86+hXj=|)TJn5GCYMxQR z&2)d0p{K>_3elhV2xN2`7%_klvL=$S>+a$f~z4CVk75`^#VatSC~ zMM=4gtVK2O?ONJM9LQGk2X+oUmtbt;gn&DyrcIQ)$~rCsUG@ADNz7d&)`D#OQQhr6 zY5+fRg9oZ#M=Y^*gbV0symMeUGqSm_-1{hbXs|GNpb+IyvYt%?3CX9JMi}e7ZAP?B z>u5%zhpO!L7l9;G7LED6Pl10M&#*H0E6vJ;Zh{k4m2JJhYz5gUPr(5o-eU{1wdgyCcx4GtOJw>TnXq4;5&dZ05<@P3P1>e<$>G)fCE?p z-UPGrYx~cOKX~{L`Del(jK3y66@Bmgef9tD*VYfQe;mF${Y>(U<7>-*t9C%_TNP|?vyQ@> z0_^Lxp4NMf?B%#_+8b=_U!%T`+Pmo0qGpiV4r=|QPM_Kn>R(U&1$w{gy{V?Jnl^M0 zWFpPyBmQ<-+2^62?qzvh=c$$^P4Y*YOp#})p7uvf?J%q29l=wM1_hY8WB-W;0h|k1 zAFvJKxx^)frwuk0EHGFagFqw}PGf4y#;gMzWxpmP+>H~Fobocw_MyDMTg~HnwrsWi zmTI#cHQ0>(c-xeQn^6$E+h&TTkb`CR0FJO>V>_kB4q`_n2s^+a*5r#Kdu*YtcY##< zc~ijxU)cRNg}XD15Co#rzSQCUgWDS3+tN5;7aymf;fnw~_67ri5v&2m2{Qu2X>BnC zD;*yMXJlR154Ia$&<~fvts^G@d-jgUTpp7_W9m%ON1Sfyfa&w-4g|T_dB7jk%ysA- zB^1^2*+;YthC_xe-|app#lXTncqj~9Kc~=Lcy2SI+n8;$w2D!P^-VMOTN(3VJ@z|} zlx#Y)e+wtAa4ulpOCqsFIyU1~XwuWQToajSJ_uL*t71gmZKfxs^Zw=1%H_B9@GmL< zh({p^F~SfiSS>6oH5>#46N?X-(U7seom?n(j09HXVT(+w5thIYV+c{XM*d*BLS9{& z3S%fk8y8o}UaDDDaNy^E%BBCfG61Is*)J%930^SbilO8Tp+gzqhz%zm-#1-nJM<<7 z04f7Gza%a4>Vxt>>dL(FSKGOqfq+f&nPWSmS0Z0LP=xB^-{4ah$S;Tb7eee5#?Sz0fTG=ziW`12 zhhnqV5e0OVc4{QT*Zkv;;P6W{HZT#F z`(9opwllf?uR4|orJ~2E?y(*mc{f6KYDrb&p=L}RSpHSSP&CD|q9)_IC&7S{2F^#2bcBy-95n7zDzs~o#`T%+2YYfuqpKE@&s|OA#AgAXL3_{*qEV*5Z9GaJ0#~%{7-Z_8fj89 zIy_;LW4z}}c5$-C7jSGUd?bvZu+Six#fBos@f*Z^9}N^(-82iqwGD$bU(nO(AG9$L zZaaxc5#eYlGr7B~FyO)7%3nw-hrt09CUZh$Akg;9BR2W(h>`|0(c;ShU@EH_Q)5rC zRwV2++JfpWG-x}RVIASAh-rZ_%SJowotg(x4jN>JhD={0t~scd^H`VSli1<~5bIL0 z;?^l10q`}X2*w!Mfm1JbOadb}1w$BI)F&A`NlX4OZPfX6C^6#{%R^1>>I-nFgv85I z;p`>_I_uP7a(VkoCn6d}4y?$4KuxH*njBSQ#J55q78eNMlFjL4DHYu!2!reVHOrYw zxOs=JlUtKj3>(R2Q*G#2unmQ+_W6R*?{4|x(Z)Ff<%qx zQoikp^r6;a<`biwRbVH$I0icdK>~7#0LfcQ|CB(Ncy(MD@UV>51`_UwfRQ;*d36Bb zt1iC!nH6{er~->;^A;Y`FMin**qXj3r*eEmOgYRNDhvcNsKpmaCLElcdUgd%-hm)g zq}VqqB3h9a;xc zPwDZt+vdGZ5PT zC2nez_srBZrC(FXTlg>h9q~?oBEj`BCkehc&l6yqJ0cgybQ&H$Pk{|$94O%lP}+GF z-aN&|&8Dd;oW3xqK}B;bKo#{22?k@5>zVRZ1O*1pLu>ey2=bqFM_Jk2|AI0~kN|Tb~g=ioRCU`R5Tuqr>7)`81_ImfI5M0>G@15Ksf=i=&>_r^_rk zy?i<@NfHSuPR6K3hzkM?c}MJLB0erP`zgJMsFGlg##FbC8G!OvX8|W-G=%+<`z))U zQopw^)Q>@-MF7Ib*#DQ0+tW}+h&7sNP+(@puzLbSBl{>^2#^Ad5MM*M5g>94%-Sz< zK;X+t!8V_H3DMDjr#*u04sp4Tphm>KI&&Y!VQd0~G(d^~0q&}I>4!rp<)&u_)<61- zv1hAG63f&k5*u?;cH95r!5}3e{YVXdEk8CS1IX-?KzkAa=aVg#`*YDt0NMKA-4zM{W5F6g}{2WPIgmw7g1 zn-CLi#ucInL$&?yl90Eb8tq70f#q=Bq)k_~<3M~8K;O1A>K^IPlDZ&Si*5g%Aov@W z`t_U4d!7{tp1B09kim<{e&uLEfOv;-jocBN^q3zb1qZxgq8SHeU!d7UScR9y$7It|>yXq6(~)sfMJDv#7St>lpP+vQ z>$`4i(;*N^Ytra~mI!?y5c3+8_JtjQZ|RwCW=m3X?L-!d2Lk(%Hs08|rmU!7ZvGY4 z)pR>BYon*3Ff_VSM5tw{LcF!2yNE1BTTX6R*{)1MU}ORvl)}+7Vq%q%fU)riy%?wn z2Ru0jk{LqH@U#F@4?#t`gbBbXhVY@Af`S}o0Z>5Am_OU!CRb@#TfqGGpn-Iw+hBTo zNL=j4a + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/doc/website/fonts/fontawesome-webfont.ttf b/doc/website/fonts/fontawesome-webfont.ttf new file mode 100644 index 0000000000000000000000000000000000000000..96a3639cdde5e8ab459c6380e3b9524ee81641dc GIT binary patch literal 112160 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 zÄ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 V*eEM;|ho{trTA6=?tf literal 0 HcmV?d00001 diff --git a/doc/website/fonts/fontawesome-webfont.woff b/doc/website/fonts/fontawesome-webfont.woff new file mode 100644 index 0000000000000000000000000000000000000000..628b6a52a87e62c6f22426e17c01f6a303aa194e GIT binary patch literal 65452 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 q%u^)w#YX9ZIvhtu?9Cy6YRi6f6G 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 literal 0 HcmV?d00001 diff --git a/doc/website/screenshot.png b/doc/website/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..317477678c2a03b503b58e67c1731b42530f9fa5 GIT binary patch literal 245289 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

jYTtY$s{>w2N19F1@z0ZNo#bt~S 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

{F5eu#$MVjLG3COCx*-%wjy>wZ5( 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 literal 0 HcmV?d00001 From ec3ff5f41afdf1fb6764cf531cdf22ec057961c1 Mon Sep 17 00:00:00 2001 From: umpirsky Date: Thu, 3 Mar 2016 15:40:41 +0100 Subject: [PATCH 167/426] Update some adapter pages --- doc/adapters/googleCloudStorage.md | 6 ++---- doc/adapters/gridFs.md | 12 ++++-------- doc/adapters/inMemory.md | 6 ++---- doc/adapters/local.md | 6 ++---- doc/adapters/mogileFS.md | 4 ---- doc/adapters/openCloud.md | 11 ++++------- doc/adapters/phpseclibSftp.md | 6 +++--- doc/adapters/sftp.md | 13 +++++-------- doc/adapters/zip.md | 6 ++---- 9 files changed, 24 insertions(+), 46 deletions(-) delete mode 100644 doc/adapters/mogileFS.md diff --git a/doc/adapters/googleCloudStorage.md b/doc/adapters/googleCloudStorage.md index 41de1c461..7da444ef2 100644 --- a/doc/adapters/googleCloudStorage.md +++ b/doc/adapters/googleCloudStorage.md @@ -1,13 +1,11 @@ -GoogleCloudStorage -================== +# GoogleCloudStorage To use the GoogleCloudStorage adapter you will need to create a connection using the [Google APIs Client Library for PHP] (https://github.com/google/google-api-php-client) and create a Client ID/Service Account in your [Developers Console] (https://console.developers.google.com/). You can then create the `\Google_Service_Storage` which is required for the GoogleCloudStorage adapter. -Example -------- +## Example ```php selectDB('dbname'); $gridFs = new \MongoGridFs($db); $adapter = new \Gaufrette\Adapter\GridFS($gridFs); - diff --git a/doc/adapters/inMemory.md b/doc/adapters/inMemory.md index 18e50c372..2259ba52a 100644 --- a/doc/adapters/inMemory.md +++ b/doc/adapters/inMemory.md @@ -1,10 +1,8 @@ -InMemory -======== +# InMemory This adapter is useful in test environments where you don't want to depend on external filesystems. -Example -------- +## Example `InMemory` adapter only takes an array of available files as argument. diff --git a/doc/adapters/local.md b/doc/adapters/local.md index fb82d7dcb..3f768cf59 100644 --- a/doc/adapters/local.md +++ b/doc/adapters/local.md @@ -1,10 +1,8 @@ -Local & SafeLocal -================= +# Local & SafeLocal Those two adapters aims to use local filesystem. The second one will encode in base64 the filename before storing/retrieving. -Example -------- +## Example ```php Date: Thu, 3 Mar 2016 16:48:11 +0200 Subject: [PATCH 168/426] Drop php 5.3 on travis --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 269e11c98..4518858a6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,7 +7,6 @@ cache: - $HOME/.composer/cache php: - - 5.3 - 5.4 - 5.5 - 5.6 From 54e85791496627e934501d4bd28556026cc39a54 Mon Sep 17 00:00:00 2001 From: umpirsky Date: Thu, 3 Mar 2016 16:18:00 +0100 Subject: [PATCH 169/426] Rename adapter doc files --- doc/adapters/{awsS3.md => aws-s3.md} | 0 .../{azureBlobStorage.md => azure-blob-storage.md} | 0 ...ogleCloudStorage.md => google-cloud-storage.md} | 0 doc/adapters/{gridFs.md => grid-fs.md} | 0 doc/adapters/{inMemory.md => in-memory.md} | 0 doc/adapters/{openCloud.md => open-cloud.md} | 0 .../{phpseclibSftp.md => phpseclib-sftp.md} | 0 doc/couscous.yml | 14 +++++++------- 8 files changed, 7 insertions(+), 7 deletions(-) rename doc/adapters/{awsS3.md => aws-s3.md} (100%) rename doc/adapters/{azureBlobStorage.md => azure-blob-storage.md} (100%) rename doc/adapters/{googleCloudStorage.md => google-cloud-storage.md} (100%) rename doc/adapters/{gridFs.md => grid-fs.md} (100%) rename doc/adapters/{inMemory.md => in-memory.md} (100%) rename doc/adapters/{openCloud.md => open-cloud.md} (100%) rename doc/adapters/{phpseclibSftp.md => phpseclib-sftp.md} (100%) diff --git a/doc/adapters/awsS3.md b/doc/adapters/aws-s3.md similarity index 100% rename from doc/adapters/awsS3.md rename to doc/adapters/aws-s3.md diff --git a/doc/adapters/azureBlobStorage.md b/doc/adapters/azure-blob-storage.md similarity index 100% rename from doc/adapters/azureBlobStorage.md rename to doc/adapters/azure-blob-storage.md diff --git a/doc/adapters/googleCloudStorage.md b/doc/adapters/google-cloud-storage.md similarity index 100% rename from doc/adapters/googleCloudStorage.md rename to doc/adapters/google-cloud-storage.md diff --git a/doc/adapters/gridFs.md b/doc/adapters/grid-fs.md similarity index 100% rename from doc/adapters/gridFs.md rename to doc/adapters/grid-fs.md diff --git a/doc/adapters/inMemory.md b/doc/adapters/in-memory.md similarity index 100% rename from doc/adapters/inMemory.md rename to doc/adapters/in-memory.md diff --git a/doc/adapters/openCloud.md b/doc/adapters/open-cloud.md similarity index 100% rename from doc/adapters/openCloud.md rename to doc/adapters/open-cloud.md diff --git a/doc/adapters/phpseclibSftp.md b/doc/adapters/phpseclib-sftp.md similarity index 100% rename from doc/adapters/phpseclibSftp.md rename to doc/adapters/phpseclib-sftp.md diff --git a/doc/couscous.yml b/doc/couscous.yml index 08ee74226..d7e32f63f 100644 --- a/doc/couscous.yml +++ b/doc/couscous.yml @@ -36,31 +36,31 @@ menu: relativeUrl: adapters/apc.html awss3: text: AWS S3 - relativeUrl: adapters/awss3.html + relativeUrl: adapters/aws-s3.html azureBlobStorage: text: AzureBlobStorage - relativeUrl: adapters/azureBlobStorage.html + relativeUrl: adapters/azure-blob-storage.html gtp: text: Ftp relativeUrl: adapters/ftp.html googleCloudStorage: text: Google Cloud Storage - relativeUrl: adapters/googleCloudStorage.html + relativeUrl: adapters/google-cloud-storage.html gridFs: text: GridFS - relativeUrl: adapters/gridFs.html + relativeUrl: adapters/grid-fs.html InMemory: text: InMemory - relativeUrl: adapters/inMemory.html + relativeUrl: adapters/in-memory.html local: text: Local & SafeLocal relativeUrl: adapters/local.html openCloud: text: OpenCloud & LazyOpenCloud - relativeUrl: adapters/openCloud.html + relativeUrl: adapters/open-cloud.html phpseclibSftp: text: PhpseclibSftp - relativeUrl: adapters/phpseclibSftp.html + relativeUrl: adapters/phpseclib-sftp.html sftp: text: Sftp relativeUrl: adapters/sftp.html From 89616e80692c8cf06a6959b39ea8ab1e7005c25c Mon Sep 17 00:00:00 2001 From: umpirsky Date: Thu, 3 Mar 2016 16:23:37 +0100 Subject: [PATCH 170/426] Update texts --- doc/couscous.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/couscous.yml b/doc/couscous.yml index d7e32f63f..497185b5a 100644 --- a/doc/couscous.yml +++ b/doc/couscous.yml @@ -29,7 +29,7 @@ menu: name: Adapters items: amazonS3: - text: AmazonS3 + text: Amazon S3 relativeUrl: adapters/amazon-s3.html apc: text: Apc @@ -38,7 +38,7 @@ menu: text: AWS S3 relativeUrl: adapters/aws-s3.html azureBlobStorage: - text: AzureBlobStorage + text: Azure Blob Storage relativeUrl: adapters/azure-blob-storage.html gtp: text: Ftp @@ -53,10 +53,10 @@ menu: text: InMemory relativeUrl: adapters/in-memory.html local: - text: Local & SafeLocal + text: Local relativeUrl: adapters/local.html openCloud: - text: OpenCloud & LazyOpenCloud + text: OpenCloud relativeUrl: adapters/open-cloud.html phpseclibSftp: text: PhpseclibSftp From 5a8fc0df0d3cfe54f96c5728db9b9049bff4e2ab Mon Sep 17 00:00:00 2001 From: umpirsky Date: Thu, 3 Mar 2016 16:29:30 +0100 Subject: [PATCH 171/426] Set current adapter page --- doc/adapters/amazon-s3.md | 4 ++++ doc/adapters/apc.md | 4 ++++ doc/adapters/aws-s3.md | 4 ++++ doc/adapters/azure-blob-storage.md | 4 ++++ doc/adapters/ftp.md | 4 ++++ doc/adapters/google-cloud-storage.md | 4 ++++ doc/adapters/grid-fs.md | 4 ++++ doc/adapters/in-memory.md | 4 ++++ doc/adapters/local.md | 4 ++++ doc/adapters/open-cloud.md | 4 ++++ doc/adapters/phpseclib-sftp.md | 4 ++++ doc/adapters/sftp.md | 4 ++++ doc/adapters/zip.md | 4 ++++ doc/couscous.yml | 18 +++++++++--------- 14 files changed, 61 insertions(+), 9 deletions(-) diff --git a/doc/adapters/amazon-s3.md b/doc/adapters/amazon-s3.md index 1c704b41f..cddfd3b42 100644 --- a/doc/adapters/amazon-s3.md +++ b/doc/adapters/amazon-s3.md @@ -1,3 +1,7 @@ +--- +currentMenu: amazon-s3 +--- + # AmazonS3 & AclAwareAmazonS3 When using the legacy Amazon S3 adapters (aws-sdk-php < 2), you will need to specify a CA diff --git a/doc/adapters/apc.md b/doc/adapters/apc.md index 01344a28f..badd6888e 100644 --- a/doc/adapters/apc.md +++ b/doc/adapters/apc.md @@ -1,3 +1,7 @@ +--- +currentMenu: apc +--- + # APC `apc` extension should be enabled in order to use this adapter. diff --git a/doc/adapters/aws-s3.md b/doc/adapters/aws-s3.md index e60b7b3d7..5ca835fbc 100644 --- a/doc/adapters/aws-s3.md +++ b/doc/adapters/aws-s3.md @@ -1,3 +1,7 @@ +--- +currentMenu: aws-s3 +--- + # AWS S3 First, you will need to install AWS SDK for PHP: diff --git a/doc/adapters/azure-blob-storage.md b/doc/adapters/azure-blob-storage.md index 9d7750f10..cbff2a36f 100644 --- a/doc/adapters/azure-blob-storage.md +++ b/doc/adapters/azure-blob-storage.md @@ -1,3 +1,7 @@ +--- +currentMenu: azure-blob-storage +--- + # AzureBlobStorage Azure Blob Storage is the storage service provided by Microsoft Windows Azure cloud environment. To use this adapter diff --git a/doc/adapters/ftp.md b/doc/adapters/ftp.md index 8046bc91c..129cb0683 100644 --- a/doc/adapters/ftp.md +++ b/doc/adapters/ftp.md @@ -1,3 +1,7 @@ +--- +currentMenu: ftp +--- + # FTP `ftp` extension should be enabled in order to use this adapter. diff --git a/doc/adapters/google-cloud-storage.md b/doc/adapters/google-cloud-storage.md index 7da444ef2..7f1f219ce 100644 --- a/doc/adapters/google-cloud-storage.md +++ b/doc/adapters/google-cloud-storage.md @@ -1,3 +1,7 @@ +--- +currentMenu: google-cloud-storage +--- + # GoogleCloudStorage To use the GoogleCloudStorage adapter you will need to create a connection using the [Google APIs Client Library for PHP] diff --git a/doc/adapters/grid-fs.md b/doc/adapters/grid-fs.md index 622cca29b..a24615406 100644 --- a/doc/adapters/grid-fs.md +++ b/doc/adapters/grid-fs.md @@ -1,3 +1,7 @@ +--- +currentMenu: grid-fs +--- + # GridFS ## Prerequisites diff --git a/doc/adapters/in-memory.md b/doc/adapters/in-memory.md index 2259ba52a..1468397c6 100644 --- a/doc/adapters/in-memory.md +++ b/doc/adapters/in-memory.md @@ -1,3 +1,7 @@ +--- +currentMenu: in-memory +--- + # InMemory This adapter is useful in test environments where you don't want to depend on external filesystems. diff --git a/doc/adapters/local.md b/doc/adapters/local.md index 3f768cf59..0c4c939c6 100644 --- a/doc/adapters/local.md +++ b/doc/adapters/local.md @@ -1,3 +1,7 @@ +--- +currentMenu: local +--- + # Local & SafeLocal Those two adapters aims to use local filesystem. The second one will encode in base64 the filename before storing/retrieving. diff --git a/doc/adapters/open-cloud.md b/doc/adapters/open-cloud.md index 0924ee938..b95b9822e 100644 --- a/doc/adapters/open-cloud.md +++ b/doc/adapters/open-cloud.md @@ -1,3 +1,7 @@ +--- +currentMenu: open-cloud +--- + # OpenCloud & LazyOpenCloud To use the OpenCloud adapter you will need to create a connection using the [OpenCloud SDK](https://github.com/rackspace/php-opencloud). diff --git a/doc/adapters/phpseclib-sftp.md b/doc/adapters/phpseclib-sftp.md index 711df2d9d..e25a76306 100644 --- a/doc/adapters/phpseclib-sftp.md +++ b/doc/adapters/phpseclib-sftp.md @@ -1,3 +1,7 @@ +--- +currentMenu: phpseclib-sftp +--- + # phpseclib adapter *N.B.* It is recommended to use this adapter over [SFTP](doc/adapters/sftp.md). diff --git a/doc/adapters/sftp.md b/doc/adapters/sftp.md index fde2494a7..ea3a1ec0b 100644 --- a/doc/adapters/sftp.md +++ b/doc/adapters/sftp.md @@ -1,3 +1,7 @@ +--- +currentMenu: sftp +--- + # SFTP *N.B.* SFTP adapter is not recommended to use due to https://bugs.php.net/bug.php?id=64169. It is recommended to use diff --git a/doc/adapters/zip.md b/doc/adapters/zip.md index 4f4fe8f37..4d886cbec 100644 --- a/doc/adapters/zip.md +++ b/doc/adapters/zip.md @@ -1,3 +1,7 @@ +--- +currentMenu: zip +--- + # ZIP If you want to use this `Adapter`, first you should be sure `zip` extension is installed. diff --git a/doc/couscous.yml b/doc/couscous.yml index 497185b5a..05069de73 100644 --- a/doc/couscous.yml +++ b/doc/couscous.yml @@ -28,37 +28,37 @@ menu: adapters: name: Adapters items: - amazonS3: + amazon-s3: text: Amazon S3 relativeUrl: adapters/amazon-s3.html apc: text: Apc relativeUrl: adapters/apc.html - awss3: + aws-s3: text: AWS S3 relativeUrl: adapters/aws-s3.html - azureBlobStorage: + azure-blob-storage: text: Azure Blob Storage relativeUrl: adapters/azure-blob-storage.html - gtp: + ftp: text: Ftp relativeUrl: adapters/ftp.html - googleCloudStorage: + google-cloud-storage: text: Google Cloud Storage relativeUrl: adapters/google-cloud-storage.html - gridFs: + grid-fs: text: GridFS relativeUrl: adapters/grid-fs.html - InMemory: + in-memory: text: InMemory relativeUrl: adapters/in-memory.html local: text: Local relativeUrl: adapters/local.html - openCloud: + open-cloud: text: OpenCloud relativeUrl: adapters/open-cloud.html - phpseclibSftp: + phpseclib-sftp: text: PhpseclibSftp relativeUrl: adapters/phpseclib-sftp.html sftp: From 85b7d94316df9c7ce0378112c80ca4c24f823434 Mon Sep 17 00:00:00 2001 From: umpirsky Date: Thu, 3 Mar 2016 16:38:20 +0100 Subject: [PATCH 172/426] Set base url --- doc/couscous.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/couscous.yml b/doc/couscous.yml index 05069de73..975943a30 100644 --- a/doc/couscous.yml +++ b/doc/couscous.yml @@ -3,6 +3,7 @@ subTitle: Filesystem abstraction layer github: user: KnpLabs repo: Gaufrette +baseUrl: http://knplabs.github.io/Gaufrette template: # url: https://github.com/CouscousPHP/Template-Dark index: introduction.md From fe804b29b0899f29d4d562e5ec020ee80eaec8f1 Mon Sep 17 00:00:00 2001 From: umpirsky Date: Thu, 3 Mar 2016 17:45:23 +0100 Subject: [PATCH 173/426] Remove index.bak --- doc/index.bak.md | 20 -------------------- 1 file changed, 20 deletions(-) delete mode 100644 doc/index.bak.md diff --git a/doc/index.bak.md b/doc/index.bak.md deleted file mode 100644 index 26e8d6be9..000000000 --- a/doc/index.bak.md +++ /dev/null @@ -1,20 +0,0 @@ -Adapters --------- - - * [AmazonS3 & AclAwareAmazonS3](adapters/amazonS3.md) - * [Apc](adapters/apc.md) - * [AwsS3](adapters/awsS3.md) - * [AzureBlobStorage](adapters/azureBlobStorage.md) - * [Cache](#cache) - * [Doctrine DBAL](adapters/doctrineDbal.md): TBW - * [Dropbox](adapters/dropbox.md): TBW - * [Ftp](adapters/ftp.md) - * [Google Cloud Storage](adapters/googleCloudStorage.md) - * [GridFS](adapters/gridFs.md) - * [InMemory](adapters/inMemory.md) - * [Local & SafeLocal](adapters/local.md) - * [MogileFS](adapters/mogileFS.md): TBW - * [OpenCloud & LazyOpenCloud](adapters/openCloud.md) - * [PhpseclibSftp](adapters/phpseclibSftp.md) - * [Sftp](adapters/sftp.md) - * [Zip](adapters/zip.md) From bd29451c25baeb2ab7f6732558aa5d7b2b41ff36 Mon Sep 17 00:00:00 2001 From: umpirsky Date: Thu, 3 Mar 2016 17:48:09 +0100 Subject: [PATCH 174/426] Remove broken link --- doc/basic-usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/basic-usage.md b/doc/basic-usage.md index 6e62d8b98..50d2f0ea6 100644 --- a/doc/basic-usage.md +++ b/doc/basic-usage.md @@ -4,7 +4,7 @@ currentMenu: basic-usage # Basic Usage -Following an example with the local filesystem adapter. To setup other adapters, look up [their respective documentation](https://github.com/KnpLabs/Gaufrette/tree/master/doc/#adapters). +Following an example with the local filesystem adapter. To setup other adapters, look up their respective documentation. ```php Date: Thu, 3 Mar 2016 17:48:48 +0100 Subject: [PATCH 175/426] Fix installation doc --- doc/installation.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/doc/installation.md b/doc/installation.md index 7328224fd..8cf5d1ea9 100644 --- a/doc/installation.md +++ b/doc/installation.md @@ -7,6 +7,5 @@ currentMenu: installation You can install it through Composer: ```bash -$ composer require knplabs/gaufrette:~0.2 # Stable version -$ composer require knplabs/gaufrette:0.4.*@dev # Development version +$ composer require knplabs/gaufrette ``` From 9a4ed897341db540291c27b064180eba93f74604 Mon Sep 17 00:00:00 2001 From: umpirsky Date: Thu, 3 Mar 2016 17:52:47 +0100 Subject: [PATCH 176/426] Switch to https --- doc/couscous.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/couscous.yml b/doc/couscous.yml index 975943a30..e3e462683 100644 --- a/doc/couscous.yml +++ b/doc/couscous.yml @@ -3,7 +3,7 @@ subTitle: Filesystem abstraction layer github: user: KnpLabs repo: Gaufrette -baseUrl: http://knplabs.github.io/Gaufrette +baseUrl: https://knplabs.github.io/Gaufrette template: # url: https://github.com/CouscousPHP/Template-Dark index: introduction.md From d43849dad0e027a49e0dda7cd0ea2483fcbef806 Mon Sep 17 00:00:00 2001 From: umpirsky Date: Fri, 4 Mar 2016 09:07:24 +0100 Subject: [PATCH 177/426] Remove screnshot and gitignore of docs theme --- doc/website/.gitignore | 1 - doc/website/screenshot.png | Bin 245289 -> 0 bytes 2 files changed, 1 deletion(-) delete mode 100644 doc/website/.gitignore delete mode 100644 doc/website/screenshot.png diff --git a/doc/website/.gitignore b/doc/website/.gitignore deleted file mode 100644 index 9ca439cb4..000000000 --- a/doc/website/.gitignore +++ /dev/null @@ -1 +0,0 @@ -.couscous/ diff --git a/doc/website/screenshot.png b/doc/website/screenshot.png deleted file mode 100644 index 317477678c2a03b503b58e67c1731b42530f9fa5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 245289 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

jYTtY$s{>w2N19F1@z0ZNo#bt~S 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

{F5eu#$MVjLG3COCx*-%wjy>wZ5( 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 From 17b23e22b8c4fab46f6a87b2bd5c5c217e64561d Mon Sep 17 00:00:00 2001 From: umpirsky Date: Fri, 4 Mar 2016 09:14:20 +0100 Subject: [PATCH 178/426] Fix broken markdown and broken links --- doc/adapters/phpseclib-sftp.md | 2 +- doc/adapters/sftp.md | 4 ++-- doc/caching.md | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/doc/adapters/phpseclib-sftp.md b/doc/adapters/phpseclib-sftp.md index e25a76306..cf281f8f7 100644 --- a/doc/adapters/phpseclib-sftp.md +++ b/doc/adapters/phpseclib-sftp.md @@ -4,7 +4,7 @@ currentMenu: phpseclib-sftp # phpseclib adapter -*N.B.* It is recommended to use this adapter over [SFTP](doc/adapters/sftp.md). +*N.B.* It is recommended to use this adapter over [SFTP](sftp.html). ## Prerequisites diff --git a/doc/adapters/sftp.md b/doc/adapters/sftp.md index ea3a1ec0b..910f396be 100644 --- a/doc/adapters/sftp.md +++ b/doc/adapters/sftp.md @@ -5,10 +5,10 @@ currentMenu: sftp # SFTP *N.B.* SFTP adapter is not recommended to use due to https://bugs.php.net/bug.php?id=64169. It is recommended to use -[phpseclib SFTP adapter](phpseclib_sftp.md) instead. +[phpseclib SFTP adapter](phpseclib-sftp.html) instead. This adapter is based on the `ssh2` extension. If you don't have this extension available and you can't install it, -the [`PhpseclibSftp`](phpseclibSftp.md) adapter is based on a full-php ssh client. +the [`PhpseclibSftp`](phpseclib-sftp.html) adapter is based on a full-php ssh client. ## Prerequisites diff --git a/doc/caching.md b/doc/caching.md index a8436fd45..80cfdf5da 100644 --- a/doc/caching.md +++ b/doc/caching.md @@ -8,8 +8,8 @@ If you have to deal with a slow filesystem, it is out of question to use it dire So, you need a cache! Happily, Gaufrette offers a cache system ready for use. It consist of an adapter decorator itself composed of two adapters: - * The *source* adapter that should be cached - * The *cache* adapter that is used to cache +* The *source* adapter that should be cached +* The *cache* adapter that is used to cache Here is an example of how to cache an ftp filesystem: From 795b8e74bec03571cd8a971c15be6fbe2b867192 Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Thu, 3 Mar 2016 18:57:41 +0200 Subject: [PATCH 179/426] Travis adjustments and tuning --- .travis.yml | 57 ++++++++++++++++++++++++++++------------------------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4518858a6..8c134100b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,27 +1,30 @@ -language: php - -sudo: false - -cache: - directories: - - $HOME/.composer/cache - -php: - - 5.4 - - 5.5 - - 5.6 - - hhvm - -matrix: - allow_failures: - - php: hhvm - fast_finish: true - -before_script: - - echo "extension = mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini - - cp tests/Gaufrette/Functional/adapters/DoctrineDbal.php.dist tests/Gaufrette/Functional/adapters/DoctrineDbal.php -f - - composer self-update && composer install --dev --prefer-source --no-interaction --ignore-platform-reqs - -script: - - php bin/phpspec run -fpretty --verbose - - bin/phpunit +language: php + +sudo: false + +cache: + directories: + - $HOME/.composer/cache/files + +php: + - 5.4 + - 5.5 + - 5.6 + - hhvm + +matrix: + allow_failures: + - php: hhvm + fast_finish: true + +before_script: + - if [[ ${TRAVIS_PHP_VERSION:0:1} == "5" ]]; then echo "extension = mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini && phpenv config-rm xdebug.ini; fi + # waiting https://github.com/mongodb/mongo-php-library/pull/57 + #- if [[ ${TRAVIS_PHP_VERSION:0:1} == "7" ]]; then echo "extension = mongodb.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; fi + - if [[ ${TRAVIS_PHP_VERSION:0:4} == "hhvm" ]]; then composer require mongofill/mongofill --ignore-platform-reqs; fi + - cp tests/Gaufrette/Functional/adapters/DoctrineDbal.php.dist tests/Gaufrette/Functional/adapters/DoctrineDbal.php -f + - composer self-update && composer install --prefer-source --no-interaction --ignore-platform-reqs + +script: + - php bin/phpspec run -fpretty --verbose + - php bin/phpunit From 9fa228769335106cea723c253ca9a9b2e26cfe78 Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Fri, 4 Mar 2016 12:55:34 +0200 Subject: [PATCH 180/426] Drop php 5.3 in composer --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 618ef5f1f..90bf497f5 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ } ], "require": { - "php": ">=5.3.2" + "php": ">=5.4" }, "require-dev": { "aws/aws-sdk-php": "~2", From 0dd8064ae67f8d882a2c68e4037cfda1f2bb07a3 Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Fri, 4 Mar 2016 13:49:00 +0200 Subject: [PATCH 181/426] Fix link to new docs --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 69f1123a2..7512f3913 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ solution. ### Documentation -Read the official [Gaufrette documentation](doc/index.md). +Read the official [Gaufrette documentation](http://knplabs.github.io/Gaufrette/). ### Symfony integration From da4b4954209d358a66ee038f3e074f86cbb27e73 Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Sat, 5 Mar 2016 13:44:12 +0200 Subject: [PATCH 182/426] Add dbal doc --- doc/adapters/doctrine-dbal.md | 38 +++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 doc/adapters/doctrine-dbal.md diff --git a/doc/adapters/doctrine-dbal.md b/doc/adapters/doctrine-dbal.md new file mode 100644 index 000000000..5297e9716 --- /dev/null +++ b/doc/adapters/doctrine-dbal.md @@ -0,0 +1,38 @@ +--- +currentMenu: doctrine-dbal +-------------------------- + +# Doctrine DBAL + +If you aren't already using Doctrine, you should install DBAL via + +```bash +composer require doctrine/dbal +``` + +In order to use the adapter, you will need to prepare the table with the following columns: + +| Columns | +|----------| +| key | +| content | +| mtime | +| checksum | + +## Example + +`Doctrine` adapter takes three arguments : + * the first, mandatory, is a prepared DBAL connection (you can read more about it in [the DBAL docs](http://doctrine-orm.readthedocs.org/projects/doctrine-dbal/en/latest/reference/configuration.html)) + * the second, mandatory, is a table name where the files will be stored + * the third one is optional array of columns, with which you can override the default columns names + +```php + Date: Sat, 5 Mar 2016 16:08:40 +0200 Subject: [PATCH 183/426] Update couscous.yml --- doc/couscous.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/doc/couscous.yml b/doc/couscous.yml index e3e462683..8ca6a6701 100644 --- a/doc/couscous.yml +++ b/doc/couscous.yml @@ -41,6 +41,9 @@ menu: azure-blob-storage: text: Azure Blob Storage relativeUrl: adapters/azure-blob-storage.html + doctrine-dbal: + text: Doctrine DBAL + relativeUrl: adapters/doctrine-dbal.html ftp: text: Ftp relativeUrl: adapters/ftp.html From 039321356eb696f9ec59a5a12cbadc57e9f4ca37 Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Sat, 5 Mar 2016 16:10:10 +0200 Subject: [PATCH 184/426] Update doctrine-dbal.md --- doc/adapters/doctrine-dbal.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/adapters/doctrine-dbal.md b/doc/adapters/doctrine-dbal.md index 5297e9716..9eb713a3c 100644 --- a/doc/adapters/doctrine-dbal.md +++ b/doc/adapters/doctrine-dbal.md @@ -4,7 +4,7 @@ currentMenu: doctrine-dbal # Doctrine DBAL -If you aren't already using Doctrine, you should install DBAL via +If you aren't already using Doctrine, you should install DBAL through: ```bash composer require doctrine/dbal From a44578d6d845a18660d233f3ca690bc0eb33eb05 Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Sat, 5 Mar 2016 16:10:56 +0200 Subject: [PATCH 185/426] Update doctrine-dbal.md --- doc/adapters/doctrine-dbal.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/adapters/doctrine-dbal.md b/doc/adapters/doctrine-dbal.md index 9eb713a3c..a3817aa95 100644 --- a/doc/adapters/doctrine-dbal.md +++ b/doc/adapters/doctrine-dbal.md @@ -7,7 +7,7 @@ currentMenu: doctrine-dbal If you aren't already using Doctrine, you should install DBAL through: ```bash -composer require doctrine/dbal +$ composer require doctrine/dbal ``` In order to use the adapter, you will need to prepare the table with the following columns: @@ -21,7 +21,7 @@ In order to use the adapter, you will need to prepare the table with the followi ## Example -`Doctrine` adapter takes three arguments : +`Doctrine` adapter takes three arguments: * the first, mandatory, is a prepared DBAL connection (you can read more about it in [the DBAL docs](http://doctrine-orm.readthedocs.org/projects/doctrine-dbal/en/latest/reference/configuration.html)) * the second, mandatory, is a table name where the files will be stored * the third one is optional array of columns, with which you can override the default columns names From 7d789afafa3a9c2fd2cc7657c4fe204ade4733e7 Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Sat, 5 Mar 2016 16:12:22 +0200 Subject: [PATCH 186/426] Update doctrine-dbal.md --- doc/adapters/doctrine-dbal.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/doc/adapters/doctrine-dbal.md b/doc/adapters/doctrine-dbal.md index a3817aa95..f8a45cf3d 100644 --- a/doc/adapters/doctrine-dbal.md +++ b/doc/adapters/doctrine-dbal.md @@ -24,7 +24,8 @@ In order to use the adapter, you will need to prepare the table with the followi `Doctrine` adapter takes three arguments: * the first, mandatory, is a prepared DBAL connection (you can read more about it in [the DBAL docs](http://doctrine-orm.readthedocs.org/projects/doctrine-dbal/en/latest/reference/configuration.html)) * the second, mandatory, is a table name where the files will be stored - * the third one is optional array of columns, with which you can override the default columns names + * the third one is optional array of columns, which allows you to override the default column names + ```php Date: Sat, 5 Mar 2016 16:12:56 +0200 Subject: [PATCH 187/426] Update doctrine-dbal.md --- doc/adapters/doctrine-dbal.md | 1 - 1 file changed, 1 deletion(-) diff --git a/doc/adapters/doctrine-dbal.md b/doc/adapters/doctrine-dbal.md index f8a45cf3d..f6c8774b8 100644 --- a/doc/adapters/doctrine-dbal.md +++ b/doc/adapters/doctrine-dbal.md @@ -26,7 +26,6 @@ In order to use the adapter, you will need to prepare the table with the followi * the second, mandatory, is a table name where the files will be stored * the third one is optional array of columns, which allows you to override the default column names - ```php Date: Thu, 3 Mar 2016 09:44:09 +0100 Subject: [PATCH 188/426] Remove very useful docblocks --- src/Gaufrette/Adapter/Apc.php | 2 -- src/Gaufrette/Adapter/AzureBlobStorage.php | 2 -- src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php | 2 -- src/Gaufrette/Adapter/Cache.php | 2 -- src/Gaufrette/Adapter/DoctrineDbal.php | 2 -- src/Gaufrette/Adapter/Dropbox.php | 2 -- src/Gaufrette/Adapter/Ftp.php | 4 +--- src/Gaufrette/Adapter/GridFS.php | 2 -- src/Gaufrette/Adapter/InMemory.php | 2 -- src/Gaufrette/Adapter/Local.php | 2 -- src/Gaufrette/Adapter/MogileFS.php | 2 -- src/Gaufrette/Adapter/OpenCloud.php | 2 -- .../Adapter/OpenStackCloudFiles/ObjectStoreFactory.php | 2 -- src/Gaufrette/Adapter/PhpseclibSftp.php | 2 -- src/Gaufrette/Adapter/Sftp.php | 4 +--- src/Gaufrette/File.php | 4 +--- src/Gaufrette/Filesystem.php | 2 -- src/Gaufrette/Stream/InMemoryBuffer.php | 2 -- src/Gaufrette/Stream/Local.php | 2 -- src/Gaufrette/StreamMode.php | 2 -- 20 files changed, 3 insertions(+), 43 deletions(-) diff --git a/src/Gaufrette/Adapter/Apc.php b/src/Gaufrette/Adapter/Apc.php index 41be154b0..4bd280f52 100644 --- a/src/Gaufrette/Adapter/Apc.php +++ b/src/Gaufrette/Adapter/Apc.php @@ -18,8 +18,6 @@ class Apc implements Adapter protected $ttl; /** - * Constructor - * * @throws \RuntimeException * @param string $prefix to avoid conflicts between filesystems * @param int $ttl time to live, default is 0 diff --git a/src/Gaufrette/Adapter/AzureBlobStorage.php b/src/Gaufrette/Adapter/AzureBlobStorage.php index 0d49e2025..6f60f4bd7 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage.php @@ -48,8 +48,6 @@ class AzureBlobStorage implements Adapter, protected $blobProxy; /** - * Constructor - * * @param AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory * @param string $containerName * @param bool $create diff --git a/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php b/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php index be6e0a7d7..dbf44716d 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php @@ -17,8 +17,6 @@ class BlobProxyFactory implements BlobProxyFactoryInterface protected $connectionString; /** - * Constructor - * * @param string $connectionString */ public function __construct($connectionString) diff --git a/src/Gaufrette/Adapter/Cache.php b/src/Gaufrette/Adapter/Cache.php index 1fc0c8b22..7d1e8f788 100644 --- a/src/Gaufrette/Adapter/Cache.php +++ b/src/Gaufrette/Adapter/Cache.php @@ -36,8 +36,6 @@ class Cache implements Adapter, protected $serializeCache; /** - * Constructor - * * @param Adapter $source The source adapter that must be cached * @param Adapter $cache The adapter used to cache the source * @param integer $ttl Time to live of a cached file diff --git a/src/Gaufrette/Adapter/DoctrineDbal.php b/src/Gaufrette/Adapter/DoctrineDbal.php index 27664f944..3d54d154b 100644 --- a/src/Gaufrette/Adapter/DoctrineDbal.php +++ b/src/Gaufrette/Adapter/DoctrineDbal.php @@ -28,8 +28,6 @@ class DoctrineDbal implements Adapter, ); /** - * Constructor - * * @param Connection $connection The DBAL connection * @param string $table The files table * @param array $columns The column names diff --git a/src/Gaufrette/Adapter/Dropbox.php b/src/Gaufrette/Adapter/Dropbox.php index 4acd52944..18ab253ee 100644 --- a/src/Gaufrette/Adapter/Dropbox.php +++ b/src/Gaufrette/Adapter/Dropbox.php @@ -20,8 +20,6 @@ class Dropbox implements Adapter protected $client; /** - * Constructor - * * @param \Dropbox_API $client The Dropbox API client */ public function __construct(DropboxApi $client) diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index 0e61ed5f2..f00a14408 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -30,8 +30,6 @@ class Ftp implements Adapter, protected $fileData = array(); /** - * Constructor - * * @param string $directory The directory to use in the ftp server * @param string $host The host of the ftp server * @param array $options The options like port, username, password, passive, create, mode @@ -502,7 +500,7 @@ private function connect() $this->connection = ftp_connect($this->host, $this->port); } else { if(function_exists('ftp_ssl_connect')) { - $this->connection = ftp_ssl_connect($this->host, $this->port); + $this->connection = ftp_ssl_connect($this->host, $this->port); } else { throw new \RuntimeException('This Server Has No SSL-FTP Available.'); } diff --git a/src/Gaufrette/Adapter/GridFS.php b/src/Gaufrette/Adapter/GridFS.php index e8e8c3532..c39c1998a 100644 --- a/src/Gaufrette/Adapter/GridFS.php +++ b/src/Gaufrette/Adapter/GridFS.php @@ -22,8 +22,6 @@ class GridFS implements Adapter, protected $gridFS = null; /** - * Constructor - * * @param \MongoGridFS $gridFS */ public function __construct(MongoGridFs $gridFS) diff --git a/src/Gaufrette/Adapter/InMemory.php b/src/Gaufrette/Adapter/InMemory.php index e232b745d..29e50158b 100644 --- a/src/Gaufrette/Adapter/InMemory.php +++ b/src/Gaufrette/Adapter/InMemory.php @@ -18,8 +18,6 @@ class InMemory implements Adapter protected $files = array(); /** - * Constructor - * * @param array $files An array of files */ public function __construct(array $files = array()) diff --git a/src/Gaufrette/Adapter/Local.php b/src/Gaufrette/Adapter/Local.php index 1cb93fcc6..e9360620e 100644 --- a/src/Gaufrette/Adapter/Local.php +++ b/src/Gaufrette/Adapter/Local.php @@ -25,8 +25,6 @@ class Local implements Adapter, private $mode; /** - * Constructor - * * @param string $directory Directory where the filesystem is located * @param boolean $create Whether to create the directory if it does not * exist (default FALSE) diff --git a/src/Gaufrette/Adapter/MogileFS.php b/src/Gaufrette/Adapter/MogileFS.php index 61da741de..a8db4e2bc 100644 --- a/src/Gaufrette/Adapter/MogileFS.php +++ b/src/Gaufrette/Adapter/MogileFS.php @@ -27,8 +27,6 @@ class MogileFS implements Adapter protected $socket; /** - * Constructor - * * @param domain MogileFS domain * @param hosts Array of MogileFS trackers */ diff --git a/src/Gaufrette/Adapter/OpenCloud.php b/src/Gaufrette/Adapter/OpenCloud.php index cd6206dd6..d4969830b 100644 --- a/src/Gaufrette/Adapter/OpenCloud.php +++ b/src/Gaufrette/Adapter/OpenCloud.php @@ -41,8 +41,6 @@ class OpenCloud implements Adapter, protected $container; /** - * Constructor - * * @param Service $objectStore * @param string $containerName The name of the container * @param bool $createContainer Whether to create the container if it does not exist diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactory.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactory.php index 16ef879d0..8eba380e5 100644 --- a/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactory.php +++ b/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactory.php @@ -32,8 +32,6 @@ class ObjectStoreFactory implements ObjectStoreFactoryInterface protected $objectStoreType; /** - * Constructor - * * @param OpenStack $connection * @param string $region * @param string $urlType diff --git a/src/Gaufrette/Adapter/PhpseclibSftp.php b/src/Gaufrette/Adapter/PhpseclibSftp.php index 1c873964e..fd3a7c212 100644 --- a/src/Gaufrette/Adapter/PhpseclibSftp.php +++ b/src/Gaufrette/Adapter/PhpseclibSftp.php @@ -17,8 +17,6 @@ class PhpseclibSftp implements Adapter, protected $initialized = false; /** - * Constructor. - * * @param SFTP $sftp An Sftp instance * @param string $directory The distant directory * @param bool $create Whether to create the remote directory if it diff --git a/src/Gaufrette/Adapter/Sftp.php b/src/Gaufrette/Adapter/Sftp.php index 782928374..de98e2171 100644 --- a/src/Gaufrette/Adapter/Sftp.php +++ b/src/Gaufrette/Adapter/Sftp.php @@ -14,8 +14,6 @@ class Sftp implements Adapter, protected $initialized = false; /** - * Constructor - * * @param \Ssh\Sftp $sftp An Sftp instance * @param string $directory The distant directory * @param boolean $create Whether to create the remote directory if it @@ -208,7 +206,7 @@ protected function ensureDirectoryExists($directory, $create = false) if (false === $resource && (!$create || !$this->createDirectory($directory))) { throw new \RuntimeException(sprintf('The directory \'%s\' does not exist and could not be created.', $directory)); } - + // make sure we don't leak the resource if (is_resource($resource)) { closedir($resource); diff --git a/src/Gaufrette/File.php b/src/Gaufrette/File.php index 2e9601165..6b995056b 100644 --- a/src/Gaufrette/File.php +++ b/src/Gaufrette/File.php @@ -45,8 +45,6 @@ class File protected $mtime = null; /** - * Constructor - * * @param string $key * @param Filesystem $filesystem */ @@ -114,7 +112,7 @@ public function getSize() * Returns the file modified time * * @return int - */ + */ public function getMtime() { return $this->mtime = $this->filesystem->mtime($this->key); diff --git a/src/Gaufrette/Filesystem.php b/src/Gaufrette/Filesystem.php index d38a3864d..9162f0fa6 100644 --- a/src/Gaufrette/Filesystem.php +++ b/src/Gaufrette/Filesystem.php @@ -22,8 +22,6 @@ class Filesystem protected $fileRegister = array(); /** - * Constructor - * * @param Adapter $adapter A configured Adapter instance */ public function __construct(Adapter $adapter) diff --git a/src/Gaufrette/Stream/InMemoryBuffer.php b/src/Gaufrette/Stream/InMemoryBuffer.php index 09d527ca4..cca06652a 100644 --- a/src/Gaufrette/Stream/InMemoryBuffer.php +++ b/src/Gaufrette/Stream/InMemoryBuffer.php @@ -18,8 +18,6 @@ class InMemoryBuffer implements Stream private $synchronized; /** - * Constructor - * * @param Filesystem $filesystem The filesystem managing the file to stream * @param string $key The file key */ diff --git a/src/Gaufrette/Stream/Local.php b/src/Gaufrette/Stream/Local.php index 897831907..776a4bf21 100644 --- a/src/Gaufrette/Stream/Local.php +++ b/src/Gaufrette/Stream/Local.php @@ -17,8 +17,6 @@ class Local implements Stream private $fileHandle; /** - * Constructor - * * @param string $path */ public function __construct($path) diff --git a/src/Gaufrette/StreamMode.php b/src/Gaufrette/StreamMode.php index 581a29890..28a7b988e 100644 --- a/src/Gaufrette/StreamMode.php +++ b/src/Gaufrette/StreamMode.php @@ -15,8 +15,6 @@ class StreamMode private $flag; /** - * Constructor - * * @param string $mode A stream mode as for the use of fopen() */ public function __construct($mode) From fe647ddedc9f4bb40486fa254c228d895376d71a Mon Sep 17 00:00:00 2001 From: umpirsky Date: Thu, 3 Mar 2016 09:56:25 +0100 Subject: [PATCH 189/426] CS fixes --- src/Gaufrette/Adapter.php | 32 ++--- src/Gaufrette/Adapter/AclAwareAmazonS3.php | 25 ++-- src/Gaufrette/Adapter/AmazonS3.php | 51 ++++---- src/Gaufrette/Adapter/Apc.php | 38 +++--- src/Gaufrette/Adapter/AwsS3.php | 55 ++++---- src/Gaufrette/Adapter/AzureBlobStorage.php | 67 +++++----- .../AzureBlobStorage/BlobProxyFactory.php | 6 +- .../BlobProxyFactoryInterface.php | 4 +- src/Gaufrette/Adapter/Cache.php | 46 +++---- src/Gaufrette/Adapter/ChecksumCalculator.php | 4 +- src/Gaufrette/Adapter/DoctrineDbal.php | 39 +++--- src/Gaufrette/Adapter/Dropbox.php | 24 ++-- src/Gaufrette/Adapter/FileFactory.php | 4 +- src/Gaufrette/Adapter/Ftp.php | 120 +++++++++--------- src/Gaufrette/Adapter/GoogleCloudStorage.php | 15 +-- src/Gaufrette/Adapter/GridFS.php | 40 +++--- src/Gaufrette/Adapter/InMemory.php | 41 +++--- src/Gaufrette/Adapter/LazyOpenCloud.php | 8 +- src/Gaufrette/Adapter/ListKeysAware.php | 7 +- src/Gaufrette/Adapter/Local.php | 56 ++++---- src/Gaufrette/Adapter/MetadataSupporter.php | 5 +- src/Gaufrette/Adapter/MimeTypeProvider.php | 4 +- src/Gaufrette/Adapter/MogileFS.php | 82 ++++++------ src/Gaufrette/Adapter/OpenCloud.php | 53 ++++---- .../ObjectStoreFactory.php | 8 +- .../ObjectStoreFactoryInterface.php | 2 +- src/Gaufrette/Adapter/SafeLocal.php | 7 +- src/Gaufrette/Adapter/Sftp.php | 42 +++--- src/Gaufrette/Adapter/SizeCalculator.php | 4 +- src/Gaufrette/Adapter/StreamFactory.php | 4 +- src/Gaufrette/Adapter/Zip.php | 25 ++-- src/Gaufrette/Exception.php | 2 +- src/Gaufrette/Exception/FileAlreadyExists.php | 2 +- src/Gaufrette/Exception/FileNotFound.php | 2 +- src/Gaufrette/Exception/UnexpectedFile.php | 2 +- src/Gaufrette/File.php | 54 ++++---- src/Gaufrette/Filesystem.php | 85 +++++++------ src/Gaufrette/FilesystemMap.php | 26 ++-- src/Gaufrette/Stream.php | 51 ++++---- src/Gaufrette/Stream/InMemoryBuffer.php | 53 ++++---- src/Gaufrette/Stream/Local.php | 30 ++--- src/Gaufrette/StreamMode.php | 40 +++--- src/Gaufrette/StreamWrapper.php | 52 ++++---- src/Gaufrette/Util/Checksum.php | 6 +- src/Gaufrette/Util/Path.php | 19 ++- src/Gaufrette/Util/Size.php | 8 +- 46 files changed, 687 insertions(+), 663 deletions(-) diff --git a/src/Gaufrette/Adapter.php b/src/Gaufrette/Adapter.php index a77f32560..c6c4a95ba 100644 --- a/src/Gaufrette/Adapter.php +++ b/src/Gaufrette/Adapter.php @@ -3,7 +3,7 @@ namespace Gaufrette; /** - * Interface for the filesystem adapters + * Interface for the filesystem adapters. * * @author Antoine Hérault * @author Leszek Prabucki @@ -11,74 +11,74 @@ interface Adapter { /** - * Reads the content of the file + * Reads the content of the file. * * @param string $key * - * @return string|boolean if cannot read content + * @return string|bool if cannot read content */ public function read($key); /** - * Writes the given content into the file + * Writes the given content into the file. * * @param string $key * @param string $content * - * @return integer|boolean The number of bytes that were written into the file + * @return int|bool The number of bytes that were written into the file */ public function write($key, $content); /** - * Indicates whether the file exists + * Indicates whether the file exists. * * @param string $key * - * @return boolean + * @return bool */ public function exists($key); /** - * Returns an array of all keys (files and directories) + * Returns an array of all keys (files and directories). * * @return array */ public function keys(); /** - * Returns the last modified time + * Returns the last modified time. * * @param string $key * - * @return integer|boolean An UNIX like timestamp or false + * @return int|bool An UNIX like timestamp or false */ public function mtime($key); /** - * Deletes the file + * Deletes the file. * * @param string $key * - * @return boolean + * @return bool */ public function delete($key); /** - * Renames a file + * Renames a file. * * @param string $sourceKey * @param string $targetKey * - * @return boolean + * @return bool */ public function rename($sourceKey, $targetKey); /** - * Check if key is directory + * Check if key is directory. * * @param string $key * - * @return boolean + * @return bool */ public function isDirectory($key); } diff --git a/src/Gaufrette/Adapter/AclAwareAmazonS3.php b/src/Gaufrette/Adapter/AclAwareAmazonS3.php index ab0fdaccf..6f7ee361e 100644 --- a/src/Gaufrette/Adapter/AclAwareAmazonS3.php +++ b/src/Gaufrette/Adapter/AclAwareAmazonS3.php @@ -2,17 +2,16 @@ namespace Gaufrette\Adapter; -use \AmazonS3 as AmazonClient; +use AmazonS3 as AmazonClient; use Gaufrette\Adapter; /** - * Makes the AmazonS3 adapter ACL aware. Uses the AWS SDK for PHP v1.x + * Makes the AmazonS3 adapter ACL aware. Uses the AWS SDK for PHP v1.x. * * See the AwsS3 adapter for using the AWS SDK for PHP v2.x. There is * no distinction in the AwsS3 adapter between an ACL aware adapter * and regular adapter. * - * @package Gaufrette * @author Johannes M. Schmitt */ class AclAwareAmazonS3 implements Adapter, @@ -69,7 +68,7 @@ public function setUsers(array $users) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function read($key) { @@ -77,7 +76,7 @@ public function read($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function rename($key, $new) { @@ -95,7 +94,7 @@ public function rename($key, $new) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function write($key, $content) { @@ -113,7 +112,7 @@ public function write($key, $content) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function exists($key) { @@ -121,7 +120,7 @@ public function exists($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function mtime($key) { @@ -129,7 +128,7 @@ public function mtime($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function keys() { @@ -137,7 +136,7 @@ public function keys() } /** - * {@inheritDoc} + * {@inheritdoc} */ public function delete($key) { @@ -145,7 +144,7 @@ public function delete($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function setMetadata($key, $metadata) { @@ -155,7 +154,7 @@ public function setMetadata($key, $metadata) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function getMetadata($key) { @@ -167,7 +166,7 @@ public function getMetadata($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function isDirectory($key) { diff --git a/src/Gaufrette/Adapter/AmazonS3.php b/src/Gaufrette/Adapter/AmazonS3.php index 8843b08bb..8b65b8c3b 100644 --- a/src/Gaufrette/Adapter/AmazonS3.php +++ b/src/Gaufrette/Adapter/AmazonS3.php @@ -2,7 +2,7 @@ namespace Gaufrette\Adapter; -use \AmazonS3 as AmazonClient; +use AmazonS3 as AmazonClient; use Gaufrette\Adapter; /** @@ -10,7 +10,6 @@ * * See the AwsS3 adapter for using the AWS SDK for PHP v2.x. * - * @package Gaufrette * @author Antoine Hérault * @author Leszek Prabucki */ @@ -26,7 +25,7 @@ class AmazonS3 implements Adapter, public function __construct(AmazonClient $service, $bucket, $options = array()) { $this->service = $service; - $this->bucket = $bucket; + $this->bucket = $bucket; $this->options = array_replace_recursive( array('directory' => '', 'create' => false, 'region' => $service->hostname, 'acl' => AmazonClient::ACL_PUBLIC), $options @@ -34,7 +33,7 @@ public function __construct(AmazonClient $service, $bucket, $options = array()) } /** - * Set the acl used when writing files + * Set the acl used when writing files. * * @param string $acl */ @@ -44,7 +43,7 @@ public function setAcl($acl) } /** - * Get the acl used when writing files + * Get the acl used when writing files. * * @return string */ @@ -54,7 +53,7 @@ public function getAcl() } /** - * Set the base directory the user will have access to + * Set the base directory the user will have access to. * * @param string $directory */ @@ -64,7 +63,7 @@ public function setDirectory($directory) } /** - * Get the directory the user has access to + * Get the directory the user has access to. * * @return string */ @@ -74,7 +73,7 @@ public function getDirectory() } /** - * {@inheritDoc} + * {@inheritdoc} */ public function setMetadata($key, $metadata) { @@ -84,7 +83,7 @@ public function setMetadata($key, $metadata) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function getMetadata($key) { @@ -94,7 +93,7 @@ public function getMetadata($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function read($key) { @@ -114,7 +113,7 @@ public function read($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function rename($sourceKey, $targetKey) { @@ -122,12 +121,12 @@ public function rename($sourceKey, $targetKey) $response = $this->service->copy_object( array( // source - 'bucket' => $this->bucket, - 'filename' => $this->computePath($sourceKey) + 'bucket' => $this->bucket, + 'filename' => $this->computePath($sourceKey), ), array( // target - 'bucket' => $this->bucket, - 'filename' => $this->computePath($targetKey) + 'bucket' => $this->bucket, + 'filename' => $this->computePath($targetKey), ), $this->getMetadata($sourceKey) ); @@ -136,14 +135,14 @@ public function rename($sourceKey, $targetKey) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function write($key, $content) { $this->ensureBucketExists(); $opt = array_replace_recursive( - array('acl' => $this->options['acl']), + array('acl' => $this->options['acl']), $this->getMetadata($key), array('body' => $content) ); @@ -158,11 +157,11 @@ public function write($key, $content) return false; }; - return intval($response->header["x-aws-requestheaders"]["Content-Length"]); + return intval($response->header['x-aws-requestheaders']['Content-Length']); } /** - * {@inheritDoc} + * {@inheritdoc} */ public function exists($key) { @@ -175,7 +174,7 @@ public function exists($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function mtime($key) { @@ -191,7 +190,7 @@ public function mtime($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function keys() { @@ -212,7 +211,7 @@ public function keys() } /** - * {@inheritDoc} + * {@inheritdoc} */ public function delete($key) { @@ -228,7 +227,7 @@ public function delete($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function isDirectory($key) { @@ -242,10 +241,10 @@ public function isDirectory($key) /** * Ensures the specified bucket exists. If the bucket does not exists * and the create parameter is set to true, it will try to create the - * bucket + * bucket. * * @throws \RuntimeException if the bucket does not exists or could not be - * created + * created */ private function ensureBucketExists() { @@ -286,7 +285,7 @@ private function ensureBucketExists() } /** - * Computes the path for the specified key taking the bucket in account + * Computes the path for the specified key taking the bucket in account. * * @param string $key The key for which to compute the path * diff --git a/src/Gaufrette/Adapter/Apc.php b/src/Gaufrette/Adapter/Apc.php index 4bd280f52..f7271bf9e 100644 --- a/src/Gaufrette/Adapter/Apc.php +++ b/src/Gaufrette/Adapter/Apc.php @@ -6,7 +6,7 @@ use Gaufrette\Util; /** - * Apc adapter, a non-persistent adapter for when this sort of thing is appropriate + * Apc adapter, a non-persistent adapter for when this sort of thing is appropriate. * * @author Alexander Deruwe * @author Antoine Hérault @@ -19,8 +19,9 @@ class Apc implements Adapter /** * @throws \RuntimeException - * @param string $prefix to avoid conflicts between filesystems - * @param int $ttl time to live, default is 0 + * + * @param string $prefix to avoid conflicts between filesystems + * @param int $ttl time to live, default is 0 */ public function __construct($prefix, $ttl = 0) { @@ -33,7 +34,7 @@ public function __construct($prefix, $ttl = 0) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function read($key) { @@ -41,7 +42,7 @@ public function read($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function write($key, $content, array $metadata = null) { @@ -55,7 +56,7 @@ public function write($key, $content, array $metadata = null) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function exists($key) { @@ -63,7 +64,7 @@ public function exists($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function keys() { @@ -84,7 +85,7 @@ public function keys() } /** - * {@inheritDoc} + * {@inheritdoc} */ public function mtime($key) { @@ -94,7 +95,7 @@ public function mtime($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function delete($key) { @@ -102,19 +103,19 @@ public function delete($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function rename($sourceKey, $targetKey) { // TODO: this probably allows for race conditions... - $written = $this->write($targetKey, $this->read($sourceKey)); + $written = $this->write($targetKey, $this->read($sourceKey)); $deleted = $this->delete($sourceKey); return $written && $deleted; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function isDirectory($key) { @@ -122,21 +123,22 @@ public function isDirectory($key) } /** - * Computes the path for the given key + * Computes the path for the given key. + * + * @param string $key * - * @param string $key * @return string */ public function computePath($key) { - return $this->prefix . $key; + return $this->prefix.$key; } /** - * @param string $key - by default '' - * @param integer $format - by default APC_ITER_NONE - * @return \APCIterator + * @param string $key - by default '' + * @param int $format - by default APC_ITER_NONE * + * @return \APCIterator */ protected function getCachedKeysIterator($key = '', $format = APC_ITER_NONE) { diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index 5879108dd..27598020e 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -6,9 +6,8 @@ use Aws\S3\S3Client; /** - * Amazon S3 adapter using the AWS SDK for PHP v2.x + * Amazon S3 adapter using the AWS SDK for PHP v2.x. * - * @package Gaufrette * @author Michael Dowling */ class AwsS3 implements Adapter, @@ -40,14 +39,15 @@ public function __construct(S3Client $service, $bucket, array $options = array() } /** - * Gets the publicly accessible URL of an Amazon S3 object + * Gets the publicly accessible URL of an Amazon S3 object. * * @param string $key Object key * @param array $options Associative array of options used to buld the URL - * - expires: The time at which the URL should expire - * represented as a UNIX timestamp - * - Any options available in the Amazon S3 GetObject - * operation may be specified. + * - expires: The time at which the URL should expire + * represented as a UNIX timestamp + * - Any options available in the Amazon S3 GetObject + * operation may be specified. + * * @return string */ public function getUrl($key, array $options = array()) @@ -61,7 +61,7 @@ public function getUrl($key, array $options = array()) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function setMetadata($key, $metadata) { @@ -75,7 +75,7 @@ public function setMetadata($key, $metadata) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function getMetadata($key) { @@ -83,7 +83,7 @@ public function getMetadata($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function read($key) { @@ -98,7 +98,7 @@ public function read($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function rename($sourceKey, $targetKey) { @@ -112,6 +112,7 @@ public function rename($sourceKey, $targetKey) try { $this->service->copyObject(array_merge($options, $this->getMetadata($targetKey))); + return $this->delete($sourceKey); } catch (\Exception $e) { return false; @@ -119,14 +120,14 @@ public function rename($sourceKey, $targetKey) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function write($key, $content) { $this->ensureBucketExists(); $options = $this->getOptions($key, array('Body' => $content)); - /** + /* * If the ContentType was not already set in the metadata, then we autodetect * it to prevent everything being served up as binary/octet-stream. */ @@ -139,6 +140,7 @@ public function write($key, $content) try { $this->service->putObject($options); + return strlen($content); } catch (\Exception $e) { return false; @@ -146,7 +148,7 @@ public function write($key, $content) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function exists($key) { @@ -154,12 +156,13 @@ public function exists($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function mtime($key) { try { $result = $this->service->headObject($this->getOptions($key)); + return strtotime($result['LastModified']); } catch (\Exception $e) { return false; @@ -167,12 +170,13 @@ public function mtime($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function size($key) { try { $result = $this->service->headObject($this->getOptions($key)); + return $result['ContentLength']; } catch (\Exception $e) { return false; @@ -180,7 +184,7 @@ public function size($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function keys() { @@ -209,12 +213,13 @@ public function listKeys($prefix = '') } /** - * {@inheritDoc} + * {@inheritdoc} */ public function delete($key) { try { $this->service->deleteObject($this->getOptions($key)); + return true; } catch (\Exception $e) { return false; @@ -222,14 +227,14 @@ public function delete($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function isDirectory($key) { $result = $this->service->listObjects(array( - 'Bucket' => $this->bucket, - 'Prefix' => rtrim($this->computePath($key), '/') . '/', - 'MaxKeys' => 1 + 'Bucket' => $this->bucket, + 'Prefix' => rtrim($this->computePath($key), '/').'/', + 'MaxKeys' => 1, )); return count($result['Contents']) > 0; @@ -242,7 +247,7 @@ public function isDirectory($key) * client object. * * @throws \RuntimeException if the bucket does not exists or could not be - * created + * created */ protected function ensureBucketExists() { @@ -278,7 +283,7 @@ protected function getOptions($key, array $options = array()) $options['Bucket'] = $this->bucket; $options['Key'] = $this->computePath($key); - /** + /* * Merge global options for adapter, which are set in the constructor, with metadata. * Metadata will override global options. */ @@ -297,7 +302,7 @@ protected function computePath($key) } /** - * Computes the key from the specified path + * Computes the key from the specified path. * * @param string $path * diff --git a/src/Gaufrette/Adapter/AzureBlobStorage.php b/src/Gaufrette/Adapter/AzureBlobStorage.php index 6f60f4bd7..ed3548c80 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage.php @@ -5,7 +5,6 @@ use Gaufrette\Adapter; use Gaufrette\Util; use Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface; - use WindowsAzure\Blob\Models\CreateBlobOptions; use WindowsAzure\Blob\Models\CreateContainerOptions; use WindowsAzure\Blob\Models\DeleteContainerOptions; @@ -13,7 +12,7 @@ use WindowsAzure\Common\ServiceException; /** - * Microsoft Azure Blob Storage adapter + * Microsoft Azure Blob Storage adapter. * * @author Luciano Mammino * @author Paweł Czyżewski @@ -22,28 +21,28 @@ class AzureBlobStorage implements Adapter, MetadataSupporter { /** - * Error constants + * Error constants. */ const ERROR_CONTAINER_ALREADY_EXISTS = 'ContainerAlreadyExists'; const ERROR_CONTAINER_NOT_FOUND = 'ContainerNotFound'; /** - * @var AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory + * @var AzureBlobStorage\BlobProxyFactoryInterface */ protected $blobProxyFactory; /** - * @var string $containerName + * @var string */ protected $containerName; /** - * @var bool $detectContentType + * @var bool */ protected $detectContentType; /** - * @var \WindowsAzure\Blob\Internal\IBlob $blobProxy + * @var \WindowsAzure\Blob\Internal\IBlob */ protected $blobProxy; @@ -64,11 +63,12 @@ public function __construct(BlobProxyFactoryInterface $blobProxyFactory, $contai } /** - * Creates a new container + * Creates a new container. + * + * @param string $containerName + * @param \WindowsAzure\Blob\Models\CreateContainerOptions $options * - * @param string $containerName - * @param \WindowsAzure\Blob\Models\CreateContainerOptions $options - * @throws \RuntimeException if cannot create the container + * @throws \RuntimeException if cannot create the container */ public function createContainer($containerName, CreateContainerOptions $options = null) { @@ -91,11 +91,12 @@ public function createContainer($containerName, CreateContainerOptions $options } /** - * Deletes a container + * Deletes a container. + * + * @param string $containerName + * @param DeleteContainerOptions $options * - * @param string $containerName - * @param DeleteContainerOptions $options - * @throws \RuntimeException if cannot delete the container + * @throws \RuntimeException if cannot delete the container */ public function deleteContainer($containerName, DeleteContainerOptions $options = null) { @@ -118,7 +119,7 @@ public function deleteContainer($containerName, DeleteContainerOptions $options } /** - * {@inheritDoc} + * {@inheritdoc} */ public function read($key) { @@ -136,7 +137,7 @@ public function read($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function write($key, $content) { @@ -162,7 +163,7 @@ public function write($key, $content) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function exists($key) { @@ -196,7 +197,7 @@ public function exists($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function keys() { @@ -206,7 +207,7 @@ public function keys() $blobList = $this->blobProxy->listBlobs($this->containerName); return array_map( - function($blob) { + function ($blob) { return $blob->getName(); }, $blobList->getBlobs() @@ -225,7 +226,7 @@ function($blob) { } /** - * {@inheritDoc} + * {@inheritdoc} */ public function mtime($key) { @@ -243,7 +244,7 @@ public function mtime($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function delete($key) { @@ -261,7 +262,7 @@ public function delete($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function rename($sourceKey, $targetKey) { @@ -280,7 +281,7 @@ public function rename($sourceKey, $targetKey) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function isDirectory($key) { @@ -289,7 +290,7 @@ public function isDirectory($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function setMetadata($key, $content) { @@ -311,7 +312,7 @@ public function setMetadata($key, $content) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function getMetadata($key) { @@ -335,7 +336,7 @@ public function getMetadata($key) } /** - * Lazy initialization, automatically called when some method is called after construction + * Lazy initialization, automatically called when some method is called after construction. */ protected function init() { @@ -345,10 +346,11 @@ protected function init() } /** - * Throws a runtime exception if a give ServiceException derived from a "container not found" error + * Throws a runtime exception if a give ServiceException derived from a "container not found" error. + * + * @param ServiceException $exception + * @param string $action * - * @param ServiceException $exception - * @param string $action * @throws \RuntimeException */ protected function failIfContainerNotFound(ServiceException $exception, $action) @@ -365,9 +367,10 @@ protected function failIfContainerNotFound(ServiceException $exception, $action) } /** - * Extracts the error code from a service exception + * Extracts the error code from a service exception. + * + * @param ServiceException $exception * - * @param ServiceException $exception * @return string */ protected function getErrorCodeFromServiceException(ServiceException $exception) diff --git a/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php b/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php index dbf44716d..2ab9d8475 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php @@ -5,14 +5,14 @@ use WindowsAzure\Common\ServicesBuilder; /** - * Basic implementation for a Blob proxy factory + * Basic implementation for a Blob proxy factory. * * @author Luciano Mammino */ class BlobProxyFactory implements BlobProxyFactoryInterface { /** - * @var string $connectionString + * @var string */ protected $connectionString; @@ -25,7 +25,7 @@ public function __construct($connectionString) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function create() { diff --git a/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactoryInterface.php b/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactoryInterface.php index 8424fe742..379d935d8 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactoryInterface.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactoryInterface.php @@ -3,14 +3,14 @@ namespace Gaufrette\Adapter\AzureBlobStorage; /** - * Interface to define Blob proxy factories + * Interface to define Blob proxy factories. * * @author Luciano Mammino */ interface BlobProxyFactoryInterface { /** - * Creates a new instance of the Blob proxy + * Creates a new instance of the Blob proxy. * * @return \WindowsAzure\Blob\Internal\IBlob */ diff --git a/src/Gaufrette/Adapter/Cache.php b/src/Gaufrette/Adapter/Cache.php index 7d1e8f788..f54f73656 100644 --- a/src/Gaufrette/Adapter/Cache.php +++ b/src/Gaufrette/Adapter/Cache.php @@ -7,9 +7,8 @@ use Gaufrette\Adapter\InMemory as InMemoryAdapter; /** - * Cache adapter + * Cache adapter. * - * @package Gaufrette * @author Antoine Hérault */ class Cache implements Adapter, @@ -26,7 +25,7 @@ class Cache implements Adapter, protected $cache; /** - * @var integer + * @var int */ protected $ttl; @@ -38,7 +37,7 @@ class Cache implements Adapter, /** * @param Adapter $source The source adapter that must be cached * @param Adapter $cache The adapter used to cache the source - * @param integer $ttl Time to live of a cached file + * @param int $ttl Time to live of a cached file * @param Adapter $serializeCache The adapter used to cache serializations */ public function __construct(Adapter $source, Adapter $cache, $ttl = 0, Adapter $serializeCache = null) @@ -54,9 +53,9 @@ public function __construct(Adapter $source, Adapter $cache, $ttl = 0, Adapter $ } /** - * Returns the time to live of the cache + * Returns the time to live of the cache. * - * @return integer $ttl + * @return int $ttl */ public function getTtl() { @@ -64,9 +63,9 @@ public function getTtl() } /** - * Defines the time to live of the cache + * Defines the time to live of the cache. * - * @param integer $ttl + * @param int $ttl */ public function setTtl($ttl) { @@ -74,7 +73,7 @@ public function setTtl($ttl) } /** - * {@InheritDoc} + * {@inheritdoc} */ public function read($key) { @@ -89,7 +88,7 @@ public function read($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function rename($key, $new) { @@ -97,7 +96,7 @@ public function rename($key, $new) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function write($key, $content, array $metadata = null) { @@ -117,18 +116,19 @@ public function write($key, $content, array $metadata = null) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function exists($key) { if ($this->needsReload($key)) { return $this->source->exists($key); } + return $this->cache->exists($key); } /** - * {@inheritDoc} + * {@inheritdoc} */ public function mtime($key) { @@ -136,7 +136,7 @@ public function mtime($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function keys() { @@ -153,7 +153,7 @@ public function keys() } /** - * {@inheritDoc} + * {@inheritdoc} */ public function delete($key) { @@ -161,7 +161,7 @@ public function delete($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function isDirectory($key) { @@ -169,7 +169,7 @@ public function isDirectory($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function setMetadata($key, $metadata) { @@ -183,7 +183,7 @@ public function setMetadata($key, $metadata) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function getMetadata($key) { @@ -195,7 +195,7 @@ public function getMetadata($key) } /** - * Indicates whether the cache for the specified key needs to be reloaded + * Indicates whether the cache for the specified key needs to be reloaded. * * @param string $key */ @@ -212,14 +212,15 @@ public function needsReload($key) $dateSource = $this->source->mtime($key); $needsReload = $dateCache < $dateSource; } - } catch (\RuntimeException $e) { } + } catch (\RuntimeException $e) { + } } return $needsReload; } /** - * Indicates whether the serialized cache file needs to be rebuild + * Indicates whether the serialized cache file needs to be rebuild. * * @param string $cacheFile */ @@ -230,7 +231,8 @@ public function needsRebuild($cacheFile) if ($this->serializeCache->exists($cacheFile)) { try { $needsRebuild = time() - $this->ttl >= $this->serializeCache->mtime($cacheFile); - } catch (\RuntimeException $e) { } + } catch (\RuntimeException $e) { + } } return $needsRebuild; diff --git a/src/Gaufrette/Adapter/ChecksumCalculator.php b/src/Gaufrette/Adapter/ChecksumCalculator.php index 53ea990e9..46ca2b7db 100644 --- a/src/Gaufrette/Adapter/ChecksumCalculator.php +++ b/src/Gaufrette/Adapter/ChecksumCalculator.php @@ -3,14 +3,14 @@ namespace Gaufrette\Adapter; /** - * Interface which add checksum calculation support to adapter + * Interface which add checksum calculation support to adapter. * * @author Leszek Prabucki */ interface ChecksumCalculator { /** - * Returns the checksum of the specified key + * Returns the checksum of the specified key. * * @param string $key * diff --git a/src/Gaufrette/Adapter/DoctrineDbal.php b/src/Gaufrette/Adapter/DoctrineDbal.php index 3d54d154b..d21deb6be 100644 --- a/src/Gaufrette/Adapter/DoctrineDbal.php +++ b/src/Gaufrette/Adapter/DoctrineDbal.php @@ -4,11 +4,10 @@ use Gaufrette\Adapter; use Gaufrette\Util; - use Doctrine\DBAL\Connection; /** - * Doctrine DBAL adapter + * Doctrine DBAL adapter. * * @author Markus Bachmann * @author Antoine Hérault @@ -21,9 +20,9 @@ class DoctrineDbal implements Adapter, protected $connection; protected $table; protected $columns = array( - 'key' => 'key', - 'content' => 'content', - 'mtime' => 'mtime', + 'key' => 'key', + 'content' => 'content', + 'mtime' => 'mtime', 'checksum' => 'checksum', ); @@ -35,12 +34,12 @@ class DoctrineDbal implements Adapter, public function __construct(Connection $connection, $table, array $columns = array()) { $this->connection = $connection; - $this->table = $table; - $this->columns = array_replace($this->columns, $columns); + $this->table = $table; + $this->columns = array_replace($this->columns, $columns); } /** - * {@inheritDoc} + * {@inheritdoc} */ public function keys() { @@ -55,7 +54,7 @@ public function keys() } /** - * {@inheritDoc} + * {@inheritdoc} */ public function rename($sourceKey, $targetKey) { @@ -67,7 +66,7 @@ public function rename($sourceKey, $targetKey) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function mtime($key) { @@ -75,7 +74,7 @@ public function mtime($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function checksum($key) { @@ -83,7 +82,7 @@ public function checksum($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function exists($key) { @@ -99,7 +98,7 @@ public function exists($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function read($key) { @@ -107,7 +106,7 @@ public function read($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function delete($key) { @@ -118,13 +117,13 @@ public function delete($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function write($key, $content) { $values = array( - $this->getQuotedColumn('content') => $content, - $this->getQuotedColumn('mtime') => time(), + $this->getQuotedColumn('content') => $content, + $this->getQuotedColumn('mtime') => time(), $this->getQuotedColumn('checksum') => Util\Checksum::fromContent($content), ); @@ -143,7 +142,7 @@ public function write($key, $content) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function isDirectory($key) { @@ -166,7 +165,7 @@ private function getColumnValue($key, $column) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function listKeys($prefix = '') { @@ -187,7 +186,7 @@ public function listKeys($prefix = '') 'keys' => array_map(function ($value) { return $value['_key']; }, - $keys) + $keys), ); } diff --git a/src/Gaufrette/Adapter/Dropbox.php b/src/Gaufrette/Adapter/Dropbox.php index 18ab253ee..463a05ead 100644 --- a/src/Gaufrette/Adapter/Dropbox.php +++ b/src/Gaufrette/Adapter/Dropbox.php @@ -5,11 +5,11 @@ use Gaufrette\Adapter; use Gaufrette\Util; use Gaufrette\Exception; -use \Dropbox_API as DropboxApi; -use \Dropbox_Exception_NotFound as DropboxNotFoundException; +use Dropbox_API as DropboxApi; +use Dropbox_Exception_NotFound as DropboxNotFoundException; /** - * Dropbox adapter + * Dropbox adapter. * * @author Markus Bachmann * @author Antoine Hérault @@ -28,7 +28,7 @@ public function __construct(DropboxApi $client) } /** - * {@inheritDoc} + * {@inheritdoc} * * @throws \Dropbox_Exception_Forbidden * @throws \Dropbox_Exception_OverQuota @@ -44,7 +44,7 @@ public function read($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function isDirectory($key) { @@ -58,7 +58,7 @@ public function isDirectory($key) } /** - * {@inheritDoc} + * {@inheritdoc} * * @throws \Dropbox_Exception */ @@ -82,7 +82,7 @@ public function write($key, $content) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function delete($key) { @@ -96,7 +96,7 @@ public function delete($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function rename($sourceKey, $targetKey) { @@ -110,7 +110,7 @@ public function rename($sourceKey, $targetKey) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function mtime($key) { @@ -124,12 +124,12 @@ public function mtime($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function keys() { $metadata = $this->client->getMetaData('/', true); - if (! isset($metadata['contents'])) { + if (!isset($metadata['contents'])) { return array(); } @@ -147,7 +147,7 @@ public function keys() } /** - * {@inheritDoc} + * {@inheritdoc} */ public function exists($key) { diff --git a/src/Gaufrette/Adapter/FileFactory.php b/src/Gaufrette/Adapter/FileFactory.php index 0eb93492e..b11c2b59e 100644 --- a/src/Gaufrette/Adapter/FileFactory.php +++ b/src/Gaufrette/Adapter/FileFactory.php @@ -5,14 +5,14 @@ use Gaufrette\Filesystem; /** - * Interface for the file creation class + * Interface for the file creation class. * * @author Leszek Prabucki */ interface FileFactory { /** - * Creates a new File instance and returns it + * Creates a new File instance and returns it. * * @param string $key * @param Filesystem $filesystem diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index f00a14408..b7e24036d 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -5,12 +5,10 @@ use Gaufrette\Adapter; use Gaufrette\File; use Gaufrette\Filesystem; -use Gaufrette\Exception; /** - * Ftp adapter + * Ftp adapter. * - * @package Gaufrette * @author Antoine Hérault */ class Ftp implements Adapter, @@ -41,18 +39,18 @@ public function __construct($directory, $host, $options = array()) } $this->directory = (string) $directory; - $this->host = $host; - $this->port = isset($options['port']) ? $options['port'] : 21; - $this->username = isset($options['username']) ? $options['username'] : null; - $this->password = isset($options['password']) ? $options['password'] : null; - $this->passive = isset($options['passive']) ? $options['passive'] : false; - $this->create = isset($options['create']) ? $options['create'] : false; - $this->mode = isset($options['mode']) ? $options['mode'] : FTP_BINARY; - $this->ssl = isset($options['ssl']) ? $options['ssl'] : false; + $this->host = $host; + $this->port = isset($options['port']) ? $options['port'] : 21; + $this->username = isset($options['username']) ? $options['username'] : null; + $this->password = isset($options['password']) ? $options['password'] : null; + $this->passive = isset($options['passive']) ? $options['passive'] : false; + $this->create = isset($options['create']) ? $options['create'] : false; + $this->mode = isset($options['mode']) ? $options['mode'] : FTP_BINARY; + $this->ssl = isset($options['ssl']) ? $options['ssl'] : false; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function read($key) { @@ -72,7 +70,7 @@ public function read($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function write($key, $content) { @@ -99,7 +97,7 @@ public function write($key, $content) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function rename($sourceKey, $targetKey) { @@ -114,7 +112,7 @@ public function rename($sourceKey, $targetKey) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function exists($key) { @@ -138,7 +136,7 @@ public function exists($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function keys() { @@ -150,7 +148,7 @@ public function keys() } /** - * {@inheritDoc} + * {@inheritdoc} */ public function listKeys($prefix = '') { @@ -179,7 +177,7 @@ public function listKeys($prefix = '') } /** - * {@inheritDoc} + * {@inheritdoc} */ public function mtime($key) { @@ -196,7 +194,7 @@ public function mtime($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function delete($key) { @@ -210,7 +208,7 @@ public function delete($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function isDirectory($key) { @@ -234,21 +232,20 @@ public function listDirectory($directory = '') $directory = preg_replace('/^[\/]*([^\/].*)$/', '/$1', $directory); $items = $this->parseRawlist( - ftp_rawlist($this->getConnection(), '-al ' . $this->directory . $directory ) ? : array() + ftp_rawlist($this->getConnection(), '-al '.$this->directory.$directory) ?: array() ); $fileData = $dirs = array(); foreach ($items as $itemData) { - if ('..' === $itemData['name'] || '.' === $itemData['name']) { continue; } $item = array( - 'name' => $itemData['name'], - 'path' => trim(($directory ? $directory . '/' : '') . $itemData['name'], '/'), - 'time' => $itemData['time'], - 'size' => $itemData['size'], + 'name' => $itemData['name'], + 'path' => trim(($directory ? $directory.'/' : '').$itemData['name'], '/'), + 'time' => $itemData['time'], + 'size' => $itemData['size'], ); if ('-' === substr($itemData['perms'], 0, 1)) { @@ -261,13 +258,13 @@ public function listDirectory($directory = '') $this->fileData = array_merge($fileData, $this->fileData); return array( - 'keys' => array_keys($fileData), - 'dirs' => $dirs + 'keys' => array_keys($fileData), + 'dirs' => $dirs, ); } /** - * {@inheritDoc} + * {@inheritdoc} */ public function createFile($key, Filesystem $filesystem) { @@ -292,11 +289,11 @@ public function createFile($key, Filesystem $filesystem) /** * Ensures the specified directory exists. If it does not, and the create - * parameter is set to TRUE, it tries to create it + * parameter is set to TRUE, it tries to create it. * - * @param string $directory - * @param boolean $create Whether to create the directory if it does not - * exist + * @param string $directory + * @param bool $create Whether to create the directory if it does not + * exist * * @throws RuntimeException if the directory does not exist and could not * be created @@ -313,7 +310,7 @@ protected function ensureDirectoryExists($directory, $create = false) } /** - * Creates the specified directory and its parent directories + * Creates the specified directory and its parent directories. * * @param string $directory Directory to create * @@ -335,8 +332,9 @@ protected function createDirectory($directory) } /** - * @param string $directory - full directory path - * @return boolean + * @param string $directory - full directory path + * + * @return bool */ private function isDir($directory) { @@ -358,13 +356,13 @@ private function fetchKeys($directory = '', $onlyKeys = true) { $directory = preg_replace('/^[\/]*([^\/].*)$/', '/$1', $directory); - $lines = ftp_rawlist($this->getConnection(), '-alR '. $this->directory . $directory); + $lines = ftp_rawlist($this->getConnection(), '-alR '.$this->directory.$directory); if (false === $lines) { return array('keys' => array(), 'dirs' => array()); } - $regexDir = '/'.preg_quote($this->directory . $directory, '/').'\/?(.+):$/u'; + $regexDir = '/'.preg_quote($this->directory.$directory, '/').'\/?(.+):$/u'; $regexItem = '/^(?:([d\-\d])\S+)\s+\S+(?:(?:\s+\S+){5})?\s+(\S+)\s+(.+?)$/'; $prevLine = null; @@ -378,7 +376,7 @@ private function fetchKeys($directory = '', $onlyKeys = true) if ($onlyKeys) { $keys = array( 'keys' => array_merge($keys['keys'], $keys['dirs']), - 'dirs' => array() + 'dirs' => array(), ); } } elseif (preg_match($regexItem, $line, $tokens)) { @@ -388,7 +386,7 @@ private function fetchKeys($directory = '', $onlyKeys = true) continue; } - $path = ltrim($directory . '/' . $name, '/'); + $path = ltrim($directory.'/'.$name, '/'); if ('d' === $tokens[1] || '

' === $tokens[2]) { $keys['dirs'][] = $path; @@ -403,7 +401,7 @@ private function fetchKeys($directory = '', $onlyKeys = true) if ($onlyKeys) { $keys = array( 'keys' => array_merge($keys['keys'], $keys['dirs']), - 'dirs' => array() + 'dirs' => array(), ); } @@ -415,7 +413,7 @@ private function fetchKeys($directory = '', $onlyKeys = true) } /** - * Parses the given raw list + * Parses the given raw list. * * @param array $rawlist * @@ -428,24 +426,24 @@ private function parseRawlist(array $rawlist) $infos = preg_split("/[\s]+/", $line, 9); if ($this->isLinuxListing($infos)) { - $infos[7] = (strrpos($infos[7], ':') != 2 ) ? ($infos[7] . ' 00:00') : (date('Y') . ' ' . $infos[7]); + $infos[7] = (strrpos($infos[7], ':') != 2) ? ($infos[7].' 00:00') : (date('Y').' '.$infos[7]); if ('total' !== $infos[0]) { $parsed[] = array( 'perms' => $infos[0], - 'num' => $infos[1], - 'size' => $infos[4], - 'time' => strtotime($infos[5] . ' ' . $infos[6] . '. ' . $infos[7]), - 'name' => $infos[8] + 'num' => $infos[1], + 'size' => $infos[4], + 'time' => strtotime($infos[5].' '.$infos[6].'. '.$infos[7]), + 'name' => $infos[8], ); } } else { $isDir = (boolean) ('' === $infos[2]); $parsed[] = array( 'perms' => $isDir ? 'd' : '-', - 'num' => '', - 'size' => $isDir ? '' : $infos[2], - 'time' => strtotime($infos[0] . ' ' . $infos[1]), - 'name' => $infos[3] + 'num' => '', + 'size' => $isDir ? '' : $infos[2], + 'time' => strtotime($infos[0].' '.$infos[1]), + 'name' => $infos[3], ); } } @@ -454,19 +452,19 @@ private function parseRawlist(array $rawlist) } /** - * Computes the path for the given key + * Computes the path for the given key. * * @param string $key */ private function computePath($key) { - return rtrim($this->directory, '/') . '/' . $key; + return rtrim($this->directory, '/').'/'.$key; } /** - * Indicates whether the adapter has an open ftp connection + * Indicates whether the adapter has an open ftp connection. * - * @return boolean + * @return bool */ private function isConnected() { @@ -475,7 +473,7 @@ private function isConnected() /** * Returns an opened ftp connection resource. If the connection is not - * already opened, it open it before + * already opened, it open it before. * * @return resource The ftp connection */ @@ -489,7 +487,7 @@ private function getConnection() } /** - * Opens the adapter's ftp connection + * Opens the adapter's ftp connection. * * @throws RuntimeException if could not connect */ @@ -499,7 +497,7 @@ private function connect() if (!$this->ssl) { $this->connection = ftp_connect($this->host, $this->port); } else { - if(function_exists('ftp_ssl_connect')) { + if (function_exists('ftp_ssl_connect')) { $this->connection = ftp_ssl_connect($this->host, $this->port); } else { throw new \RuntimeException('This Server Has No SSL-FTP Available.'); @@ -509,8 +507,8 @@ private function connect() throw new \RuntimeException(sprintf('Could not connect to \'%s\' (port: %s).', $this->host, $this->port)); } - $username = $this->username ? : 'anonymous'; - $password = $this->password ? : ''; + $username = $this->username ?: 'anonymous'; + $password = $this->password ?: ''; // login ftp user if (!@ftp_login($this->connection, $username, $password)) { @@ -542,7 +540,7 @@ private function connect() } /** - * Closes the adapter's ftp connection + * Closes the adapter's ftp connection. */ private function close() { diff --git a/src/Gaufrette/Adapter/GoogleCloudStorage.php b/src/Gaufrette/Adapter/GoogleCloudStorage.php index d580db628..6eaf01364 100644 --- a/src/Gaufrette/Adapter/GoogleCloudStorage.php +++ b/src/Gaufrette/Adapter/GoogleCloudStorage.php @@ -5,9 +5,8 @@ use Gaufrette\Adapter; /** - * Google Cloud Storage adapter using the Google APIs Client Library for PHP + * Google Cloud Storage adapter using the Google APIs Client Library for PHP. * - * @package Gaufrette * @author Patrik Karisch */ class GoogleCloudStorage implements Adapter, @@ -119,10 +118,10 @@ public function write($key, $content) $metadata = $this->getMetadata($key); $options = array( 'uploadType' => 'multipart', - 'data' => $content + 'data' => $content, ); - /** + /* * If the ContentType was not already set in the metadata, then we autodetect * it to prevent everything being served up as application/octet-stream. */ @@ -165,8 +164,8 @@ public function write($key, $content) if ($this->options['acl'] == 'public') { $acl = new \Google_Service_Storage_ObjectAccessControl(); - $acl->setEntity("allUsers"); - $acl->setRole("READER"); + $acl->setEntity('allUsers'); + $acl->setRole('READER'); $this->service->objectAccessControls->insert($this->bucket, $path, $acl); } @@ -261,7 +260,7 @@ public function rename($sourceKey, $targetKey) */ public function isDirectory($key) { - if ($this->exists($key . '/')) { + if ($this->exists($key.'/')) { return true; } @@ -276,7 +275,7 @@ public function listKeys($prefix = '') $this->ensureBucketExists(); $options = array(); - if ((string)$prefix != '') { + if ((string) $prefix != '') { $options['prefix'] = $this->computePath($prefix); } elseif (!empty($this->options['directory'])) { $options['prefix'] = $this->options['directory']; diff --git a/src/Gaufrette/Adapter/GridFS.php b/src/Gaufrette/Adapter/GridFS.php index c39c1998a..c6b9a97b6 100644 --- a/src/Gaufrette/Adapter/GridFS.php +++ b/src/Gaufrette/Adapter/GridFS.php @@ -3,11 +3,11 @@ namespace Gaufrette\Adapter; use Gaufrette\Adapter; -use \MongoGridFS as MongoGridFs; -use \MongoDate; +use MongoGridFS as MongoGridFs; +use MongoDate; /** - * Adapter for the GridFS filesystem on MongoDB database + * Adapter for the GridFS filesystem on MongoDB database. * * @author Tomi Saarinen * @author Antoine Hérault @@ -30,7 +30,7 @@ public function __construct(MongoGridFs $gridFS) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function read($key) { @@ -40,7 +40,7 @@ public function read($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function write($key, $content) { @@ -49,14 +49,14 @@ public function write($key, $content) } $metadata = array_replace_recursive(array('date' => new MongoDate()), $this->getMetadata($key), array('filename' => $key)); - $id = $this->gridFS->storeBytes($content, $metadata); + $id = $this->gridFS->storeBytes($content, $metadata); $file = $this->gridFS->findOne(array('_id' => $id)); return $file->getSize(); } /** - * {@inheritDoc} + * {@inheritdoc} */ public function isDirectory($key) { @@ -64,7 +64,7 @@ public function isDirectory($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function rename($sourceKey, $targetKey) { @@ -75,7 +75,7 @@ public function rename($sourceKey, $targetKey) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function exists($key) { @@ -83,11 +83,11 @@ public function exists($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function keys() { - $keys = array(); + $keys = array(); $cursor = $this->gridFS->find(array(), array('filename')); foreach ($cursor as $file) { @@ -98,7 +98,7 @@ public function keys() } /** - * {@inheritDoc} + * {@inheritdoc} */ public function mtime($key) { @@ -108,7 +108,7 @@ public function mtime($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function checksum($key) { @@ -118,7 +118,7 @@ public function checksum($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function delete($key) { @@ -128,7 +128,7 @@ public function delete($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function setMetadata($key, $metadata) { @@ -136,7 +136,7 @@ public function setMetadata($key, $metadata) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function getMetadata($key) { @@ -149,7 +149,7 @@ private function find($key, array $fields = array()) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function listKeys($prefix = '') { @@ -158,17 +158,17 @@ public function listKeys($prefix = '') if ('' == $prefix) { return array( 'dirs' => array(), - 'keys' => $this->keys() + 'keys' => $this->keys(), ); } $result = array( 'dirs' => array(), - 'keys' => array() + 'keys' => array(), ); $gridFiles = $this->gridFS->find(array( - 'filename' => new \MongoRegex(sprintf('/^%s/', $prefix)) + 'filename' => new \MongoRegex(sprintf('/^%s/', $prefix)), )); foreach ($gridFiles as $file) { diff --git a/src/Gaufrette/Adapter/InMemory.php b/src/Gaufrette/Adapter/InMemory.php index 29e50158b..364b2cffb 100644 --- a/src/Gaufrette/Adapter/InMemory.php +++ b/src/Gaufrette/Adapter/InMemory.php @@ -6,11 +6,10 @@ use Gaufrette\Util; /** - * In memory adapter + * In memory adapter. * * Stores some files in memory for test purposes * - * @package Gaufrette * @author Antoine Hérault */ class InMemory implements Adapter @@ -26,7 +25,7 @@ public function __construct(array $files = array()) } /** - * Defines the files + * Defines the files. * * @param array $files An array of files */ @@ -39,8 +38,8 @@ public function setFiles(array $files) } $file = array_merge(array( - 'content' => null, - 'mtime' => null, + 'content' => null, + 'mtime' => null, ), $file); $this->setFile($key, $file['content'], $file['mtime']); @@ -48,11 +47,11 @@ public function setFiles(array $files) } /** - * Defines a file + * Defines a file. * - * @param string $key The key - * @param string $content The content - * @param integer $mtime The last modified time (automatically set to now if NULL) + * @param string $key The key + * @param string $content The content + * @param int $mtime The last modified time (automatically set to now if NULL) */ public function setFile($key, $content = null, $mtime = null) { @@ -61,13 +60,13 @@ public function setFile($key, $content = null, $mtime = null) } $this->files[$key] = array( - 'content' => (string) $content, - 'mtime' => (integer) $mtime + 'content' => (string) $content, + 'mtime' => (integer) $mtime, ); } /** - * {@inheritDoc} + * {@inheritdoc} */ public function read($key) { @@ -75,7 +74,7 @@ public function read($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function rename($sourceKey, $targetKey) { @@ -86,18 +85,18 @@ public function rename($sourceKey, $targetKey) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function write($key, $content, array $metadata = null) { - $this->files[$key]['content'] = $content; - $this->files[$key]['mtime'] = time(); + $this->files[$key]['content'] = $content; + $this->files[$key]['mtime'] = time(); return Util\Size::fromContent($content); } /** - * {@inheritDoc} + * {@inheritdoc} */ public function exists($key) { @@ -105,7 +104,7 @@ public function exists($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function keys() { @@ -113,7 +112,7 @@ public function keys() } /** - * {@inheritDoc} + * {@inheritdoc} */ public function mtime($key) { @@ -121,7 +120,7 @@ public function mtime($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function delete($key) { @@ -132,7 +131,7 @@ public function delete($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function isDirectory($path) { diff --git a/src/Gaufrette/Adapter/LazyOpenCloud.php b/src/Gaufrette/Adapter/LazyOpenCloud.php index d68bb733a..b37c5a02c 100644 --- a/src/Gaufrette/Adapter/LazyOpenCloud.php +++ b/src/Gaufrette/Adapter/LazyOpenCloud.php @@ -5,7 +5,7 @@ use Gaufrette\Adapter\OpenStackCloudFiles\ObjectStoreFactoryInterface; /** - * LazyOpenCloud + * LazyOpenCloud. * * @author Daniel Richter */ @@ -18,8 +18,8 @@ class LazyOpenCloud extends OpenCloud /** * @param ObjectStoreFactoryInterface $objectStoreFactory - * @param string $containerName - * @param bool $createContainer + * @param string $containerName + * @param bool $createContainer */ public function __construct(ObjectStoreFactoryInterface $objectStoreFactory, $containerName, $createContainer = false) { @@ -29,7 +29,7 @@ public function __construct(ObjectStoreFactoryInterface $objectStoreFactory, $co } /** - * Override parent to lazy-load object store + * Override parent to lazy-load object store. * * {@inheritdoc} */ diff --git a/src/Gaufrette/Adapter/ListKeysAware.php b/src/Gaufrette/Adapter/ListKeysAware.php index 41fb8f059..bea2a3d8d 100644 --- a/src/Gaufrette/Adapter/ListKeysAware.php +++ b/src/Gaufrette/Adapter/ListKeysAware.php @@ -3,7 +3,7 @@ namespace Gaufrette\Adapter; /** - * interface that adds support of native listKeys to adapter + * interface that adds support of native listKeys to adapter. * * @author Andrew Tch */ @@ -11,10 +11,11 @@ interface ListKeysAware { /** * Lists keys beginning with pattern given - * (no wildcard / regex matching) + * (no wildcard / regex matching). * * @param string $prefix + * * @return array */ public function listKeys($prefix = ''); -} \ No newline at end of file +} diff --git a/src/Gaufrette/Adapter/Local.php b/src/Gaufrette/Adapter/Local.php index e9360620e..dd0cd723a 100644 --- a/src/Gaufrette/Adapter/Local.php +++ b/src/Gaufrette/Adapter/Local.php @@ -5,11 +5,10 @@ use Gaufrette\Util; use Gaufrette\Adapter; use Gaufrette\Stream; -use Gaufrette\Adapter\StreamFactory; -use Gaufrette\Exception; + /** - * Adapter for the local filesystem + * Adapter for the local filesystem. * * @author Antoine Hérault * @author Leszek Prabucki @@ -25,10 +24,10 @@ class Local implements Adapter, private $mode; /** - * @param string $directory Directory where the filesystem is located - * @param boolean $create Whether to create the directory if it does not - * exist (default FALSE) - * @param integer $mode Mode for mkdir + * @param string $directory Directory where the filesystem is located + * @param bool $create Whether to create the directory if it does not + * exist (default FALSE) + * @param int $mode Mode for mkdir * * @throws RuntimeException if the specified directory does not exist and * could not be created @@ -46,7 +45,7 @@ public function __construct($directory, $create = false, $mode = 0777) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function read($key) { @@ -54,7 +53,7 @@ public function read($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function write($key, $content) { @@ -65,7 +64,7 @@ public function write($key, $content) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function rename($sourceKey, $targetKey) { @@ -76,7 +75,7 @@ public function rename($sourceKey, $targetKey) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function exists($key) { @@ -84,7 +83,7 @@ public function exists($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function keys() { @@ -99,7 +98,7 @@ public function keys() \RecursiveIteratorIterator::CHILD_FIRST ); } catch (\Exception $e) { - $files = new \EmptyIterator; + $files = new \EmptyIterator(); } $keys = array(); @@ -112,7 +111,7 @@ public function keys() } /** - * {@inheritDoc} + * {@inheritdoc} */ public function mtime($key) { @@ -120,7 +119,7 @@ public function mtime($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function delete($key) { @@ -132,8 +131,9 @@ public function delete($key) } /** - * @param string $key - * @return boolean + * @param string $key + * + * @return bool */ public function isDirectory($key) { @@ -141,7 +141,7 @@ public function isDirectory($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function createStream($key) { @@ -175,7 +175,7 @@ public function mimeType($key) } /** - * Computes the key from the specified path + * Computes the key from the specified path. * * @param string $path * @@ -189,7 +189,7 @@ public function computeKey($path) } /** - * Computes the path from the specified key + * Computes the path from the specified key. * * @param string $key The key which for to compute the path * @@ -197,17 +197,17 @@ public function computeKey($path) * * @throws OutOfBoundsException If the computed path is out of the * directory - * @throws RuntimeException If directory does not exists and cannot be created + * @throws RuntimeException If directory does not exists and cannot be created */ protected function computePath($key) { $this->ensureDirectoryExists($this->directory, $this->create); - return $this->normalizePath($this->directory . '/' . $key); + return $this->normalizePath($this->directory.'/'.$key); } /** - * Normalizes the given path + * Normalizes the given path. * * @param string $path * @@ -225,11 +225,11 @@ protected function normalizePath($path) } /** - * Ensures the specified directory exists, creates it if it does not + * Ensures the specified directory exists, creates it if it does not. * - * @param string $directory Path of the directory to test - * @param boolean $create Whether to create the directory if it does - * not exist + * @param string $directory Path of the directory to test + * @param bool $create Whether to create the directory if it does + * not exist * * @throws RuntimeException if the directory does not exists and could not * be created @@ -246,7 +246,7 @@ protected function ensureDirectoryExists($directory, $create = false) } /** - * Creates the specified directory and its parents + * Creates the specified directory and its parents. * * @param string $directory Path of the directory to create * diff --git a/src/Gaufrette/Adapter/MetadataSupporter.php b/src/Gaufrette/Adapter/MetadataSupporter.php index dd25d4894..16a4daeb4 100644 --- a/src/Gaufrette/Adapter/MetadataSupporter.php +++ b/src/Gaufrette/Adapter/MetadataSupporter.php @@ -3,7 +3,7 @@ namespace Gaufrette\Adapter; /** - * Interface which add supports for metadata + * Interface which add supports for metadata. * * @author Leszek Prabucki */ @@ -16,7 +16,8 @@ interface MetadataSupporter public function setMetadata($key, $content); /** - * @param string $key + * @param string $key + * * @return array */ public function getMetadata($key); diff --git a/src/Gaufrette/Adapter/MimeTypeProvider.php b/src/Gaufrette/Adapter/MimeTypeProvider.php index 1fe1aed56..16b1fc88a 100644 --- a/src/Gaufrette/Adapter/MimeTypeProvider.php +++ b/src/Gaufrette/Adapter/MimeTypeProvider.php @@ -3,14 +3,14 @@ namespace Gaufrette\Adapter; /** - * Interface which add mime type provider support to adapter + * Interface which add mime type provider support to adapter. * * @author Gildas Quemener */ interface MimeTypeProvider { /** - * Returns the mime type of the specified key + * Returns the mime type of the specified key. * * @param string $key * diff --git a/src/Gaufrette/Adapter/MogileFS.php b/src/Gaufrette/Adapter/MogileFS.php index a8db4e2bc..f56278cbc 100644 --- a/src/Gaufrette/Adapter/MogileFS.php +++ b/src/Gaufrette/Adapter/MogileFS.php @@ -16,11 +16,11 @@ */ class MogileFS implements Adapter { - const ERR_OTHER = 0; + const ERR_OTHER = 0; const ERR_UNKNOWN_KEY = 1; - const ERR_EMPTY_FILE = 2; - const ERR_NONE_MATCH = 3; - const ERR_KEY_EXISTS = 4; + const ERR_EMPTY_FILE = 2; + const ERR_NONE_MATCH = 3; + const ERR_KEY_EXISTS = 4; protected $domain; protected $hosts; @@ -37,11 +37,11 @@ public function __construct($domain, array $hosts) } $this->domain = $domain; - $this->hosts = $hosts; + $this->hosts = $hosts; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function read($key) { @@ -71,14 +71,14 @@ public function read($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function write($key, $content, array $metadata = null) { $closeres = false; if (mb_strlen($content) > 0) { - $res = $this->doRequest("CREATE_OPEN", array("key" => $key, "class" => $metadata['mogile_class'])); + $res = $this->doRequest('CREATE_OPEN', array('key' => $key, 'class' => $metadata['mogile_class'])); if ($res && preg_match('/^http:\/\/([a-z0-9.-]*):([0-9]*)\/(.*)$/', $res['path'], $matches)) { $host = $matches[1]; @@ -88,9 +88,9 @@ public function write($key, $content, array $metadata = null) $status = $this->putFile($res['path'], $content); if ($status) { - $params = array("key" => $key, "class" => $metadata['mogile_class'], "devid" => $res['devid'], - "fid" => $res['fid'], "path" => urldecode($res['path'])); - $closeres = $this->doRequest("CREATE_CLOSE", $params); + $params = array('key' => $key, 'class' => $metadata['mogile_class'], 'devid' => $res['devid'], + 'fid' => $res['fid'], 'path' => urldecode($res['path']), ); + $closeres = $this->doRequest('CREATE_CLOSE', $params); } } } @@ -103,7 +103,7 @@ public function write($key, $content, array $metadata = null) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function delete($key) { @@ -113,20 +113,20 @@ public function delete($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function rename($sourceKey, $targetKey) { $this->doRequest('RENAME', array( - 'from_key' => $sourceKey, - 'to_key' => $targetKey + 'from_key' => $sourceKey, + 'to_key' => $targetKey, )); return true; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function exists($key) { @@ -140,7 +140,7 @@ public function exists($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function keys() { @@ -160,7 +160,7 @@ public function keys() } /** - * {@inheritDoc} + * {@inheritdoc} */ public function mtime($key) { @@ -168,7 +168,7 @@ public function mtime($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function isDirectory($key) { @@ -176,7 +176,7 @@ public function isDirectory($key) } /** - * Get available domains and classes from tracker + * Get available domains and classes from tracker. * * @return mixed Array on success, false on failure */ @@ -189,13 +189,13 @@ public function getDomains() $domains = array(); - for ($i = 1; $i <= $res['domains']; $i++) { - $dom = 'domain' . $i; + for ($i = 1; $i <= $res['domains']; ++$i) { + $dom = 'domain'.$i; $classes = array(); // Associate classes to current domain (class name => mindevcount) - for ($j = 1; $j <= $res[$dom.'classes']; $j++) { - $classes[$res[$dom . 'class' . $j . 'name']] = $res[$dom . 'class' . $j . 'mindevcount']; + for ($j = 1; $j <= $res[$dom.'classes']; ++$j) { + $classes[$res[$dom.'class'.$j.'name']] = $res[$dom.'class'.$j.'mindevcount']; } $domains[] = array('name' => $res[$dom], 'classes' => $classes); @@ -205,7 +205,7 @@ public function getDomains() } /** - * Tries to connect MogileFS tracker + * Tries to connect MogileFS tracker. * * @return Socket */ @@ -234,9 +234,9 @@ private function connect() } /** - * Close connection to MogileFS tracker + * Close connection to MogileFS tracker. * - * @return boolean + * @return bool */ private function close() { @@ -248,10 +248,11 @@ private function close() } /** - * Makes request to MogileFS tracker + * Makes request to MogileFS tracker. * * @param cmd Command * @param args Array of arguments + * * @return mixed Array on success, false on failure */ private function doRequest($cmd, $args = array()) @@ -299,25 +300,27 @@ private function doRequest($cmd, $args = array()) } /** - * Get file location at server from MogileFS tracker + * Get file location at server from MogileFS tracker. * * @param key File key + * * @return mixed Array on success, false on failure */ private function getPaths($key) { - $res = $this->doRequest("GET_PATHS", array("key" => $key)); + $res = $this->doRequest('GET_PATHS', array('key' => $key)); unset($res['paths']); return $res; } /** - * Sends file to MogileFS tracker + * Sends file to MogileFS tracker. * * @param path Save path at server * @param data Data to save - * @return boolean + * + * @return bool */ private function putFile($path, $data) { @@ -335,11 +338,11 @@ private function putFile($path, $data) stream_set_blocking($fp, true); stream_set_timeout($fp, 30, 200000); - $out = "PUT ". $url['path']. " HTTP/1.1". $b; - $out .= "Host: ". $url['host']. $b; - $out .= "Content-Length: ". Util\Size::fromContent($data). $b. $b; + $out = 'PUT '.$url['path'].' HTTP/1.1'.$b; + $out .= 'Host: '.$url['host'].$b; + $out .= 'Content-Length: '.Util\Size::fromContent($data).$b.$b; $out .= $data; - $out .= $b. $b; + $out .= $b.$b; fwrite($fp, $out); fflush($fp); @@ -356,9 +359,10 @@ private function putFile($path, $data) } /** - * Closes the underlying connection + * Closes the underlying connection. */ - public function __destruct() { - $this->close(); + public function __destruct() + { + $this->close(); } } diff --git a/src/Gaufrette/Adapter/OpenCloud.php b/src/Gaufrette/Adapter/OpenCloud.php index d4969830b..da060ede4 100644 --- a/src/Gaufrette/Adapter/OpenCloud.php +++ b/src/Gaufrette/Adapter/OpenCloud.php @@ -11,9 +11,8 @@ use OpenCloud\ObjectStore\Exception\ObjectNotFoundException; /** - * OpenCloud adapter + * OpenCloud adapter. * - * @package Gaufrette * @author James Watson * @author Daniel Richter */ @@ -47,15 +46,16 @@ class OpenCloud implements Adapter, */ public function __construct(Service $objectStore, $containerName, $createContainer = false) { - $this->objectStore = $objectStore; - $this->containerName = $containerName; + $this->objectStore = $objectStore; + $this->containerName = $containerName; $this->createContainer = $createContainer; } /** - * Returns an initialized container + * Returns an initialized container. * * @throws \RuntimeException + * * @return Container */ protected function getContainer() @@ -80,11 +80,11 @@ protected function getContainer() } /** - * Reads the content of the file + * Reads the content of the file. * * @param string $key * - * @return string|boolean if cannot read content + * @return string|bool if cannot read content */ public function read($key) { @@ -96,19 +96,18 @@ public function read($key) } /** - * Writes the given content into the file + * Writes the given content into the file. * * @param string $key * @param string $content * - * @return integer|boolean The number of bytes that were written into the file + * @return int|bool The number of bytes that were written into the file */ public function write($key, $content) { try { $object = $this->getContainer()->uploadObject($key, $content); - } - catch (CreateUpdateError $updateError) { + } catch (CreateUpdateError $updateError) { return false; } @@ -116,11 +115,11 @@ public function write($key, $content) } /** - * Indicates whether the file exists + * Indicates whether the file exists. * * @param string $key * - * @return boolean + * @return bool */ public function exists($key) { @@ -134,14 +133,14 @@ public function exists($key) } /** - * Returns an array of all keys (files and directories) + * Returns an array of all keys (files and directories). * * @return array */ public function keys() { $objectList = $this->getContainer()->objectList(); - $keys = array (); + $keys = array(); while ($object = $objectList->next()) { $keys[] = $object->getName(); @@ -153,11 +152,11 @@ public function keys() } /** - * Returns the last modified time + * Returns the last modified time. * * @param string $key * - * @return integer|boolean An UNIX like timestamp or false + * @return int|bool An UNIX like timestamp or false */ public function mtime($key) { @@ -169,11 +168,11 @@ public function mtime($key) } /** - * Deletes the file + * Deletes the file. * * @param string $key * - * @return boolean + * @return bool */ public function delete($key) { @@ -183,8 +182,7 @@ public function delete($key) try { $object->delete(); - } - catch (DeleteError $deleteError) { + } catch (DeleteError $deleteError) { return false; } @@ -192,12 +190,12 @@ public function delete($key) } /** - * Renames a file + * Renames a file. * * @param string $sourceKey * @param string $targetKey * - * @return boolean + * @return bool */ public function rename($sourceKey, $targetKey) { @@ -211,11 +209,11 @@ public function rename($sourceKey, $targetKey) } /** - * Check if key is directory + * Check if key is directory. * * @param string $key * - * @return boolean + * @return bool */ public function isDirectory($key) { @@ -223,7 +221,7 @@ public function isDirectory($key) } /** - * Returns the checksum of the specified key + * Returns the checksum of the specified key. * * @param string $key * @@ -247,8 +245,7 @@ protected function tryGetObject($key) { try { return $this->getContainer()->getObject($key); - } - catch (ObjectNotFoundException $objFetchError) { + } catch (ObjectNotFoundException $objFetchError) { return false; } } diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactory.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactory.php index 8eba380e5..996ae2925 100644 --- a/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactory.php +++ b/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactory.php @@ -5,7 +5,7 @@ use OpenCloud\OpenStack; /** - * ObjectStoreFactory + * ObjectStoreFactory. * * @author Daniel Richter */ @@ -33,9 +33,9 @@ class ObjectStoreFactory implements ObjectStoreFactoryInterface /** * @param OpenStack $connection - * @param string $region - * @param string $urlType - * @param string $objectStoreType + * @param string $region + * @param string $urlType + * @param string $objectStoreType */ public function __construct(OpenStack $connection, $region, $urlType, $objectStoreType = 'cloudFiles') { diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactoryInterface.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactoryInterface.php index e679189f8..1129d8ea5 100644 --- a/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactoryInterface.php +++ b/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactoryInterface.php @@ -5,7 +5,7 @@ use OpenCloud\ObjectStore\Service; /** - * ObjectStoreFactoryInterface + * ObjectStoreFactoryInterface. * * @author Daniel Richter */ diff --git a/src/Gaufrette/Adapter/SafeLocal.php b/src/Gaufrette/Adapter/SafeLocal.php index 5dbb37073..9c3dc6570 100644 --- a/src/Gaufrette/Adapter/SafeLocal.php +++ b/src/Gaufrette/Adapter/SafeLocal.php @@ -4,15 +4,14 @@ /** * Safe local adapter that encodes key to avoid the use of the directories - * structure + * structure. * - * @package Gaufrette * @author Antoine Hérault */ class SafeLocal extends Local { /** - * {@inheritDoc} + * {@inheritdoc} */ public function computeKey($path) { @@ -20,7 +19,7 @@ public function computeKey($path) } /** - * {@inheritDoc} + * {@inheritdoc} */ protected function computePath($key) { diff --git a/src/Gaufrette/Adapter/Sftp.php b/src/Gaufrette/Adapter/Sftp.php index de98e2171..69907b40b 100644 --- a/src/Gaufrette/Adapter/Sftp.php +++ b/src/Gaufrette/Adapter/Sftp.php @@ -16,18 +16,18 @@ class Sftp implements Adapter, /** * @param \Ssh\Sftp $sftp An Sftp instance * @param string $directory The distant directory - * @param boolean $create Whether to create the remote directory if it + * @param bool $create Whether to create the remote directory if it * does not exist */ public function __construct(SftpClient $sftp, $directory = null, $create = false) { - $this->sftp = $sftp; + $this->sftp = $sftp; $this->directory = $directory; - $this->create = $create; + $this->create = $create; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function read($key) { @@ -39,7 +39,7 @@ public function read($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function rename($sourceKey, $targetKey) { @@ -52,7 +52,7 @@ public function rename($sourceKey, $targetKey) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function write($key, $content) { @@ -66,7 +66,7 @@ public function write($key, $content) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function exists($key) { @@ -79,7 +79,7 @@ public function exists($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function isDirectory($key) { @@ -91,7 +91,7 @@ public function isDirectory($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function keys() { @@ -113,7 +113,7 @@ public function keys() } /** - * {@inheritDoc} + * {@inheritdoc} */ public function mtime($key) { @@ -123,7 +123,7 @@ public function mtime($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function checksum($key) { @@ -137,7 +137,7 @@ public function checksum($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function delete($key) { @@ -147,7 +147,7 @@ public function delete($key) } /** - * Computes the key from the specified path + * Computes the key from the specified path. * * @param string $path * @@ -163,7 +163,7 @@ public function computeKey($path) } /** - * Computes the path for the specified key + * Computes the path for the specified key. * * @param string $key * @@ -171,11 +171,11 @@ public function computeKey($path) */ protected function computePath($key) { - return $this->directory . '/' . ltrim($key, '/'); + return $this->directory.'/'.ltrim($key, '/'); } /** - * Performs the adapter's initialization + * Performs the adapter's initialization. * * It will ensure the root directory exists */ @@ -190,10 +190,10 @@ protected function initialize() } /** - * Ensures the specified directory exists + * Ensures the specified directory exists. * - * @param string $directory The directory that we ensure the existence - * @param boolean $create Whether to create it if it does not exist + * @param string $directory The directory that we ensure the existence + * @param bool $create Whether to create it if it does not exist * * @throws RuntimeException if the specified directory does not exist and * could not be created @@ -214,11 +214,11 @@ protected function ensureDirectoryExists($directory, $create = false) } /** - * Creates the specified directory and its parents + * Creates the specified directory and its parents. * * @param string $directory The directory to create * - * @return boolean TRUE on success, or FALSE on failure + * @return bool TRUE on success, or FALSE on failure */ protected function createDirectory($directory) { diff --git a/src/Gaufrette/Adapter/SizeCalculator.php b/src/Gaufrette/Adapter/SizeCalculator.php index 6fd483043..512537a5b 100644 --- a/src/Gaufrette/Adapter/SizeCalculator.php +++ b/src/Gaufrette/Adapter/SizeCalculator.php @@ -3,14 +3,14 @@ namespace Gaufrette\Adapter; /** - * Interface which add size calculation support to adapter + * Interface which add size calculation support to adapter. * * @author Markus Poerschke */ interface SizeCalculator { /** - * Returns the size of the specified key + * Returns the size of the specified key. * * @param string $key * diff --git a/src/Gaufrette/Adapter/StreamFactory.php b/src/Gaufrette/Adapter/StreamFactory.php index 4f4f42135..bbf8f21bd 100644 --- a/src/Gaufrette/Adapter/StreamFactory.php +++ b/src/Gaufrette/Adapter/StreamFactory.php @@ -3,14 +3,14 @@ namespace Gaufrette\Adapter; /** - * Interface for the stream creation class + * Interface for the stream creation class. * * @author Leszek Prabucki */ interface StreamFactory { /** - * Creates a new stream instance of the specified file + * Creates a new stream instance of the specified file. * * @param string $key * diff --git a/src/Gaufrette/Adapter/Zip.php b/src/Gaufrette/Adapter/Zip.php index 0eeccb720..35fc1323d 100644 --- a/src/Gaufrette/Adapter/Zip.php +++ b/src/Gaufrette/Adapter/Zip.php @@ -5,10 +5,9 @@ use ZipArchive; use Gaufrette\Adapter; use Gaufrette\Util; -use Gaufrette\Exception; /** - * ZIP Archive adapter + * ZIP Archive adapter. * * @author Boris Guéry * @author Antoine Hérault @@ -39,7 +38,7 @@ public function __construct($zipFile) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function read($key) { @@ -51,7 +50,7 @@ public function read($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function write($key, $content) { @@ -67,7 +66,7 @@ public function write($key, $content) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function exists($key) { @@ -75,7 +74,7 @@ public function exists($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function keys() { @@ -91,7 +90,7 @@ public function keys() /** * @todo implement * - * {@inheritDoc} + * {@inheritdoc} */ public function isDirectory($key) { @@ -99,7 +98,7 @@ public function isDirectory($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function mtime($key) { @@ -109,7 +108,7 @@ public function mtime($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function delete($key) { @@ -121,7 +120,7 @@ public function delete($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function rename($sourceKey, $targetKey) { @@ -134,9 +133,10 @@ public function rename($sourceKey, $targetKey) /** * Returns the stat of a file in the zip archive - * (name, index, crc, mtime, compression size, compression method, filesize) + * (name, index, crc, mtime, compression size, compression method, filesize). * * @param $key + * * @return array|bool */ public function getStat($key) @@ -155,7 +155,6 @@ public function __destruct() try { $this->zipArchive->close(); } catch (\Exception $e) { - } unset($this->zipArchive); } @@ -206,7 +205,7 @@ protected function reinitZipArchive() } /** - * Saves archive modifications and updates current ZipArchive instance + * Saves archive modifications and updates current ZipArchive instance. * * @throws \RuntimeException If file could not be saved */ diff --git a/src/Gaufrette/Exception.php b/src/Gaufrette/Exception.php index 10072d1a5..626dc6994 100644 --- a/src/Gaufrette/Exception.php +++ b/src/Gaufrette/Exception.php @@ -3,7 +3,7 @@ namespace Gaufrette; /** - * Interface for the Gaufrette related exceptions + * Interface for the Gaufrette related exceptions. * * @author Antoine Hérault */ diff --git a/src/Gaufrette/Exception/FileAlreadyExists.php b/src/Gaufrette/Exception/FileAlreadyExists.php index cb9d90f7e..b58b5337b 100644 --- a/src/Gaufrette/Exception/FileAlreadyExists.php +++ b/src/Gaufrette/Exception/FileAlreadyExists.php @@ -5,7 +5,7 @@ use Gaufrette\Exception; /** - * Exception to be thrown when a file already exists + * Exception to be thrown when a file already exists. * * @author Benjamin Dulau */ diff --git a/src/Gaufrette/Exception/FileNotFound.php b/src/Gaufrette/Exception/FileNotFound.php index 0e37b8f33..840ab9971 100644 --- a/src/Gaufrette/Exception/FileNotFound.php +++ b/src/Gaufrette/Exception/FileNotFound.php @@ -5,7 +5,7 @@ use Gaufrette\Exception; /** - * Exception to be thrown when a file was not found + * Exception to be thrown when a file was not found. * * @author Antoine Hérault */ diff --git a/src/Gaufrette/Exception/UnexpectedFile.php b/src/Gaufrette/Exception/UnexpectedFile.php index d3e35c7b0..38749c2fe 100644 --- a/src/Gaufrette/Exception/UnexpectedFile.php +++ b/src/Gaufrette/Exception/UnexpectedFile.php @@ -5,7 +5,7 @@ use Gaufrette\Exception; /** - * Exception to be thrown when an unexpected file exists + * Exception to be thrown when an unexpected file exists. * * @author Antoine Hérault */ diff --git a/src/Gaufrette/File.php b/src/Gaufrette/File.php index 6b995056b..5a704417e 100644 --- a/src/Gaufrette/File.php +++ b/src/Gaufrette/File.php @@ -6,7 +6,7 @@ use Gaufrette\Exception\FileNotFound; /** - * Points to a file in a filesystem + * Points to a file in a filesystem. * * @author Antoine Hérault */ @@ -16,7 +16,8 @@ class File protected $filesystem; /** - * Content variable is lazy. It will not be read from filesystem until it's requested first time + * Content variable is lazy. It will not be read from filesystem until it's requested first time. + * * @var mixed content */ protected $content = null; @@ -27,19 +28,22 @@ class File protected $metadata = null; /** - * Human readable filename (usually the end of the key) + * Human readable filename (usually the end of the key). + * * @var string name */ protected $name = null; /** - * File size in bytes + * File size in bytes. + * * @var int size */ protected $size = 0; /** - * File date modified + * File date modified. + * * @var int mtime */ protected $mtime = null; @@ -56,7 +60,7 @@ public function __construct($key, Filesystem $filesystem) } /** - * Returns the key + * Returns the key. * * @return string */ @@ -66,11 +70,12 @@ public function getKey() } /** - * Returns the content + * Returns the content. * * @throws FileNotFound * - * @param array $metadata optional metadata which should be send when read + * @param array $metadata optional metadata which should be send when read + * * @return string */ public function getContent($metadata = array()) @@ -109,7 +114,7 @@ public function getSize() } /** - * Returns the file modified time + * Returns the file modified time. * * @return int */ @@ -127,13 +132,13 @@ public function setSize($size) } /** - * Sets the content + * Sets the content. * * @param string $content * @param array $metadata optional metadata which should be send when write * - * @return integer The number of bytes that were written into the file, or - * FALSE on failure + * @return int The number of bytes that were written into the file, or + * FALSE on failure */ public function setContent($content, $metadata = array()) { @@ -152,9 +157,9 @@ public function setName($name) } /** - * Indicates whether the file exists in the filesystem + * Indicates whether the file exists in the filesystem. * - * @return boolean + * @return bool */ public function exists() { @@ -162,12 +167,14 @@ public function exists() } /** - * Deletes the file from the filesystem + * Deletes the file from the filesystem. * * @throws FileNotFound - * @throws \RuntimeException when cannot delete file - * @param array $metadata optional metadata which should be send when write - * @return boolean TRUE on success + * @throws \RuntimeException when cannot delete file + * + * @param array $metadata optional metadata which should be send when write + * + * @return bool TRUE on success */ public function delete($metadata = array()) { @@ -177,7 +184,7 @@ public function delete($metadata = array()) } /** - * Creates a new file stream instance of the file + * Creates a new file stream instance of the file. * * @return Stream */ @@ -187,10 +194,11 @@ public function createStream() } /** - * Sets the metadata array to be stored in adapters that can support it + * Sets the metadata array to be stored in adapters that can support it. + * + * @param array $metadata * - * @param array $metadata - * @return boolean + * @return bool */ protected function setMetadata(array $metadata) { @@ -204,7 +212,7 @@ protected function setMetadata(array $metadata) } /** - * @return boolean + * @return bool */ private function supportsMetadata() { diff --git a/src/Gaufrette/Filesystem.php b/src/Gaufrette/Filesystem.php index 9162f0fa6..8d23932ee 100644 --- a/src/Gaufrette/Filesystem.php +++ b/src/Gaufrette/Filesystem.php @@ -5,7 +5,7 @@ use Gaufrette\Adapter\ListKeysAware; /** - * A filesystem is used to store and retrieve files + * A filesystem is used to store and retrieve files. * * @author Antoine Hérault * @author Leszek Prabucki @@ -15,7 +15,7 @@ class Filesystem protected $adapter; /** - * Contains File objects created with $this->createFile() method + * Contains File objects created with $this->createFile() method. * * @var array */ @@ -30,7 +30,7 @@ public function __construct(Adapter $adapter) } /** - * Returns the adapter + * Returns the adapter. * * @return Adapter */ @@ -40,11 +40,11 @@ public function getAdapter() } /** - * Indicates whether the file matching the specified key exists + * Indicates whether the file matching the specified key exists. * * @param string $key * - * @return boolean TRUE if the file exists, FALSE otherwise + * @return bool TRUE if the file exists, FALSE otherwise */ public function has($key) { @@ -52,12 +52,13 @@ public function has($key) } /** - * Renames a file + * Renames a file. * * @param string $sourceKey * @param string $targetKey * - * @return boolean TRUE if the rename was successful + * @return bool TRUE if the rename was successful + * * @throws Exception\FileNotFound when sourceKey does not exist * @throws Exception\UnexpectedFile when targetKey exists * @throws \RuntimeException when cannot rename @@ -70,11 +71,11 @@ public function rename($sourceKey, $targetKey) throw new Exception\UnexpectedFile($targetKey); } - if (! $this->adapter->rename($sourceKey, $targetKey)) { + if (!$this->adapter->rename($sourceKey, $targetKey)) { throw new \RuntimeException(sprintf('Could not rename the "%s" key to "%s".', $sourceKey, $targetKey)); } - if($this->isFileInRegister($sourceKey)) { + if ($this->isFileInRegister($sourceKey)) { $this->fileRegister[$targetKey] = $this->fileRegister[$sourceKey]; unset($this->fileRegister[$sourceKey]); } @@ -83,12 +84,13 @@ public function rename($sourceKey, $targetKey) } /** - * Returns the file matching the specified key + * Returns the file matching the specified key. * - * @param string $key Key of the file - * @param boolean $create Whether to create the file if it does not exist + * @param string $key Key of the file + * @param bool $create Whether to create the file if it does not exist * * @throws Exception\FileNotFound + * * @return File */ public function get($key, $create = false) @@ -101,15 +103,16 @@ public function get($key, $create = false) } /** - * Writes the given content into the file + * Writes the given content into the file. + * + * @param string $key Key of the file + * @param string $content Content to write in the file + * @param bool $overwrite Whether to overwrite the file if exists * - * @param string $key Key of the file - * @param string $content Content to write in the file - * @param boolean $overwrite Whether to overwrite the file if exists * @throws Exception\FileAlreadyExists When file already exists and overwrite is false * @throws \RuntimeException When for any reason content could not be written * - * @return integer The number of bytes that were written into the file + * @return int The number of bytes that were written into the file */ public function write($key, $content, $overwrite = false) { @@ -127,9 +130,10 @@ public function write($key, $content, $overwrite = false) } /** - * Reads the content from the file + * Reads the content from the file. + * + * @param string $key Key of the file * - * @param string $key Key of the file * @throws Exception\FileNotFound when file does not exist * @throws \RuntimeException when cannot read file * @@ -149,12 +153,13 @@ public function read($key) } /** - * Deletes the file matching the specified key + * Deletes the file matching the specified key. * * @param string $key + * * @throws \RuntimeException when cannot read file * - * @return boolean + * @return bool */ public function delete($key) { @@ -162,6 +167,7 @@ public function delete($key) if ($this->adapter->delete($key)) { $this->removeFromRegister($key); + return true; } @@ -169,7 +175,7 @@ public function delete($key) } /** - * Returns an array of all keys + * Returns an array of all keys. * * @return array */ @@ -180,12 +186,13 @@ public function keys() /** * Lists keys beginning with given prefix - * (no wildcard / regex matching) + * (no wildcard / regex matching). * * if adapter implements ListKeysAware interface, adapter's implementation will be used, * in not, ALL keys will be requested and iterated through. * - * @param string $prefix + * @param string $prefix + * * @return array */ public function listKeys($prefix = '') @@ -209,16 +216,16 @@ public function listKeys($prefix = '') return array( 'keys' => $keys, - 'dirs' => $dirs + 'dirs' => $dirs, ); } /** - * Returns the last modified time of the specified file + * Returns the last modified time of the specified file. * * @param string $key * - * @return integer An UNIX like timestamp + * @return int An UNIX like timestamp */ public function mtime($key) { @@ -228,7 +235,7 @@ public function mtime($key) } /** - * Returns the checksum of the specified file's content + * Returns the checksum of the specified file's content. * * @param string $key * @@ -246,11 +253,11 @@ public function checksum($key) } /** - * Returns the size of the specified file's content + * Returns the size of the specified file's content. * * @param string $key * - * @return integer File size in Bytes + * @return int File size in Bytes */ public function size($key) { @@ -267,6 +274,7 @@ public function size($key) * Gets a new stream instance of the specified file. * * @param $key + * * @return Stream|Stream\InMemoryBuffer */ public function createStream($key) @@ -282,11 +290,12 @@ public function createStream($key) * Creates a new file in a filesystem. * * @param $key + * * @return File */ public function createFile($key) { - if(false === $this->isFileInRegister($key)) { + if (false === $this->isFileInRegister($key)) { if ($this->adapter instanceof Adapter\FileFactory) { $this->fileRegister[$key] = $this->adapter->createFile($key, $this); } else { @@ -298,7 +307,7 @@ public function createFile($key) } /** - * Get the mime type of the provided key + * Get the mime type of the provided key. * * @param string $key * @@ -319,24 +328,24 @@ public function mimeType($key) } /** - * Checks if matching file by given key exists in the filesystem + * Checks if matching file by given key exists in the filesystem. * * Key must be non empty string, otherwise it will throw Exception\FileNotFound * {@see http://php.net/manual/en/function.empty.php} * * @param string $key * - * @throws Exception\FileNotFound when sourceKey does not exist + * @throws Exception\FileNotFound when sourceKey does not exist */ private function assertHasFile($key) { - if (! empty($key) && ! $this->has($key)) { + if (!empty($key) && !$this->has($key)) { throw new Exception\FileNotFound($key); } } /** - * Checks if matching File object by given key exists in the fileRegister + * Checks if matching File object by given key exists in the fileRegister. * * @param string $key * @@ -348,7 +357,7 @@ private function isFileInRegister($key) } /** - * Clear files register + * Clear files register. */ public function clearFileRegister() { @@ -356,7 +365,7 @@ public function clearFileRegister() } /** - * Removes File object from register + * Removes File object from register. * * @param string $key */ diff --git a/src/Gaufrette/FilesystemMap.php b/src/Gaufrette/FilesystemMap.php index 173c732a3..384e69721 100644 --- a/src/Gaufrette/FilesystemMap.php +++ b/src/Gaufrette/FilesystemMap.php @@ -3,7 +3,7 @@ namespace Gaufrette; /** - * Associates filesystem instances to domains + * Associates filesystem instances to domains. * * @author Antoine Hérault */ @@ -13,7 +13,7 @@ class FilesystemMap /** * Returns an array of all the registered filesystems where the key is the - * domain and the value the filesystem + * domain and the value the filesystem. * * @return array */ @@ -23,7 +23,7 @@ public function all() } /** - * Register the given filesystem for the specified domain + * Register the given filesystem for the specified domain. * * @param string $domain * @param Filesystem $filesystem @@ -33,7 +33,7 @@ public function all() */ public function set($domain, Filesystem $filesystem) { - if (! preg_match('/^[-_a-zA-Z0-9]+$/', $domain)) { + if (!preg_match('/^[-_a-zA-Z0-9]+$/', $domain)) { throw new \InvalidArgumentException(sprintf( 'The specified domain "%s" is not a valid domain.', $domain @@ -45,11 +45,11 @@ public function set($domain, Filesystem $filesystem) /** * Indicates whether there is a filesystem registered for the specified - * domain + * domain. * * @param string $domain * - * @return Boolean + * @return bool */ public function has($domain) { @@ -57,7 +57,7 @@ public function has($domain) } /** - * Returns the filesystem registered for the specified domain + * Returns the filesystem registered for the specified domain. * * @param string $domain * @@ -68,7 +68,7 @@ public function has($domain) */ public function get($domain) { - if (! $this->has($domain)) { + if (!$this->has($domain)) { throw new \InvalidArgumentException(sprintf( 'There is no filesystem defined for the "%s" domain.', $domain @@ -79,15 +79,13 @@ public function get($domain) } /** - * Removes the filesystem registered for the specified domain + * Removes the filesystem registered for the specified domain. * * @param string $domain - * - * @return void */ public function remove($domain) { - if (! $this->has($domain)) { + if (!$this->has($domain)) { throw new \InvalidArgumentException(sprintf( 'Cannot remove the "%s" filesystem as it is not defined.', $domain @@ -98,9 +96,7 @@ public function remove($domain) } /** - * Clears all the registered filesystems - * - * @return void + * Clears all the registered filesystems. */ public function clear() { diff --git a/src/Gaufrette/Stream.php b/src/Gaufrette/Stream.php index 6b7ff2829..7f640b80d 100644 --- a/src/Gaufrette/Stream.php +++ b/src/Gaufrette/Stream.php @@ -3,108 +3,107 @@ namespace Gaufrette; /** - * Interface for the file streams + * Interface for the file streams. * * @author Antoine Hérault */ interface Stream { /** - * Opens the stream in the specified mode + * Opens the stream in the specified mode. * * @param StreamMode $mode * - * @return Boolean TRUE on success or FALSE on failure + * @return bool TRUE on success or FALSE on failure */ public function open(StreamMode $mode); /** - * Reads the specified number of bytes from the current position + * Reads the specified number of bytes from the current position. * * If the current position is the end-of-file, you must return an empty * string. * - * @param integer $count The number of bytes + * @param int $count The number of bytes * * @return string */ public function read($count); /** - * Writes the specified data + * Writes the specified data. * * Don't forget to update the current position of the stream by number of * bytes that were successfully written. * * @param string $data * - * @return integer The number of bytes that were successfully written + * @return int The number of bytes that were successfully written */ public function write($data); /** - * Closes the stream + * Closes the stream. * * It must free all the resources. If there is any data to flush, you * should do so - * - * @return void */ public function close(); /** - * Flushes the output + * Flushes the output. * * If you have cached data that is not yet stored into the underlying * storage, you should do so now * - * @return Boolean TRUE on success or FALSE on failure + * @return bool TRUE on success or FALSE on failure */ public function flush(); /** - * Seeks to the specified offset + * Seeks to the specified offset. * - * @param integer $offset - * @param integer $whence + * @param int $offset + * @param int $whence * - * @return Boolean + * @return bool */ public function seek($offset, $whence = SEEK_SET); /** - * Returns the current position + * Returns the current position. * - * @return integer + * @return int */ public function tell(); /** - * Indicates whether the current position is the end-of-file + * Indicates whether the current position is the end-of-file. * - * @return Boolean + * @return bool */ public function eof(); /** - * Gathers statistics of the stream + * Gathers statistics of the stream. * * @return array */ public function stat(); /** - * Retrieve the underlying resource + * Retrieve the underlying resource. + * + * @param int $castAs * - * @param integer $castAs - * @return mixed using resource or false + * @return mixed using resource or false */ public function cast($castAs); /** - * Delete a file + * Delete a file. * - * @return Boolean TRUE on success FALSE otherwise + * @return bool TRUE on success FALSE otherwise */ public function unlink(); } diff --git a/src/Gaufrette/Stream/InMemoryBuffer.php b/src/Gaufrette/Stream/InMemoryBuffer.php index cca06652a..4fc279091 100644 --- a/src/Gaufrette/Stream/InMemoryBuffer.php +++ b/src/Gaufrette/Stream/InMemoryBuffer.php @@ -24,11 +24,11 @@ class InMemoryBuffer implements Stream public function __construct(Filesystem $filesystem, $key) { $this->filesystem = $filesystem; - $this->key = $key; + $this->key = $key; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function open(StreamMode $mode) { @@ -64,7 +64,7 @@ public function read($count) } $chunk = substr($this->content, $this->position, $count); - $this->position+= Util\Size::fromContent($chunk); + $this->position += Util\Size::fromContent($chunk); return $chunk; } @@ -77,23 +77,23 @@ public function write($data) $numWrittenBytes = Util\Size::fromContent($data); - $newPosition = $this->position + $numWrittenBytes; - $newNumBytes = $newPosition > $this->numBytes ? $newPosition : $this->numBytes; + $newPosition = $this->position + $numWrittenBytes; + $newNumBytes = $newPosition > $this->numBytes ? $newPosition : $this->numBytes; if ($this->eof()) { $this->numBytes += $numWrittenBytes; if ($this->hasNewContentAtFurtherPosition()) { - $data = str_pad($data, $this->position + strlen($data), " ", STR_PAD_LEFT); + $data = str_pad($data, $this->position + strlen($data), ' ', STR_PAD_LEFT); } $this->content .= $data; } else { $before = substr($this->content, 0, $this->position); - $after = $newNumBytes > $newPosition ? substr($this->content, $newPosition) : ''; - $this->content = $before . $data . $after; + $after = $newNumBytes > $newPosition ? substr($this->content, $newPosition) : ''; + $this->content = $before.$data.$after; } - $this->position = $newPosition; - $this->numBytes = $newNumBytes; + $this->position = $newPosition; + $this->numBytes = $newNumBytes; $this->synchronized = false; return $numWrittenBytes; @@ -101,7 +101,7 @@ public function write($data) public function close() { - if (! $this->synchronized) { + if (!$this->synchronized) { $this->flush(); } } @@ -151,28 +151,28 @@ public function eof() } /** - * {@inheritDoc} + * {@inheritdoc} */ public function stat() { if ($this->filesystem->has($this->key)) { $isDirectory = $this->filesystem->isDirectory($this->key); - $time = $this->filesystem->mtime($this->key); + $time = $this->filesystem->mtime($this->key); $stats = array( - 'dev' => 1, - 'ino' => 0, - 'mode' => $isDirectory ? 16893 : 33204, + 'dev' => 1, + 'ino' => 0, + 'mode' => $isDirectory ? 16893 : 33204, 'nlink' => 1, - 'uid' => 0, - 'gid' => 0, - 'rdev' => 0, - 'size' => $isDirectory ? 0 : Util\Size::fromContent($this->content), + 'uid' => 0, + 'gid' => 0, + 'rdev' => 0, + 'size' => $isDirectory ? 0 : Util\Size::fromContent($this->content), 'atime' => $time, 'mtime' => $time, 'ctime' => $time, 'blksize' => -1, - 'blocks' => -1, + 'blocks' => -1, ); return array_merge(array_values($stats), $stats); @@ -182,7 +182,7 @@ public function stat() } /** - * {@inheritDoc} + * {@inheritdoc} */ public function cast($castAst) { @@ -190,7 +190,7 @@ public function cast($castAst) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function unlink() { @@ -202,7 +202,7 @@ public function unlink() } /** - * @return Boolean + * @return bool */ protected function hasNewContentAtFurtherPosition() { @@ -210,8 +210,9 @@ protected function hasNewContentAtFurtherPosition() } /** - * @param string $content Empty string by default - * @param bool $overwrite Overwrite by default + * @param string $content Empty string by default + * @param bool $overwrite Overwrite by default + * * @return string */ protected function writeContent($content = '', $overwrite = true) diff --git a/src/Gaufrette/Stream/Local.php b/src/Gaufrette/Stream/Local.php index 776a4bf21..801d9b284 100644 --- a/src/Gaufrette/Stream/Local.php +++ b/src/Gaufrette/Stream/Local.php @@ -6,7 +6,7 @@ use Gaufrette\StreamMode; /** - * Local stream + * Local stream. * * @author Antoine Hérault */ @@ -25,7 +25,7 @@ public function __construct($path) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function open(StreamMode $mode) { @@ -50,11 +50,11 @@ public function open(StreamMode $mode) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function read($count) { - if (! $this->fileHandle) { + if (!$this->fileHandle) { return false; } @@ -66,11 +66,11 @@ public function read($count) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function write($data) { - if (! $this->fileHandle) { + if (!$this->fileHandle) { return false; } @@ -82,11 +82,11 @@ public function write($data) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function close() { - if (! $this->fileHandle) { + if (!$this->fileHandle) { return false; } @@ -101,7 +101,7 @@ public function close() } /** - * {@inheritDoc} + * {@inheritdoc} */ public function flush() { @@ -113,7 +113,7 @@ public function flush() } /** - * {@inheritDoc} + * {@inheritdoc} */ public function seek($offset, $whence = SEEK_SET) { @@ -125,7 +125,7 @@ public function seek($offset, $whence = SEEK_SET) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function tell() { @@ -137,7 +137,7 @@ public function tell() } /** - * {@inheritDoc} + * {@inheritdoc} */ public function eof() { @@ -149,7 +149,7 @@ public function eof() } /** - * {@inheritDoc} + * {@inheritdoc} */ public function stat() { @@ -163,7 +163,7 @@ public function stat() } /** - * {@inheritDoc} + * {@inheritdoc} */ public function cast($castAs) { @@ -175,7 +175,7 @@ public function cast($castAs) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function unlink() { diff --git a/src/Gaufrette/StreamMode.php b/src/Gaufrette/StreamMode.php index 28a7b988e..7dbf83a1c 100644 --- a/src/Gaufrette/StreamMode.php +++ b/src/Gaufrette/StreamMode.php @@ -3,7 +3,7 @@ namespace Gaufrette; /** - * Represents a stream mode + * Represents a stream mode. * * @author Antoine Hérault */ @@ -30,7 +30,7 @@ public function __construct($mode) } /** - * Returns the underlying mode + * Returns the underlying mode. * * @return string */ @@ -40,9 +40,9 @@ public function getMode() } /** - * Indicates whether the mode allows to read + * Indicates whether the mode allows to read. * - * @return Boolean + * @return bool */ public function allowsRead() { @@ -54,9 +54,9 @@ public function allowsRead() } /** - * Indicates whether the mode allows to write + * Indicates whether the mode allows to write. * - * @return Boolean + * @return bool */ public function allowsWrite() { @@ -68,9 +68,9 @@ public function allowsWrite() } /** - * Indicates whether the mode allows to open an existing file + * Indicates whether the mode allows to open an existing file. * - * @return Boolean + * @return bool */ public function allowsExistingFileOpening() { @@ -78,9 +78,9 @@ public function allowsExistingFileOpening() } /** - * Indicates whether the mode allows to create a new file + * Indicates whether the mode allows to create a new file. * - * @return Boolean + * @return bool */ public function allowsNewFileOpening() { @@ -89,9 +89,9 @@ public function allowsNewFileOpening() /** * Indicates whether the mode implies to delete the existing content of the - * file when it already exists + * file when it already exists. * - * @return Boolean + * @return bool */ public function impliesExistingContentDeletion() { @@ -100,9 +100,9 @@ public function impliesExistingContentDeletion() /** * Indicates whether the mode implies positioning the cursor at the - * beginning of the file + * beginning of the file. * - * @return Boolean + * @return bool */ public function impliesPositioningCursorAtTheBeginning() { @@ -111,9 +111,9 @@ public function impliesPositioningCursorAtTheBeginning() /** * Indicates whether the mode implies positioning the cursor at the end of - * the file + * the file. * - * @return Boolean + * @return bool */ public function impliesPositioningCursorAtTheEnd() { @@ -121,9 +121,9 @@ public function impliesPositioningCursorAtTheEnd() } /** - * Indicates whether the stream is in binary mode + * Indicates whether the stream is in binary mode. * - * @return Boolean + * @return bool */ public function isBinary() { @@ -131,9 +131,9 @@ public function isBinary() } /** - * Indicates whether the stream is in text mode + * Indicates whether the stream is in text mode. * - * @return Boolean + * @return bool */ public function isText() { diff --git a/src/Gaufrette/StreamWrapper.php b/src/Gaufrette/StreamWrapper.php index 7c94e0716..b96076683 100644 --- a/src/Gaufrette/StreamWrapper.php +++ b/src/Gaufrette/StreamWrapper.php @@ -3,7 +3,7 @@ namespace Gaufrette; /** - * Stream wrapper class for the Gaufrette filesystems + * Stream wrapper class for the Gaufrette filesystems. * * @author Antoine Hérault * @author Leszek Prabucki @@ -15,7 +15,7 @@ class StreamWrapper private $stream; /** - * Defines the filesystem map + * Defines the filesystem map. * * @param FilesystemMap $map */ @@ -25,7 +25,7 @@ public static function setFilesystemMap(FilesystemMap $map) } /** - * Returns the filesystem map + * Returns the filesystem map. * * @return FilesystemMap $map */ @@ -39,7 +39,7 @@ public static function getFilesystemMap() } /** - * Registers the stream wrapper to handle the specified scheme + * Registers the stream wrapper to handle the specified scheme. * * @param string $scheme Default is gaufrette */ @@ -47,7 +47,7 @@ public static function register($scheme = 'gaufrette') { static::streamWrapperUnregister($scheme); - if (! static::streamWrapperRegister($scheme, __CLASS__)) { + if (!static::streamWrapperRegister($scheme, __CLASS__)) { throw new \RuntimeException(sprintf( 'Could not register stream wrapper class %s for scheme %s.', __CLASS__, @@ -93,7 +93,8 @@ public function stream_open($path, $mode) } /** - * @param int $bytes + * @param int $bytes + * * @return mixed */ public function stream_read($bytes) @@ -106,7 +107,8 @@ public function stream_read($bytes) } /** - * @param string $data + * @param string $data + * * @return int */ public function stream_write($data) @@ -126,7 +128,7 @@ public function stream_close() } /** - * @return boolean + * @return bool */ public function stream_flush() { @@ -138,9 +140,10 @@ public function stream_flush() } /** - * @param int $offset - * @param int $whence - one of values [SEEK_SET, SEEK_CUR, SEEK_END] - * @return boolean + * @param int $offset + * @param int $whence - one of values [SEEK_SET, SEEK_CUR, SEEK_END] + * + * @return bool */ public function stream_seek($offset, $whence = SEEK_SET) { @@ -164,7 +167,7 @@ public function stream_tell() } /** - * @return boolean + * @return bool */ public function stream_eof() { @@ -188,9 +191,11 @@ public function stream_stat() } /** - * @param string $path - * @param int $flags + * @param string $path + * @param int $flags + * * @return mixed + * * @todo handle $flags parameter */ public function url_stat($path, $flags) @@ -206,7 +211,8 @@ public function url_stat($path, $flags) } /** - * @param string $path + * @param string $path + * * @return mixed */ public function unlink($path) @@ -238,24 +244,24 @@ protected function createStream($path) { $parts = array_merge( array( - 'scheme' => null, - 'host' => null, - 'path' => null, - 'query' => null, - 'fragment' => null, + 'scheme' => null, + 'host' => null, + 'path' => null, + 'query' => null, + 'fragment' => null, ), parse_url($path) ?: array() ); $domain = $parts['host']; - $key = substr($parts['path'], 1); + $key = substr($parts['path'], 1); if (null !== $parts['query']) { - $key.= '?' . $parts['query']; + $key .= '?'.$parts['query']; } if (null !== $parts['fragment']) { - $key.= '#' . $parts['fragment']; + $key .= '#'.$parts['fragment']; } if (empty($domain) || empty($key)) { diff --git a/src/Gaufrette/Util/Checksum.php b/src/Gaufrette/Util/Checksum.php index 85b10bebf..63afffaed 100644 --- a/src/Gaufrette/Util/Checksum.php +++ b/src/Gaufrette/Util/Checksum.php @@ -3,14 +3,14 @@ namespace Gaufrette\Util; /** - * Checksum utils + * Checksum utils. * * @author Antoine Hérault */ class Checksum { /** - * Returns the checksum of the given content + * Returns the checksum of the given content. * * @param string $content * @@ -22,7 +22,7 @@ public static function fromContent($content) } /** - * Returns the checksum of the specified file + * Returns the checksum of the specified file. * * @param string $filename * diff --git a/src/Gaufrette/Util/Path.php b/src/Gaufrette/Util/Path.php index fbe17d68c..3903ad47d 100644 --- a/src/Gaufrette/Util/Path.php +++ b/src/Gaufrette/Util/Path.php @@ -3,15 +3,14 @@ namespace Gaufrette\Util; /** - * Path utils + * Path utils. * - * @package Gaufrette * @author Antoine Hérault */ class Path { /** - * Normalizes the given path + * Normalizes the given path. * * @param string $path * @@ -19,10 +18,10 @@ class Path */ public static function normalize($path) { - $path = str_replace('\\', '/', $path); + $path = str_replace('\\', '/', $path); $prefix = static::getAbsolutePrefix($path); - $path = substr($path, strlen($prefix)); - $parts = array_filter(explode('/', $path), 'strlen'); + $path = substr($path, strlen($prefix)); + $parts = array_filter(explode('/', $path), 'strlen'); $tokens = array(); foreach ($parts as $part) { @@ -41,15 +40,15 @@ public static function normalize($path) } } - return $prefix . implode('/', $tokens); + return $prefix.implode('/', $tokens); } /** - * Indicates whether the given path is absolute or not + * Indicates whether the given path is absolute or not. * * @param string $path A normalized path * - * @return boolean + * @return bool */ public static function isAbsolute($path) { @@ -57,7 +56,7 @@ public static function isAbsolute($path) } /** - * Returns the absolute prefix of the given path + * Returns the absolute prefix of the given path. * * @param string $path A normalized path * diff --git a/src/Gaufrette/Util/Size.php b/src/Gaufrette/Util/Size.php index 91537cdf1..97aae1c5b 100644 --- a/src/Gaufrette/Util/Size.php +++ b/src/Gaufrette/Util/Size.php @@ -3,18 +3,18 @@ namespace Gaufrette\Util; /** - * Utility class for file sizes + * Utility class for file sizes. * * @author Antoine Hérault */ class Size { /** - * Returns the size in bytes from the given content + * Returns the size in bytes from the given content. * * @param string $content * - * @return integer + * @return int * * @todo handle the case the mbstring is not loaded */ @@ -26,7 +26,7 @@ public static function fromContent($content) } /** - * Returns the size in bytes from the given file + * Returns the size in bytes from the given file. * * @param string $filename * From 39b377e5f83e033252bde2d2a3ab1ee029ca19c1 Mon Sep 17 00:00:00 2001 From: umpirsky Date: Sat, 5 Mar 2016 16:17:54 +0100 Subject: [PATCH 190/426] Make badges uniform wth shields.io Add code quality, packagist and license badge --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 7512f3913..720375cff 100644 --- a/README.md +++ b/README.md @@ -3,9 +3,12 @@ Gaufrette Gaufrette provides a filesystem abstraction layer. -[![Build Status](https://secure.travis-ci.org/KnpLabs/Gaufrette.png)](http://travis-ci.org/KnpLabs/Gaufrette) -[![Join the chat at https://gitter.im/KnpLabs/Gaufrette](https://badges.gitter.im/KnpLabs/Gaufrette.svg)](https://gitter.im/KnpLabs/Gaufrette) -[![Stories in Ready](https://badge.waffle.io/knplabs/gaufrette.png?label=ready&title=Ready)](https://waffle.io/knplabs/gaufrette) +[![Build Status](https://img.shields.io/travis/KnpLabs/Gaufrette/master.svg?style=flat-square)](http://travis-ci.org/KnpLabs/Gaufrette) +[![Quality Score](https://img.shields.io/scrutinizer/g/KnpLabs/Gaufrette.svg?style=flat-square)](https://scrutinizer-ci.com/g/KnpLabs/Gaufrette) +[![Packagist Version](https://img.shields.io/packagist/v/KnpLabs/Gaufrette.svg?style=flat-square)](https://packagist.org/packages/KnpLabs/Gaufrette) +[![Total Downloads](https://img.shields.io/packagist/dt/KnpLabs/Gaufrette.svg?style=flat-square)](https://packagist.org/packages/KnpLabs/Gaufrette) +[![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) +[![Join the chat at Gitter](https://img.shields.io/gitter/room/nwjs/nw.js.svg?style=flat-square)](https://gitter.im/KnpLabs/Gaufrette) Why use Gaufrette? ------------------ From f9101bc47cb80c19df249386777a48146f8cb439 Mon Sep 17 00:00:00 2001 From: umpirsky Date: Fri, 4 Mar 2016 15:12:57 +0100 Subject: [PATCH 191/426] Added league/flysystem as a dev dependency --- composer.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 90bf497f5..16f8f000c 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,8 @@ "herzult/php-ssh": "*", "phpunit/phpunit": "3.7.*", "microsoft/windowsazure": "dev-master", - "mikey179/vfsStream": "~1.2.0" + "mikey179/vfsStream": "~1.2.0", + "league/flysystem": "~1.0" }, "suggest": { "knplabs/knp-gaufrette-bundle": "to use with Symfony2", @@ -50,6 +51,7 @@ "amazonwebservices/aws-sdk-for-php": "to use the legacy Amazon S3 adapters", "doctrine/dbal": "to use the Doctrine DBAL adapter", "microsoft/windowsazure": "to use Microsoft Azure Blob Storage adapter", + "league/flysystem": "to use Flysystem adapters", "ext-zip": "to use the Zip adapter", "ext-apc": "to use the APC adapter", "ext-curl": "*", From 0c42e092634488844cd8f15c645cd78c7dd762ae Mon Sep 17 00:00:00 2001 From: umpirsky Date: Fri, 4 Mar 2016 15:33:07 +0100 Subject: [PATCH 192/426] Create new adapter docs --- doc/couscous.yml | 3 ++ doc/implementing-new-adapter.md | 57 +++++++++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 doc/implementing-new-adapter.md diff --git a/doc/couscous.yml b/doc/couscous.yml index 8ca6a6701..382a1a8dc 100644 --- a/doc/couscous.yml +++ b/doc/couscous.yml @@ -26,6 +26,9 @@ menu: streaming: text: Streaming relativeUrl: streaming.html + implementing-new-adapter: + text: Implementing new Adapter + relativeUrl: implementing-new-adapter.html adapters: name: Adapters items: diff --git a/doc/implementing-new-adapter.md b/doc/implementing-new-adapter.md new file mode 100644 index 000000000..066c51368 --- /dev/null +++ b/doc/implementing-new-adapter.md @@ -0,0 +1,57 @@ +--- +currentMenu: implementing-new-adapter +--- + +# Implementing new Adapter + +Let's say we want to support new storage system with existing Graufette API. +The way to do it is by implementing a new adapter. + +We will illustrate this by implementing example adapter for KnpStorage file system. + +## Spec BDD + +We encourage contributors to start with describing a new adapter: + +```bash +$ ./bin/phpspec describe Gaufrette/Adapter/KnpStorage +Specification for Gaufrette\Adapter\KnpStorage created in Gaufrette/spec/Gaufrette/Adapter/KnpStorageSpec.php. +``` + +We describe how our adapter is instantated and make sure it implements `Gaufrette\Adapter` interface: + +```php +beConstructedWith($storage); + } + + function it_is_adapter() + { + $this->shouldImplement('Gaufrette\Adapter'); + } +} + +``` + +To get the benefits of PHPSpec code generator we run: + +```bash +$ ./bin/phpspec run spec/Gaufrette/Adapter/KnpStorageSpec.php +``` + +You can continue to play with PHPSpec, read more on [PHPSpec website](http://phpspec.readthedocs.org/en/latest/). + +## Implementing adapter interface + +Now all we need to do is to make `Gaufrette\Adapter\KnpStorage` implement all `Gaufrette\Adapter` interface methods. + +## Contibute it + +Once you are sure your adapter is ready, share it with awesome Gaufrette community by submitting a pull request. + +Thank you for doing this, you are awesome! From 40bcf3a9bc5989be44982a400da910889cbf7a55 Mon Sep 17 00:00:00 2001 From: umpirsky Date: Fri, 4 Mar 2016 16:43:23 +0100 Subject: [PATCH 193/426] Flysistem adapter --- spec/Gaufrette/Adapter/FlysistemSpec.php | 70 +++++++++++++++++++ src/Gaufrette/Adapter/Flysistem.php | 88 ++++++++++++++++++++++++ 2 files changed, 158 insertions(+) create mode 100644 spec/Gaufrette/Adapter/FlysistemSpec.php create mode 100644 src/Gaufrette/Adapter/Flysistem.php diff --git a/spec/Gaufrette/Adapter/FlysistemSpec.php b/spec/Gaufrette/Adapter/FlysistemSpec.php new file mode 100644 index 000000000..562d4f5ee --- /dev/null +++ b/spec/Gaufrette/Adapter/FlysistemSpec.php @@ -0,0 +1,70 @@ +beConstructedWith($adapter, $config); + } + + function it_is_adapter() + { + $this->shouldImplement('Gaufrette\Adapter'); + } + + function it_reads_file(AdapterInterface $adapter) + { + $adapter->read('filename')->willReturn('Hello.'); + + $this->read('filename')->shouldReturn('Hello.'); + } + + function it_writes_file(AdapterInterface $adapter, Config $config) + { + $adapter->write('filename', 'Hello.', $config)->willReturn(array()); + + $this->write('filename', 'Hello.')->shouldReturn(array()); + } + + function it_checks_if_file_exists(AdapterInterface $adapter) + { + $adapter->has('filename')->willReturn(true); + + $this->exists('filename')->shouldReturn(true); + } + + function it_fetches_keys(AdapterInterface $adapter) + { + $adapter->listContents()->willReturn(array()); + + $this->keys()->shouldReturn(array()); + } + + function it_fetches_mtime(AdapterInterface $adapter) + { + $adapter->getTimestamp('filename')->willReturn(1457104978); + + $this->mtime('filename')->shouldReturn(1457104978); + } + + function it_deletes_file(AdapterInterface $adapter) + { + $adapter->delete('filename')->willReturn(true); + + $this->delete('filename')->shouldReturn(true); + } + + function it_renames_file(AdapterInterface $adapter) + { + $adapter->rename('oldfilename', 'newfilename')->willReturn(true); + + $this->rename('oldfilename', 'newfilename')->shouldReturn(true); + } +} diff --git a/src/Gaufrette/Adapter/Flysistem.php b/src/Gaufrette/Adapter/Flysistem.php new file mode 100644 index 000000000..7a3940090 --- /dev/null +++ b/src/Gaufrette/Adapter/Flysistem.php @@ -0,0 +1,88 @@ +adapter = $adapter; + $this->config = Util::ensureConfig($config); + } + + /** + * {@inheritdoc} + */ + public function read($key) + { + return $this->adapter->read($key); + } + + /** + * {@inheritdoc} + */ + public function write($key, $content) + { + return $this->adapter->write($key, $content, $this->config); + } + + /** + * {@inheritdoc} + */ + public function exists($key) + { + return $this->adapter->has($key); + } + + /** + * {@inheritdoc} + */ + public function keys() + { + return $this->adapter->listContents(); + } + + /** + * {@inheritdoc} + */ + public function mtime($key) + { + return $this->adapter->getTimestamp($key); + } + + /** + * {@inheritdoc} + */ + public function delete($key) + { + return $this->adapter->delete($key); + } + + /** + * {@inheritdoc} + */ + public function rename($sourceKey, $targetKey) + { + return $this->adapter->rename($sourceKey, $targetKey); + } + + /** + * {@inheritdoc} + */ + public function isDirectory($key) + { + // TODO: create proper exception class if Flysistem does not support isDirectory() + throw new \BadMethodCallException('isDirectory is not supported by this adapter.'); + } +} From cecb9c88be85ad388c9c6d47dd905a27f12a465a Mon Sep 17 00:00:00 2001 From: umpirsky Date: Fri, 4 Mar 2016 16:49:31 +0100 Subject: [PATCH 194/426] Rename Flysystem --- spec/Gaufrette/Adapter/{FlysistemSpec.php => FlysystemSpec.php} | 2 +- src/Gaufrette/Adapter/{Flysistem.php => Flysystem.php} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename spec/Gaufrette/Adapter/{FlysistemSpec.php => FlysystemSpec.php} (97%) rename src/Gaufrette/Adapter/{Flysistem.php => Flysystem.php} (97%) diff --git a/spec/Gaufrette/Adapter/FlysistemSpec.php b/spec/Gaufrette/Adapter/FlysystemSpec.php similarity index 97% rename from spec/Gaufrette/Adapter/FlysistemSpec.php rename to spec/Gaufrette/Adapter/FlysystemSpec.php index 562d4f5ee..415212081 100644 --- a/spec/Gaufrette/Adapter/FlysistemSpec.php +++ b/spec/Gaufrette/Adapter/FlysystemSpec.php @@ -7,7 +7,7 @@ use League\Flysystem\AdapterInterface; use League\Flysystem\Config; -class FlysistemSpec extends ObjectBehavior +class FlysystemSpec extends ObjectBehavior { function let(AdapterInterface $adapter, Config $config) { diff --git a/src/Gaufrette/Adapter/Flysistem.php b/src/Gaufrette/Adapter/Flysystem.php similarity index 97% rename from src/Gaufrette/Adapter/Flysistem.php rename to src/Gaufrette/Adapter/Flysystem.php index 7a3940090..fb8a0e513 100644 --- a/src/Gaufrette/Adapter/Flysistem.php +++ b/src/Gaufrette/Adapter/Flysystem.php @@ -6,7 +6,7 @@ use League\Flysystem\AdapterInterface; use League\Flysystem\Util; -class Flysistem implements Adapter +class Flysystem implements Adapter { private $adapter; private $config; From 55f9d0f2bbc773dbe4d0d530a907fc3202fd666d Mon Sep 17 00:00:00 2001 From: umpirsky Date: Fri, 4 Mar 2016 16:50:23 +0100 Subject: [PATCH 195/426] Fix typo --- doc/implementing-new-adapter.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/implementing-new-adapter.md b/doc/implementing-new-adapter.md index 066c51368..91ad29bf8 100644 --- a/doc/implementing-new-adapter.md +++ b/doc/implementing-new-adapter.md @@ -48,7 +48,7 @@ You can continue to play with PHPSpec, read more on [PHPSpec website](http://php ## Implementing adapter interface -Now all we need to do is to make `Gaufrette\Adapter\KnpStorage` implement all `Gaufrette\Adapter` interface methods. +Now all we need to do is to make `Gaufrette\Adapter\KnpStorage` implements all `Gaufrette\Adapter` interface methods. ## Contibute it From e85694dfe1fd7fbaec3eb5eddb9409f4320437b7 Mon Sep 17 00:00:00 2001 From: umpirsky Date: Sat, 5 Mar 2016 11:46:02 +0100 Subject: [PATCH 196/426] Flysistem returns damn array when reading files --- spec/Gaufrette/Adapter/FlysystemSpec.php | 2 +- src/Gaufrette/Adapter/Flysystem.php | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/spec/Gaufrette/Adapter/FlysystemSpec.php b/spec/Gaufrette/Adapter/FlysystemSpec.php index 415212081..b8b3a358c 100644 --- a/spec/Gaufrette/Adapter/FlysystemSpec.php +++ b/spec/Gaufrette/Adapter/FlysystemSpec.php @@ -21,7 +21,7 @@ function it_is_adapter() function it_reads_file(AdapterInterface $adapter) { - $adapter->read('filename')->willReturn('Hello.'); + $adapter->read('filename')->willReturn(['contents' => 'Hello.']); $this->read('filename')->shouldReturn('Hello.'); } diff --git a/src/Gaufrette/Adapter/Flysystem.php b/src/Gaufrette/Adapter/Flysystem.php index fb8a0e513..8dc73b47d 100644 --- a/src/Gaufrette/Adapter/Flysystem.php +++ b/src/Gaufrette/Adapter/Flysystem.php @@ -26,7 +26,8 @@ public function __construct(AdapterInterface $adapter, $config = null) */ public function read($key) { - return $this->adapter->read($key); + // TODO: handle false returned + return $this->adapter->read($key)['contents']; } /** From bf11e828af20049f935e83da5b65bdf4edaac67b Mon Sep 17 00:00:00 2001 From: umpirsky Date: Sat, 5 Mar 2016 11:58:47 +0100 Subject: [PATCH 197/426] Map flysistem keys structure --- spec/Gaufrette/Adapter/FlysystemSpec.php | 11 ++++++++--- src/Gaufrette/Adapter/Flysystem.php | 4 +++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/spec/Gaufrette/Adapter/FlysystemSpec.php b/spec/Gaufrette/Adapter/FlysystemSpec.php index b8b3a358c..1ea97e36d 100644 --- a/spec/Gaufrette/Adapter/FlysystemSpec.php +++ b/spec/Gaufrette/Adapter/FlysystemSpec.php @@ -42,9 +42,14 @@ function it_checks_if_file_exists(AdapterInterface $adapter) function it_fetches_keys(AdapterInterface $adapter) { - $adapter->listContents()->willReturn(array()); - - $this->keys()->shouldReturn(array()); + $adapter->listContents()->willReturn([[ + 'path' => 'folder', + 'timestamp' => 1457104978, + 'size' => 22, + 'type' => 'dir', + ]]); + + $this->keys()->shouldReturn(['folder']); } function it_fetches_mtime(AdapterInterface $adapter) diff --git a/src/Gaufrette/Adapter/Flysystem.php b/src/Gaufrette/Adapter/Flysystem.php index 8dc73b47d..703ad07f1 100644 --- a/src/Gaufrette/Adapter/Flysystem.php +++ b/src/Gaufrette/Adapter/Flysystem.php @@ -51,7 +51,9 @@ public function exists($key) */ public function keys() { - return $this->adapter->listContents(); + return array_map(function($content) { + return $content['path']; + }, $this->adapter->listContents()); } /** From f9ad45ff71efbc1088a1b4b5a89affc4c123b158 Mon Sep 17 00:00:00 2001 From: umpirsky Date: Sat, 5 Mar 2016 12:35:44 +0100 Subject: [PATCH 198/426] Flysistem adapter is list keys aware --- spec/Gaufrette/Adapter/FlysystemSpec.php | 20 ++++++++++++++++++ src/Gaufrette/Adapter/Flysystem.php | 27 +++++++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/spec/Gaufrette/Adapter/FlysystemSpec.php b/spec/Gaufrette/Adapter/FlysystemSpec.php index 1ea97e36d..07311e545 100644 --- a/spec/Gaufrette/Adapter/FlysystemSpec.php +++ b/spec/Gaufrette/Adapter/FlysystemSpec.php @@ -19,6 +19,11 @@ function it_is_adapter() $this->shouldImplement('Gaufrette\Adapter'); } + function it_is_list_keys_aware() + { + $this->shouldImplement('Gaufrette\Adapter\ListKeysAware'); + } + function it_reads_file(AdapterInterface $adapter) { $adapter->read('filename')->willReturn(['contents' => 'Hello.']); @@ -52,6 +57,21 @@ function it_fetches_keys(AdapterInterface $adapter) $this->keys()->shouldReturn(['folder']); } + function it_lists_keys(AdapterInterface $adapter) + { + $adapter->listContents()->willReturn([[ + 'path' => 'folder', + 'timestamp' => 1457104978, + 'size' => 22, + 'type' => 'dir', + ]]); + + $this->listKeys()->shouldReturn([ + 'keys' => [], + 'dirs' => ['folder'], + ]); + } + function it_fetches_mtime(AdapterInterface $adapter) { $adapter->getTimestamp('filename')->willReturn(1457104978); diff --git a/src/Gaufrette/Adapter/Flysystem.php b/src/Gaufrette/Adapter/Flysystem.php index 703ad07f1..e95ba79dd 100644 --- a/src/Gaufrette/Adapter/Flysystem.php +++ b/src/Gaufrette/Adapter/Flysystem.php @@ -3,10 +3,11 @@ namespace Gaufrette\Adapter; use Gaufrette\Adapter; +use Gaufrette\Adapter\ListKeysAware; use League\Flysystem\AdapterInterface; use League\Flysystem\Util; -class Flysystem implements Adapter +class Flysystem implements Adapter, ListKeysAware { private $adapter; private $config; @@ -56,6 +57,30 @@ public function keys() }, $this->adapter->listContents()); } + /** + * {@inheritdoc} + */ + public function listKeys($prefix = '') + { + $dirs = []; + $keys = []; + + foreach ($this->adapter->listContents() as $content) { + if (empty($prefix) || 0 === strpos($content['path'], $prefix)) { + if ('dir' === $content['type']) { + $dirs[] = $content['path']; + } else { + $keys[] = $content['path']; + } + } + } + + return [ + 'keys' => $keys, + 'dirs' => $dirs + ]; + } + /** * {@inheritdoc} */ From 896721b3249fb7469b3ddf502d3b0a75393f6e02 Mon Sep 17 00:00:00 2001 From: umpirsky Date: Sat, 5 Mar 2016 12:46:49 +0100 Subject: [PATCH 199/426] Added UnsupportedAdapterMethodException --- spec/Gaufrette/Adapter/FlysystemSpec.php | 5 +++++ src/Gaufrette/Adapter/Flysystem.php | 4 ++-- .../Exception/UnsupportedAdapterMethodException.php | 9 +++++++++ 3 files changed, 16 insertions(+), 2 deletions(-) create mode 100644 src/Gaufrette/Exception/UnsupportedAdapterMethodException.php diff --git a/spec/Gaufrette/Adapter/FlysystemSpec.php b/spec/Gaufrette/Adapter/FlysystemSpec.php index 07311e545..cefc46ef4 100644 --- a/spec/Gaufrette/Adapter/FlysystemSpec.php +++ b/spec/Gaufrette/Adapter/FlysystemSpec.php @@ -92,4 +92,9 @@ function it_renames_file(AdapterInterface $adapter) $this->rename('oldfilename', 'newfilename')->shouldReturn(true); } + + function it_does_not_support_is_directory(AdapterInterface $adapter) + { + $this->shouldThrow('Gaufrette\Exception\UnsupportedAdapterMethodException')->duringisDirectory('folder'); + } } diff --git a/src/Gaufrette/Adapter/Flysystem.php b/src/Gaufrette/Adapter/Flysystem.php index e95ba79dd..c44519f88 100644 --- a/src/Gaufrette/Adapter/Flysystem.php +++ b/src/Gaufrette/Adapter/Flysystem.php @@ -4,6 +4,7 @@ use Gaufrette\Adapter; use Gaufrette\Adapter\ListKeysAware; +use Gaufrette\Exception\UnsupportedAdapterMethodException; use League\Flysystem\AdapterInterface; use League\Flysystem\Util; @@ -110,7 +111,6 @@ public function rename($sourceKey, $targetKey) */ public function isDirectory($key) { - // TODO: create proper exception class if Flysistem does not support isDirectory() - throw new \BadMethodCallException('isDirectory is not supported by this adapter.'); + throw new UnsupportedAdapterMethodException('isDirectory is not supported by this adapter.'); } } diff --git a/src/Gaufrette/Exception/UnsupportedAdapterMethodException.php b/src/Gaufrette/Exception/UnsupportedAdapterMethodException.php new file mode 100644 index 000000000..8f1bb23ba --- /dev/null +++ b/src/Gaufrette/Exception/UnsupportedAdapterMethodException.php @@ -0,0 +1,9 @@ + Date: Sat, 5 Mar 2016 15:02:04 +0100 Subject: [PATCH 200/426] Flysystem adapter documentation --- doc/adapters/flysystem.md | 38 ++++++++++++++++++++++++++++++++++++++ doc/couscous.yml | 3 +++ 2 files changed, 41 insertions(+) create mode 100644 doc/adapters/flysystem.md diff --git a/doc/adapters/flysystem.md b/doc/adapters/flysystem.md new file mode 100644 index 000000000..62156d5e4 --- /dev/null +++ b/doc/adapters/flysystem.md @@ -0,0 +1,38 @@ +--- +currentMenu: flysystem +--- + +# Flysystem + +Folks from [thephpleague](http://thephpleague.com/) built extraordinary [Flysystem](https://github.com/thephpleague/flysystem) package which does exactly the same thing as Gaufrette, but with slightly different API. + +We wanted to make Gaufrette compatible with as many systems as possible, and didn't want to reinvent the wheel. +So we built a Flysystem adapter. + +With this adapter you can use any [Flysystem adapter](https://github.com/thephpleague/flysystem#adapters) with no performance penalties. It is just a tiny layer that makes Gaufrette talk to Flysystem adapters. + +## Example + +We will show using Flysystem adapter on Dropbox example. + +First, you need to install Flysystem Dropbox adapter through composer: + +```bash +$ composer require league/flysystem-dropbox +``` + +Now, just wrap Dropbox adapter into Gaufrette Flysystem adapter. + +```php +', '') + ) +); + +$filesystem = new Gaufrette\Filesystem($adapter); +``` + +As said above, same pattern can be applied to any [Flysystem adapter](https://github.com/thephpleague/flysystem#adapters). diff --git a/doc/couscous.yml b/doc/couscous.yml index 382a1a8dc..6f59946a3 100644 --- a/doc/couscous.yml +++ b/doc/couscous.yml @@ -47,6 +47,9 @@ menu: doctrine-dbal: text: Doctrine DBAL relativeUrl: adapters/doctrine-dbal.html + flysystem: + text: Flysystem + relativeUrl: adapters/flysystem.html ftp: text: Ftp relativeUrl: adapters/ftp.html From 15ab639aad7d2c4baaa2fc8a7ba8ed72acb995e4 Mon Sep 17 00:00:00 2001 From: umpirsky Date: Sat, 5 Mar 2016 15:03:08 +0100 Subject: [PATCH 201/426] Remove false todo --- src/Gaufrette/Adapter/Flysystem.php | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Gaufrette/Adapter/Flysystem.php b/src/Gaufrette/Adapter/Flysystem.php index c44519f88..5ec764ae7 100644 --- a/src/Gaufrette/Adapter/Flysystem.php +++ b/src/Gaufrette/Adapter/Flysystem.php @@ -28,7 +28,6 @@ public function __construct(AdapterInterface $adapter, $config = null) */ public function read($key) { - // TODO: handle false returned return $this->adapter->read($key)['contents']; } From f6345963ffc318a6b20f1764d696ea30a3568d09 Mon Sep 17 00:00:00 2001 From: umpirsky Date: Sat, 5 Mar 2016 15:04:08 +0100 Subject: [PATCH 202/426] CS fixes --- src/Gaufrette/Adapter/Flysystem.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/Gaufrette/Adapter/Flysystem.php b/src/Gaufrette/Adapter/Flysystem.php index 5ec764ae7..5c961ea79 100644 --- a/src/Gaufrette/Adapter/Flysystem.php +++ b/src/Gaufrette/Adapter/Flysystem.php @@ -3,7 +3,6 @@ namespace Gaufrette\Adapter; use Gaufrette\Adapter; -use Gaufrette\Adapter\ListKeysAware; use Gaufrette\Exception\UnsupportedAdapterMethodException; use League\Flysystem\AdapterInterface; use League\Flysystem\Util; @@ -52,7 +51,7 @@ public function exists($key) */ public function keys() { - return array_map(function($content) { + return array_map(function ($content) { return $content['path']; }, $this->adapter->listContents()); } @@ -77,7 +76,7 @@ public function listKeys($prefix = '') return [ 'keys' => $keys, - 'dirs' => $dirs + 'dirs' => $dirs, ]; } From d36b3758091aa352d344b8a413727a7ebd9fe5f8 Mon Sep 17 00:00:00 2001 From: umpirsky Date: Mon, 7 Mar 2016 14:15:41 +0100 Subject: [PATCH 203/426] Added phpdocs --- src/Gaufrette/Adapter/Flysystem.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Gaufrette/Adapter/Flysystem.php b/src/Gaufrette/Adapter/Flysystem.php index 5c961ea79..0e1c6dead 100644 --- a/src/Gaufrette/Adapter/Flysystem.php +++ b/src/Gaufrette/Adapter/Flysystem.php @@ -9,7 +9,14 @@ class Flysystem implements Adapter, ListKeysAware { + /** + * @var AdapterInterface + */ private $adapter; + + /** + * @var Config + */ private $config; /** From 1964b6a308908af8da31224077400e8e2d8fbbd9 Mon Sep 17 00:00:00 2001 From: umpirsky Date: Mon, 7 Mar 2016 14:20:57 +0100 Subject: [PATCH 204/426] Doc fixes reported by Andrew --- doc/adapters/flysystem.md | 2 +- doc/implementing-new-adapter.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/adapters/flysystem.md b/doc/adapters/flysystem.md index 62156d5e4..d714e8570 100644 --- a/doc/adapters/flysystem.md +++ b/doc/adapters/flysystem.md @@ -4,7 +4,7 @@ currentMenu: flysystem # Flysystem -Folks from [thephpleague](http://thephpleague.com/) built extraordinary [Flysystem](https://github.com/thephpleague/flysystem) package which does exactly the same thing as Gaufrette, but with slightly different API. +Folks from [thephpleague](http://thephpleague.com/) have built extraordinary [Flysystem](https://github.com/thephpleague/flysystem) package which does exactly the same thing as Gaufrette, but with slightly different API. We wanted to make Gaufrette compatible with as many systems as possible, and didn't want to reinvent the wheel. So we built a Flysystem adapter. diff --git a/doc/implementing-new-adapter.md b/doc/implementing-new-adapter.md index 91ad29bf8..db0d4754b 100644 --- a/doc/implementing-new-adapter.md +++ b/doc/implementing-new-adapter.md @@ -48,7 +48,7 @@ You can continue to play with PHPSpec, read more on [PHPSpec website](http://php ## Implementing adapter interface -Now all we need to do is to make `Gaufrette\Adapter\KnpStorage` implements all `Gaufrette\Adapter` interface methods. +Now all we need to do is to make sure `KnpStorage` implements all Gaufrette `Adapterinterface` methods. ## Contibute it From cc708117b6fbe12545f7dec7894641b5314dc8d8 Mon Sep 17 00:00:00 2001 From: h-hishida Date: Tue, 9 Feb 2016 14:40:54 +0900 Subject: [PATCH 205/426] [doc][AwsS3] fixed credentials in arguments for S3Client --- doc/adapters/aws-s3.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/doc/adapters/aws-s3.md b/doc/adapters/aws-s3.md index 5ca835fbc..9166ea603 100644 --- a/doc/adapters/aws-s3.md +++ b/doc/adapters/aws-s3.md @@ -19,8 +19,10 @@ use Gaufrette\Adapter\AwsS3 as AwsS3Adapter; use Gaufrette\Filesystem; $s3client = S3Client::factory(array( - 'key' => 'your_key_here', - 'secret' => 'your_secret', + 'credentials' => array( + 'key' => 'your_key_here', + 'secret' => 'your_secret', + ), 'version' => 'latest', 'region' => 'eu-west-1', )); From fa8033dcd02c2f429a10b8b66a8ffd9acfff465c Mon Sep 17 00:00:00 2001 From: Albin Kerouaton Date: Wed, 9 Mar 2016 23:57:47 +0100 Subject: [PATCH 206/426] Wrap native dirname function in order to handle only UNIX-style paths As stated by php doc (http://php.net/manual/en/function.dirname.php), dirname can return either slash or back-slash as directory separator. --- spec/Gaufrette/Util/PathSpec.php | 5 +++++ src/Gaufrette/Adapter/AmazonS3.php | 4 ++-- src/Gaufrette/Adapter/Dropbox.php | 4 ++-- src/Gaufrette/Adapter/Ftp.php | 10 +++++----- src/Gaufrette/Adapter/Local.php | 4 ++-- src/Gaufrette/Adapter/PhpseclibSftp.php | 4 ++-- src/Gaufrette/Adapter/Sftp.php | 8 ++++---- src/Gaufrette/Stream/Local.php | 2 +- src/Gaufrette/Util/Path.php | 14 ++++++++++++++ 9 files changed, 37 insertions(+), 18 deletions(-) diff --git a/spec/Gaufrette/Util/PathSpec.php b/spec/Gaufrette/Util/PathSpec.php index 6be3c02fa..f47164017 100644 --- a/spec/Gaufrette/Util/PathSpec.php +++ b/spec/Gaufrette/Util/PathSpec.php @@ -23,4 +23,9 @@ function it_normalizes_file_path() $this->normalize('/home/other/./new')->shouldReturn('/home/other/new'); $this->normalize('protocol://home/other.txt')->shouldReturn('protocol://home/other.txt'); } + + function it_returns_unix_style_dirname() + { + $this->dirname('a/test/path')->shouldReturn('a/test'); + } } diff --git a/src/Gaufrette/Adapter/AmazonS3.php b/src/Gaufrette/Adapter/AmazonS3.php index 8b65b8c3b..71b164de2 100644 --- a/src/Gaufrette/Adapter/AmazonS3.php +++ b/src/Gaufrette/Adapter/AmazonS3.php @@ -200,8 +200,8 @@ public function keys() $keys = array(); foreach ($list as $file) { - if ('.' !== dirname($file)) { - $keys[] = dirname($file); + if ('.' !== \Gaufrette\Util\Path::dirname($file)) { + $keys[] = \Gaufrette\Util\Path::dirname($file); } $keys[] = $file; } diff --git a/src/Gaufrette/Adapter/Dropbox.php b/src/Gaufrette/Adapter/Dropbox.php index 463a05ead..24ae5e21f 100644 --- a/src/Gaufrette/Adapter/Dropbox.php +++ b/src/Gaufrette/Adapter/Dropbox.php @@ -137,8 +137,8 @@ public function keys() foreach ($metadata['contents'] as $value) { $file = ltrim($value['path'], '/'); $keys[] = $file; - if ('.' !== dirname($file)) { - $keys[] = dirname($file); + if ('.' !== \Gaufrette\Util\Path::dirname($file)) { + $keys[] = \Gaufrette\Util\Path::dirname($file); } } sort($keys); diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index b7e24036d..13423472b 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -77,7 +77,7 @@ public function write($key, $content) $this->ensureDirectoryExists($this->directory, $this->create); $path = $this->computePath($key); - $directory = str_replace('\\', '/', dirname($path)); + $directory = str_replace('\\', '/', \Gaufrette\Util\Path::dirname($path)); $this->ensureDirectoryExists($directory, true); @@ -106,7 +106,7 @@ public function rename($sourceKey, $targetKey) $sourcePath = $this->computePath($sourceKey); $targetPath = $this->computePath($targetKey); - $this->ensureDirectoryExists(str_replace('\\', '/', dirname($targetPath)), true); + $this->ensureDirectoryExists(str_replace('\\', '/', \Gaufrette\Util\Path::dirname($targetPath)), true); return ftp_rename($this->getConnection(), $sourcePath, $targetPath); } @@ -119,7 +119,7 @@ public function exists($key) $this->ensureDirectoryExists($this->directory, $this->create); $file = $this->computePath($key); - $lines = ftp_rawlist($this->getConnection(), '-al ' . str_replace('\\', '/', dirname($file))); + $lines = ftp_rawlist($this->getConnection(), '-al ' . str_replace('\\', '/', \Gaufrette\Util\Path::dirname($file))); if (false === $lines) { return false; @@ -273,7 +273,7 @@ public function createFile($key, Filesystem $filesystem) $file = new File($key, $filesystem); if (!array_key_exists($key, $this->fileData)) { - $directory = dirname($key) == '.' ? '' : str_replace('\\', '/', dirname($key)); + $directory = \Gaufrette\Util\Path::dirname($key) == '.' ? '' : str_replace('\\', '/', \Gaufrette\Util\Path::dirname($key)); $this->listDirectory($directory); } @@ -319,7 +319,7 @@ protected function ensureDirectoryExists($directory, $create = false) protected function createDirectory($directory) { // create parent directory if needed - $parent = str_replace('\\', '/', dirname($directory)); + $parent = str_replace('\\', '/', \Gaufrette\Util\Path::dirname($directory)); if (!$this->isDir($parent)) { $this->createDirectory($parent); } diff --git a/src/Gaufrette/Adapter/Local.php b/src/Gaufrette/Adapter/Local.php index dd0cd723a..1df50e2fd 100644 --- a/src/Gaufrette/Adapter/Local.php +++ b/src/Gaufrette/Adapter/Local.php @@ -58,7 +58,7 @@ public function read($key) public function write($key, $content) { $path = $this->computePath($key); - $this->ensureDirectoryExists(dirname($path), true); + $this->ensureDirectoryExists(\Gaufrette\Util\Path::dirname($path), true); return file_put_contents($path, $content); } @@ -69,7 +69,7 @@ public function write($key, $content) public function rename($sourceKey, $targetKey) { $targetPath = $this->computePath($targetKey); - $this->ensureDirectoryExists(dirname($targetPath), true); + $this->ensureDirectoryExists(\Gaufrette\Util\Path::dirname($targetPath), true); return rename($this->computePath($sourceKey), $targetPath); } diff --git a/src/Gaufrette/Adapter/PhpseclibSftp.php b/src/Gaufrette/Adapter/PhpseclibSftp.php index fd3a7c212..1c8ca0d27 100644 --- a/src/Gaufrette/Adapter/PhpseclibSftp.php +++ b/src/Gaufrette/Adapter/PhpseclibSftp.php @@ -49,7 +49,7 @@ public function rename($sourceKey, $targetKey) $sourcePath = $this->computePath($sourceKey); $targetPath = $this->computePath($targetKey); - $this->ensureDirectoryExists(dirname($targetPath), true); + $this->ensureDirectoryExists(\Gaufrette\Util\Path::dirname($targetPath), true); return $this->sftp->rename($sourcePath, $targetPath); } @@ -62,7 +62,7 @@ public function write($key, $content) $this->initialize(); $path = $this->computePath($key); - $this->ensureDirectoryExists(dirname($path), true); + $this->ensureDirectoryExists(\Gaufrette\Util\Path::dirname($path), true); if ($this->sftp->put($path, $content)) { return $this->sftp->size($path); } diff --git a/src/Gaufrette/Adapter/Sftp.php b/src/Gaufrette/Adapter/Sftp.php index 69907b40b..29f314b05 100644 --- a/src/Gaufrette/Adapter/Sftp.php +++ b/src/Gaufrette/Adapter/Sftp.php @@ -46,7 +46,7 @@ public function rename($sourceKey, $targetKey) $sourcePath = $this->computePath($sourceKey); $targetPath = $this->computePath($targetKey); - $this->ensureDirectoryExists(dirname($targetPath), true); + $this->ensureDirectoryExists(\Gaufrette\Util\Path::dirname($targetPath), true); return $this->sftp->rename($sourcePath, $targetPath); } @@ -59,7 +59,7 @@ public function write($key, $content) $this->initialize(); $path = $this->computePath($key); - $this->ensureDirectoryExists(dirname($path), true); + $this->ensureDirectoryExists(\Gaufrette\Util\Path::dirname($path), true); $numBytes = $this->sftp->write($path, $content); return $numBytes; @@ -101,8 +101,8 @@ public function keys() $dirs = array(); foreach ($files as $file) { - if ('.' !== dirname($file)) { - $dirs[] = dirname($file); + if ('.' !== \Gaufrette\Util\Path::dirname($file)) { + $dirs[] = \Gaufrette\Util\Path::dirname($file); } } diff --git a/src/Gaufrette/Stream/Local.php b/src/Gaufrette/Stream/Local.php index 801d9b284..446f8786e 100644 --- a/src/Gaufrette/Stream/Local.php +++ b/src/Gaufrette/Stream/Local.php @@ -29,7 +29,7 @@ public function __construct($path) */ public function open(StreamMode $mode) { - $baseDirPath = dirname($this->path); + $baseDirPath = \Gaufrette\Util\Path::dirname($this->path); if ($mode->allowsWrite() && !is_dir($baseDirPath)) { @mkdir($baseDirPath, 0755, true); } diff --git a/src/Gaufrette/Util/Path.php b/src/Gaufrette/Util/Path.php index 3903ad47d..d22e1ca5d 100644 --- a/src/Gaufrette/Util/Path.php +++ b/src/Gaufrette/Util/Path.php @@ -72,4 +72,18 @@ public static function getAbsolutePrefix($path) return strtolower($matches['prefix']); } + + /** + * Wrap native dirname function in order to handle only UNIX-style paths + * + * @param string $path + * + * @return string + * + * @see http://php.net/manual/en/function.dirname.php + */ + public static function dirname($path) + { + return str_replace('\\', '/', \dirname($path)); + } } From 60f5ed7a54122ac0c0dfb64c05cd02866016fda2 Mon Sep 17 00:00:00 2001 From: Ryan McEvoy Date: Tue, 22 Mar 2016 11:53:44 +0000 Subject: [PATCH 207/426] Fix for "name already in use" Fix for "Cannot use phpseclib\Net\SFTP as SFTP because the name is already in use" when a compiler pass is in use on a Symfony Framework. --- src/Gaufrette/Adapter/PhpseclibSftp.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Gaufrette/Adapter/PhpseclibSftp.php b/src/Gaufrette/Adapter/PhpseclibSftp.php index fd3a7c212..d49509430 100644 --- a/src/Gaufrette/Adapter/PhpseclibSftp.php +++ b/src/Gaufrette/Adapter/PhpseclibSftp.php @@ -3,7 +3,7 @@ namespace Gaufrette\Adapter; use Gaufrette\Adapter; -use phpseclib\Net\SFTP; +use phpseclib\Net\SFTP as SecLibSFTP; use Gaufrette\Filesystem; use Gaufrette\File; @@ -17,14 +17,14 @@ class PhpseclibSftp implements Adapter, protected $initialized = false; /** - * @param SFTP $sftp An Sftp instance + * @param SecLibSFTP $sftp An Sftp instance * @param string $directory The distant directory * @param bool $create Whether to create the remote directory if it * does not exist * @param string|null $username SFTP user name * @param string|null $password SFTP user password */ - public function __construct(SFTP $sftp, $directory = null, $create = false, $username = null, $password = null) + public function __construct(SecLibSFTP $sftp, $directory = null, $create = false, $username = null, $password = null) { $this->sftp = $sftp; $this->directory = $directory; From 8b6664442c034ca459e186f2ce5b1b52b556cf18 Mon Sep 17 00:00:00 2001 From: Gareth Jones Date: Mon, 28 Mar 2016 16:17:51 +0100 Subject: [PATCH 208/426] Provide file mime type for InMemory adapter --- spec/Gaufrette/Adapter/InMemorySpec.php | 10 ++++++++++ src/Gaufrette/Adapter/InMemory.php | 13 ++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/spec/Gaufrette/Adapter/InMemorySpec.php b/spec/Gaufrette/Adapter/InMemorySpec.php index fea6610df..d895201a0 100644 --- a/spec/Gaufrette/Adapter/InMemorySpec.php +++ b/spec/Gaufrette/Adapter/InMemorySpec.php @@ -19,6 +19,16 @@ function it_is_adapter() $this->shouldHaveType('Gaufrette\Adapter'); } + function it_is_a_mime_type_provider() + { + $this->shouldHaveType('Gaufrette\Adapter\MimeTypeProvider'); + } + + function it_gets_the_file_mime_type() + { + $this->mimeType('filename')->shouldReturn('text/plain'); + } + function it_reads_file() { $this->read('filename')->shouldReturn('content'); diff --git a/src/Gaufrette/Adapter/InMemory.php b/src/Gaufrette/Adapter/InMemory.php index 364b2cffb..80fd15f16 100644 --- a/src/Gaufrette/Adapter/InMemory.php +++ b/src/Gaufrette/Adapter/InMemory.php @@ -12,7 +12,8 @@ * * @author Antoine Hérault */ -class InMemory implements Adapter +class InMemory implements Adapter, + MimeTypeProvider { protected $files = array(); @@ -137,4 +138,14 @@ public function isDirectory($path) { return false; } + + /** + * {@inheritdoc} + */ + public function mimeType($key) + { + $fileInfo = new \finfo(FILEINFO_MIME_TYPE); + + return $fileInfo->buffer($this->files[$key]['content']); + } } From 60120e50bf2639496d3442a71358f84d925eff87 Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Sat, 9 Apr 2016 00:36:58 +0300 Subject: [PATCH 209/426] Remove login/password from phpseclib adapter, closes #403 --- doc/adapters/phpseclib-sftp.md | 5 ++++- src/Gaufrette/Adapter/PhpseclibSftp.php | 4 +--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/doc/adapters/phpseclib-sftp.md b/doc/adapters/phpseclib-sftp.md index cf281f8f7..c08ed9116 100644 --- a/doc/adapters/phpseclib-sftp.md +++ b/doc/adapters/phpseclib-sftp.md @@ -22,6 +22,9 @@ composer require phpseclib/phpseclib:^2.0 $sftp = new phpseclib\Net\SFTP($host = 'localhost', $port = 22); -$adapter = new Gaufrette\Adapter\PhpseclibSftp($sftp, $distantDirectory = null, $createDirectoryIfDoesntExist = false, $username = 'gaufrette', $password = 'gaufrette'); +//now you need to login manually with the lib +$sftp->login('foo', 'bar'); + +$adapter = new Gaufrette\Adapter\PhpseclibSftp($sftp, $distantDirectory = null, $createDirectoryIfDoesntExist = false); $filesystem = new Gaufrette\Filesystem($adapter); ``` diff --git a/src/Gaufrette/Adapter/PhpseclibSftp.php b/src/Gaufrette/Adapter/PhpseclibSftp.php index d49509430..fa34fe4fe 100644 --- a/src/Gaufrette/Adapter/PhpseclibSftp.php +++ b/src/Gaufrette/Adapter/PhpseclibSftp.php @@ -21,10 +21,8 @@ class PhpseclibSftp implements Adapter, * @param string $directory The distant directory * @param bool $create Whether to create the remote directory if it * does not exist - * @param string|null $username SFTP user name - * @param string|null $password SFTP user password */ - public function __construct(SecLibSFTP $sftp, $directory = null, $create = false, $username = null, $password = null) + public function __construct(SecLibSFTP $sftp, $directory = null, $create = false) { $this->sftp = $sftp; $this->directory = $directory; From cb64fb1d55324d0e23bd337a217dc5362c1a2c8d Mon Sep 17 00:00:00 2001 From: Paul Weber Date: Tue, 10 May 2016 11:16:57 +0200 Subject: [PATCH 210/426] Added new utf8 mode flag. --- src/Gaufrette/Adapter/Ftp.php | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index b7e24036d..0284f87d1 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -26,6 +26,7 @@ class Ftp implements Adapter, protected $mode; protected $ssl; protected $fileData = array(); + protected $utf8; /** * @param string $directory The directory to use in the ftp server @@ -47,6 +48,7 @@ public function __construct($directory, $host, $options = array()) $this->create = isset($options['create']) ? $options['create'] : false; $this->mode = isset($options['mode']) ? $options['mode'] : FTP_BINARY; $this->ssl = isset($options['ssl']) ? $options['ssl'] : false; + $this->utf8 = isset($options['utf8']) ? $options['utf8'] : false; } /** @@ -522,6 +524,11 @@ private function connect() throw new \RuntimeException('Could not turn passive mode on.'); } + // enable utf8 mode if configured + if($this->utf8 == true) { + ftp_raw($this->connection, "OPTS UTF8 ON"); + } + // ensure the adapter's directory exists if ('/' !== $this->directory) { try { From b0a11603f3593695d06e264bf360f3cc7e12c653 Mon Sep 17 00:00:00 2001 From: Nicolas Date: Fri, 20 May 2016 16:37:30 +0100 Subject: [PATCH 211/426] Updated new Azure namespaces. --- composer.json | 3 +++ src/Gaufrette/Adapter/AzureBlobStorage.php | 16 ++++++++-------- .../AzureBlobStorage/BlobProxyFactory.php | 2 +- .../BlobProxyFactoryInterface.php | 2 +- 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/composer.json b/composer.json index 16f8f000c..8d247e403 100644 --- a/composer.json +++ b/composer.json @@ -25,6 +25,9 @@ "require": { "php": ">=5.4" }, + "conflict": { + "microsoft/windowsazure": "<0.4.3" + }, "require-dev": { "aws/aws-sdk-php": "~2", "amazonwebservices/aws-sdk-for-php": "1.5.*", diff --git a/src/Gaufrette/Adapter/AzureBlobStorage.php b/src/Gaufrette/Adapter/AzureBlobStorage.php index ed3548c80..41d6ff4f6 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage.php @@ -5,11 +5,11 @@ use Gaufrette\Adapter; use Gaufrette\Util; use Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface; -use WindowsAzure\Blob\Models\CreateBlobOptions; -use WindowsAzure\Blob\Models\CreateContainerOptions; -use WindowsAzure\Blob\Models\DeleteContainerOptions; -use WindowsAzure\Blob\Models\ListBlobsOptions; -use WindowsAzure\Common\ServiceException; +use MicrosoftAzure\Storage\Blob\Models\CreateBlobOptions; +use MicrosoftAzure\Storage\Blob\Models\CreateContainerOptions; +use MicrosoftAzure\Storage\Blob\Models\DeleteContainerOptions; +use MicrosoftAzure\Storage\Blob\Models\ListBlobsOptions; +use MicrosoftAzure\Storage\Common\ServiceException; /** * Microsoft Azure Blob Storage adapter. @@ -42,7 +42,7 @@ class AzureBlobStorage implements Adapter, protected $detectContentType; /** - * @var \WindowsAzure\Blob\Internal\IBlob + * @var \MicrosoftAzure\Storage\Blob\Internal\IBlob */ protected $blobProxy; @@ -65,8 +65,8 @@ public function __construct(BlobProxyFactoryInterface $blobProxyFactory, $contai /** * Creates a new container. * - * @param string $containerName - * @param \WindowsAzure\Blob\Models\CreateContainerOptions $options + * @param string $containerName + * @param \MicrosoftAzure\Storage\Blob\Models\CreateContainerOptions $options * * @throws \RuntimeException if cannot create the container */ diff --git a/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php b/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php index 2ab9d8475..0db48794c 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php @@ -2,7 +2,7 @@ namespace Gaufrette\Adapter\AzureBlobStorage; -use WindowsAzure\Common\ServicesBuilder; +use MicrosoftAzure\Storage\Common\ServicesBuilder; /** * Basic implementation for a Blob proxy factory. diff --git a/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactoryInterface.php b/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactoryInterface.php index 379d935d8..852f75ff4 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactoryInterface.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactoryInterface.php @@ -12,7 +12,7 @@ interface BlobProxyFactoryInterface /** * Creates a new instance of the Blob proxy. * - * @return \WindowsAzure\Blob\Internal\IBlob + * @return \MicrosoftAzure\Storage\Blob\Internal\IBlob */ public function create(); } From e23c058328b39bfe5bd9cfe8ce597571bd430828 Mon Sep 17 00:00:00 2001 From: beyerz Date: Sun, 22 May 2016 17:14:48 +0300 Subject: [PATCH 212/426] Get content type by resource awsS3, Inspired by Pull-Request (Handle writing file by ressource as expected by Azure #398) Return content size by resource awsS3, Inspired by Pull-Request (Handle writing file by ressource as expected by Azure #398) Calculate size from resource, Inspired by Pull-Request (Handle writing file by ressource as expected by Azure #398) --- src/Gaufrette/Adapter/AwsS3.php | 20 ++++++++++++++------ src/Gaufrette/Util/Size.php | 14 ++++++++++++++ 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index 27598020e..fe9ccf427 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -4,6 +4,8 @@ use Gaufrette\Adapter; use Aws\S3\S3Client; +use Gaufrette\Util; + /** * Amazon S3 adapter using the AWS SDK for PHP v2.x. @@ -132,16 +134,22 @@ public function write($key, $content) * it to prevent everything being served up as binary/octet-stream. */ if (!isset($options['ContentType']) && $this->detectContentType) { - $finfo = new \finfo(FILEINFO_MIME_TYPE); - $mimeType = $finfo->buffer($content); - - $options['ContentType'] = $mimeType; + $fileInfo = new \finfo(FILEINFO_MIME_TYPE); + if (is_resource($content)) { + $contentType = $fileInfo->file(stream_get_meta_data($content)['uri']); + } else { + $contentType = $fileInfo->buffer($content); + } + $options['ContentType'] = $contentType; } try { $this->service->putObject($options); - - return strlen($content); + if (is_resource($content)) { + return Util\Size::fromResource($content); + } else { + return Util\Size::fromContent($content); + } } catch (\Exception $e) { return false; } diff --git a/src/Gaufrette/Util/Size.php b/src/Gaufrette/Util/Size.php index 97aae1c5b..ba194bfa1 100644 --- a/src/Gaufrette/Util/Size.php +++ b/src/Gaufrette/Util/Size.php @@ -36,4 +36,18 @@ public static function fromFile($filename) { return filesize($filename); } + + /** + * Returns the size in bytes from the given resource. + * + * @param resource $handle + * + * @return string + */ + public static function fromResource($handle) + { + $cStat = fstat($handle); + // if the resource is a remote file, $cStat will be false + return $cStat ? $cStat['size'] : 0; + } } From 5338bbac0f17983cca524cfccd5fb115822d3ac3 Mon Sep 17 00:00:00 2001 From: Albin Kerouaton Date: Wed, 25 May 2016 23:10:22 +0200 Subject: [PATCH 213/426] Remove useless str_replace --- src/Gaufrette/Adapter/AmazonS3.php | 4 ++-- src/Gaufrette/Adapter/Dropbox.php | 4 ++-- src/Gaufrette/Adapter/Ftp.php | 11 ++++++----- src/Gaufrette/Adapter/Sftp.php | 4 ++-- 4 files changed, 12 insertions(+), 11 deletions(-) diff --git a/src/Gaufrette/Adapter/AmazonS3.php b/src/Gaufrette/Adapter/AmazonS3.php index 71b164de2..401ce3732 100644 --- a/src/Gaufrette/Adapter/AmazonS3.php +++ b/src/Gaufrette/Adapter/AmazonS3.php @@ -200,8 +200,8 @@ public function keys() $keys = array(); foreach ($list as $file) { - if ('.' !== \Gaufrette\Util\Path::dirname($file)) { - $keys[] = \Gaufrette\Util\Path::dirname($file); + if ('.' !== $dirname = \Gaufrette\Util\Path::dirname($file)) { + $keys[] = $dirname; } $keys[] = $file; } diff --git a/src/Gaufrette/Adapter/Dropbox.php b/src/Gaufrette/Adapter/Dropbox.php index 24ae5e21f..827f4fa1f 100644 --- a/src/Gaufrette/Adapter/Dropbox.php +++ b/src/Gaufrette/Adapter/Dropbox.php @@ -137,8 +137,8 @@ public function keys() foreach ($metadata['contents'] as $value) { $file = ltrim($value['path'], '/'); $keys[] = $file; - if ('.' !== \Gaufrette\Util\Path::dirname($file)) { - $keys[] = \Gaufrette\Util\Path::dirname($file); + if ('.' !== $dirname = \Gaufrette\Util\Path::dirname($file)) { + $keys[] = $dirname; } } sort($keys); diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index 13423472b..63bf26b41 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -77,7 +77,7 @@ public function write($key, $content) $this->ensureDirectoryExists($this->directory, $this->create); $path = $this->computePath($key); - $directory = str_replace('\\', '/', \Gaufrette\Util\Path::dirname($path)); + $directory = \Gaufrette\Util\Path::dirname($path); $this->ensureDirectoryExists($directory, true); @@ -106,7 +106,7 @@ public function rename($sourceKey, $targetKey) $sourcePath = $this->computePath($sourceKey); $targetPath = $this->computePath($targetKey); - $this->ensureDirectoryExists(str_replace('\\', '/', \Gaufrette\Util\Path::dirname($targetPath)), true); + $this->ensureDirectoryExists(\Gaufrette\Util\Path::dirname($targetPath)); return ftp_rename($this->getConnection(), $sourcePath, $targetPath); } @@ -119,7 +119,7 @@ public function exists($key) $this->ensureDirectoryExists($this->directory, $this->create); $file = $this->computePath($key); - $lines = ftp_rawlist($this->getConnection(), '-al ' . str_replace('\\', '/', \Gaufrette\Util\Path::dirname($file))); + $lines = ftp_rawlist($this->getConnection(), '-al ' . \Gaufrette\Util\Path::dirname($file)); if (false === $lines) { return false; @@ -273,7 +273,8 @@ public function createFile($key, Filesystem $filesystem) $file = new File($key, $filesystem); if (!array_key_exists($key, $this->fileData)) { - $directory = \Gaufrette\Util\Path::dirname($key) == '.' ? '' : str_replace('\\', '/', \Gaufrette\Util\Path::dirname($key)); + $dirname = \Gaufrette\Util\Path::dirname($key); + $directory = $dirname == '.' ? '' : $dirname; $this->listDirectory($directory); } @@ -319,7 +320,7 @@ protected function ensureDirectoryExists($directory, $create = false) protected function createDirectory($directory) { // create parent directory if needed - $parent = str_replace('\\', '/', \Gaufrette\Util\Path::dirname($directory)); + $parent = \Gaufrette\Util\Path::dirname($directory); if (!$this->isDir($parent)) { $this->createDirectory($parent); } diff --git a/src/Gaufrette/Adapter/Sftp.php b/src/Gaufrette/Adapter/Sftp.php index 29f314b05..669b8b4eb 100644 --- a/src/Gaufrette/Adapter/Sftp.php +++ b/src/Gaufrette/Adapter/Sftp.php @@ -101,8 +101,8 @@ public function keys() $dirs = array(); foreach ($files as $file) { - if ('.' !== \Gaufrette\Util\Path::dirname($file)) { - $dirs[] = \Gaufrette\Util\Path::dirname($file); + if ('.' !== $dirname = \Gaufrette\Util\Path::dirname($file)) { + $dirs[] = $dirname; } } From 61ab42933fd6b7c9637947914e64f523a6a089b9 Mon Sep 17 00:00:00 2001 From: Oskar Stark Date: Fri, 9 Sep 2016 14:46:47 +0200 Subject: [PATCH 214/426] typo fix --- src/Gaufrette/File.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gaufrette/File.php b/src/Gaufrette/File.php index 5a704417e..4947c1939 100644 --- a/src/Gaufrette/File.php +++ b/src/Gaufrette/File.php @@ -74,7 +74,7 @@ public function getKey() * * @throws FileNotFound * - * @param array $metadata optional metadata which should be send when read + * @param array $metadata optional metadata which should be set when read * * @return string */ From 9f073a5858ef11fde86bf285736b13847d089be4 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Thu, 2 Mar 2017 12:33:59 +0100 Subject: [PATCH 215/426] Fix Travis builds (#447) * Remove ignore-platform-reqs flag * Add missing extensions * Remove microsoft/windowsazure from require-dev --- .travis.yml | 12 +++++++----- README.md | 4 ++-- composer.json | 1 - 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 8c134100b..fdcf0279d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: php -sudo: false +sudo: true cache: directories: @@ -11,7 +11,7 @@ php: - 5.5 - 5.6 - hhvm - + matrix: allow_failures: - php: hhvm @@ -19,11 +19,13 @@ matrix: before_script: - if [[ ${TRAVIS_PHP_VERSION:0:1} == "5" ]]; then echo "extension = mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini && phpenv config-rm xdebug.ini; fi - # waiting https://github.com/mongodb/mongo-php-library/pull/57 - #- if [[ ${TRAVIS_PHP_VERSION:0:1} == "7" ]]; then echo "extension = mongodb.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; fi - if [[ ${TRAVIS_PHP_VERSION:0:4} == "hhvm" ]]; then composer require mongofill/mongofill --ignore-platform-reqs; fi + - sudo apt-get update + - sudo apt-get install -y libssh2-1-dev + - if [[ ${TRAVIS_PHP_VERSION:0:4} != "hhvm" ]]; then yes | pecl install ssh2 ; fi - cp tests/Gaufrette/Functional/adapters/DoctrineDbal.php.dist tests/Gaufrette/Functional/adapters/DoctrineDbal.php -f - - composer self-update && composer install --prefer-source --no-interaction --ignore-platform-reqs + - composer self-update && composer install --prefer-source --no-interaction + - if [[ ${TRAVIS_PHP_VERSION:0:3} != "5.4" ]]; then composer require microsoft/windowsazure:^0.4.3; fi script: - php bin/phpspec run -fpretty --verbose diff --git a/README.md b/README.md index 720375cff..b409c4928 100644 --- a/README.md +++ b/README.md @@ -37,8 +37,8 @@ Symfony integration is available through [KnpLabs/KnpGaufretteBundle](https://gi As some filesystem adapters use vendor libraries, you should install the vendors: - $ cd gaufrette - $ php composer.phar install + $ composer install + $ composer require microsoft/windowsazure # Needs php >= 5.5 $ sh bin/configure_test_env.sh It will avoid skip a lot of tests. diff --git a/composer.json b/composer.json index 8d247e403..05a2ef142 100644 --- a/composer.json +++ b/composer.json @@ -39,7 +39,6 @@ "dropbox-php/dropbox-php": "*", "herzult/php-ssh": "*", "phpunit/phpunit": "3.7.*", - "microsoft/windowsazure": "dev-master", "mikey179/vfsStream": "~1.2.0", "league/flysystem": "~1.0" }, From 787bbde5d61d34b71dc8dba535558292e54b7984 Mon Sep 17 00:00:00 2001 From: Konstantin Scheumann Date: Tue, 17 Jan 2017 15:39:08 +0100 Subject: [PATCH 216/426] Timeout Option for FTP Configuration --- src/Gaufrette/Adapter/Ftp.php | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index 9b5d41d83..713b4374a 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -25,6 +25,7 @@ class Ftp implements Adapter, protected $create; protected $mode; protected $ssl; + protected $timeout; protected $fileData = array(); protected $utf8; @@ -48,6 +49,7 @@ public function __construct($directory, $host, $options = array()) $this->create = isset($options['create']) ? $options['create'] : false; $this->mode = isset($options['mode']) ? $options['mode'] : FTP_BINARY; $this->ssl = isset($options['ssl']) ? $options['ssl'] : false; + $this->timeout = isset($options['timeout']) ? $options['timeout'] : 90; $this->utf8 = isset($options['utf8']) ? $options['utf8'] : false; } @@ -498,10 +500,10 @@ private function connect() { // open ftp connection if (!$this->ssl) { - $this->connection = ftp_connect($this->host, $this->port); + $this->connection = ftp_connect($this->host, $this->port, $this->timeout); } else { if (function_exists('ftp_ssl_connect')) { - $this->connection = ftp_ssl_connect($this->host, $this->port); + $this->connection = ftp_ssl_connect($this->host, $this->port, $this->timeout); } else { throw new \RuntimeException('This Server Has No SSL-FTP Available.'); } From 13cd318310cbccfcfe1e2e3e6c1f7804aa73d285 Mon Sep 17 00:00:00 2001 From: Carlos Granados Date: Tue, 20 Sep 2016 19:47:24 +0100 Subject: [PATCH 217/426] Modify Google Cloud Storage Adapter to work with version 2.x of the Google APIs Client Library for PHP --- src/Gaufrette/Adapter/GoogleCloudStorage.php | 26 ++++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/Gaufrette/Adapter/GoogleCloudStorage.php b/src/Gaufrette/Adapter/GoogleCloudStorage.php index 6eaf01364..214ab2423 100644 --- a/src/Gaufrette/Adapter/GoogleCloudStorage.php +++ b/src/Gaufrette/Adapter/GoogleCloudStorage.php @@ -3,6 +3,7 @@ namespace Gaufrette\Adapter; use Gaufrette\Adapter; +use GuzzleHttp; /** * Google Cloud Storage adapter using the Google APIs Client Library for PHP. @@ -93,15 +94,24 @@ public function read($key) return false; } - $request = new \Google_Http_Request($object->getMediaLink()); - $this->service->getClient()->getAuth()->sign($request); + if (class_exists('Google_Http_Request')) { + $request = new Google_Http_Request($object->getMediaLink()); + $this->service->getClient()->getAuth()->sign($request); + $response = $this->service->getClient()->getIo()->executeRequest($request); + if ($response[2] == 200) { + $this->setMetadata($key, $object->getMetadata()); - $response = $this->service->getClient()->getIo()->executeRequest($request); - - if ($response[2] == 200) { - $this->setMetadata($key, $object->getMetadata()); - - return $response[0]; + return $response[0]; + } + } else { + $httpClient = new GuzzleHttp\Client(); + $httpClient = $this->service->getClient()->authorize($httpClient); + $response = $httpClient->request('GET', $object->getMediaLink()); + if ($response->getStatusCode() == 200) { + $this->setMetadata($key, $object->getMetadata()); + + return $response->getBody(); + } } return false; From 25bea006c9defaa3185c98367b85a16c7dfce0d1 Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Thu, 3 Mar 2016 17:28:52 +0200 Subject: [PATCH 218/426] [POC] Appveyor CI --- appveyor.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 appveyor.yml diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..da58c696c --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,20 @@ +build: false +platform: 'x86' +clone_folder: C:\projects\gaufrette +branches: + except: + - gh-pages + +install: + - cinst -y php + - cd c:\tools\php + - echo date.timezone="UTC" >> php.ini + - SET PATH=C:\tools\php;%PATH% + - cd C:\projects\gaufrette + - php -r "readfile('http://getcomposer.org/installer');" | php + - php composer.phar install --prefer-source --no-interaction + +test_script: + - cd C:\projects\gaufrette + - php bin\phpspec.bat run -fpretty --verbose + - php bin\phpunit.bat \ No newline at end of file From c909c3366ce1efda5118c5b23144b14b9e0ce149 Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Thu, 3 Mar 2016 17:33:37 +0200 Subject: [PATCH 219/426] INstall openssl on Appveyor --- appveyor.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index da58c696c..1198279ee 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -6,8 +6,10 @@ branches: - gh-pages install: - - cinst -y php + - cinst -y OpenSSL.Light + - cinst -y php -version 5.6.18 - cd c:\tools\php + - echo extension=php_openssl.dll >> php.ini - echo date.timezone="UTC" >> php.ini - SET PATH=C:\tools\php;%PATH% - cd C:\projects\gaufrette From 7aa8c808c86cf1ed7e6022b7ed148c5068d43f6e Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Thu, 3 Mar 2016 17:42:42 +0200 Subject: [PATCH 220/426] Down to 5.6.17 on Appveyor --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 1198279ee..744b166f0 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,7 +7,7 @@ branches: install: - cinst -y OpenSSL.Light - - cinst -y php -version 5.6.18 + - cinst -y php -version 5.6.17 - cd c:\tools\php - echo extension=php_openssl.dll >> php.ini - echo date.timezone="UTC" >> php.ini From 98455f09e6002af94b419d23a9b6a9eeb389a964 Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Thu, 3 Mar 2016 17:57:54 +0200 Subject: [PATCH 221/426] Export openssl path --- appveyor.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/appveyor.yml b/appveyor.yml index 744b166f0..d63cfc6a1 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,6 +7,7 @@ branches: install: - cinst -y OpenSSL.Light + - SET PATH=C:\Program Files\OpenSSL;%PATH% - cinst -y php -version 5.6.17 - cd c:\tools\php - echo extension=php_openssl.dll >> php.ini From b3670196b266fb7acb3fd02da03de9d0c48a2658 Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Fri, 4 Mar 2016 11:29:03 +0200 Subject: [PATCH 222/426] Appveyor tuning --- appveyor.yml | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index d63cfc6a1..a87d0ab29 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,17 +5,26 @@ branches: except: - gh-pages +cache: + - '%LOCALAPPDATA%\Composer\files' + +init: + - SET PATH=C:\Program Files\OpenSSL;c:\tools\php;%PATH% + install: - cinst -y OpenSSL.Light - - SET PATH=C:\Program Files\OpenSSL;%PATH% - cinst -y php -version 5.6.17 - cd c:\tools\php + - copy php.ini-production php.ini /Y + - echo extension_dir=ext >> php.ini + - echo date.timezone="UTC" >> php.ini - echo extension=php_openssl.dll >> php.ini - echo date.timezone="UTC" >> php.ini - - SET PATH=C:\tools\php;%PATH% - cd C:\projects\gaufrette - php -r "readfile('http://getcomposer.org/installer');" | php - - php composer.phar install --prefer-source --no-interaction + - php composer.phar install --prefer-source --no-interaction --ignore-platform-reqs + - php -m + - php -i test_script: - cd C:\projects\gaufrette From ebd1efd8583dcbda66d7470cfbab99bbc202cc03 Mon Sep 17 00:00:00 2001 From: Andrew Kovalyov Date: Fri, 4 Mar 2016 12:51:39 +0200 Subject: [PATCH 223/426] Switch to https version of pear --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 05a2ef142..5a839cdf6 100644 --- a/composer.json +++ b/composer.json @@ -19,7 +19,7 @@ "repositories": [ { "type": "pear", - "url": "http://pear.php.net" + "url": "https://pear.php.net" } ], "require": { From c76d77238da8261a788fcf2f5e459f825795aa58 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Thu, 2 Mar 2017 13:41:16 +0100 Subject: [PATCH 224/426] Disable checksum verification when installing php --- appveyor.yml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index a87d0ab29..959fb9c49 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -13,13 +13,12 @@ init: install: - cinst -y OpenSSL.Light - - cinst -y php -version 5.6.17 + - cinst --allow-empty-checksums -y php -version 5.6.17 - cd c:\tools\php - copy php.ini-production php.ini /Y - echo extension_dir=ext >> php.ini - echo date.timezone="UTC" >> php.ini - echo extension=php_openssl.dll >> php.ini - - echo date.timezone="UTC" >> php.ini - cd C:\projects\gaufrette - php -r "readfile('http://getcomposer.org/installer');" | php - php composer.phar install --prefer-source --no-interaction --ignore-platform-reqs @@ -29,4 +28,4 @@ install: test_script: - cd C:\projects\gaufrette - php bin\phpspec.bat run -fpretty --verbose - - php bin\phpunit.bat \ No newline at end of file + - php bin\phpunit.bat From baa6afecf057bfe029ce9de96147ef91f184c780 Mon Sep 17 00:00:00 2001 From: Will Bradley Date: Fri, 10 Mar 2017 02:32:53 -0800 Subject: [PATCH 225/426] Add ContentType support to AwsS3 Seems like there's a bug where metadata is never actually set. --- src/Gaufrette/Adapter/AwsS3.php | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index fe9ccf427..438bd81c1 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -92,8 +92,15 @@ public function read($key) $this->ensureBucketExists(); $options = $this->getOptions($key); + // Copy ContentType metadata to local metadata + $object = $this->service->getObject($options); + if (!array_key_exists($key, $this->metadata) || !is_array($this->metadata[$key])) { + $this->metadata[$key] = array(); + } + $this->metadata[$key]['ContentType'] = $object->get('ContentType'); + try { - return (string) $this->service->getObject($options)->get('Body'); + return (string) $object->get('Body'); } catch (\Exception $e) { return false; } From d8abade5d7d8f31e74d01e8790be94cfb1ae9782 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aure=CC=81lien=20Chre=CC=81tien?= Date: Fri, 18 Mar 2016 12:28:06 +0100 Subject: [PATCH 226/426] Handle writing file by ressource as expected by Azure --- src/Gaufrette/Adapter/AzureBlobStorage.php | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/AzureBlobStorage.php b/src/Gaufrette/Adapter/AzureBlobStorage.php index 41d6ff4f6..ed38a9d4e 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage.php @@ -148,7 +148,11 @@ public function write($key, $content) if ($this->detectContentType) { $fileInfo = new \finfo(FILEINFO_MIME_TYPE); - $contentType = $fileInfo->buffer($content); + if (is_resource($content)) { + $contentType = $fileInfo->file(stream_get_meta_data($content)['uri']); + } else { + $contentType = $fileInfo->buffer($content); + } $options->setContentType($contentType); } From a6686af3a7658b907faec3dc219bb5dff7eceaaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aure=CC=81lien=20Chre=CC=81tien?= Date: Tue, 22 Mar 2016 18:38:16 +0100 Subject: [PATCH 227/426] Add get size from resources --- src/Gaufrette/Adapter/AzureBlobStorage.php | 6 +++++- src/Gaufrette/Util/Size.php | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/Gaufrette/Adapter/AzureBlobStorage.php b/src/Gaufrette/Adapter/AzureBlobStorage.php index ed38a9d4e..fa4700edc 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage.php @@ -158,7 +158,11 @@ public function write($key, $content) $this->blobProxy->createBlockBlob($this->containerName, $key, $content, $options); - return Util\Size::fromContent($content); + if (is_resource($content)) { + return Util\Size::fromResource($content); + } else { + return Util\Size::fromContent($content); + } } catch (ServiceException $e) { $this->failIfContainerNotFound($e, sprintf('write content for key "%s"', $key)); diff --git a/src/Gaufrette/Util/Size.php b/src/Gaufrette/Util/Size.php index ba194bfa1..7dd53aba3 100644 --- a/src/Gaufrette/Util/Size.php +++ b/src/Gaufrette/Util/Size.php @@ -30,7 +30,7 @@ public static function fromContent($content) * * @param string $filename * - * @return string + * @return int */ public static function fromFile($filename) { From bb86fc23bc7a579e85292c7c2bb05ce195b37eda Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aure=CC=81lien=20Chre=CC=81tien?= Date: Thu, 24 Mar 2016 13:43:09 +0100 Subject: [PATCH 228/426] Fix blob content-type methode --- src/Gaufrette/Adapter/AzureBlobStorage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/AzureBlobStorage.php b/src/Gaufrette/Adapter/AzureBlobStorage.php index fa4700edc..488003d68 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage.php @@ -153,7 +153,7 @@ public function write($key, $content) } else { $contentType = $fileInfo->buffer($content); } - $options->setContentType($contentType); + $options->setBlobContentType($contentType); } $this->blobProxy->createBlockBlob($this->containerName, $key, $content, $options); From 71e7c530d355783ebde17d2577a22f2e35176f22 Mon Sep 17 00:00:00 2001 From: Albin Kerouaton Date: Thu, 26 May 2016 08:58:45 +0200 Subject: [PATCH 229/426] Fix how content types are guessed Based on #412 and #398. --- src/Gaufrette/Adapter/AwsS3.php | 30 +++++++++++++------- src/Gaufrette/Adapter/AzureBlobStorage.php | 28 ++++++++++++------ src/Gaufrette/Adapter/GoogleCloudStorage.php | 19 +++++++++++-- 3 files changed, 57 insertions(+), 20 deletions(-) diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index fe9ccf427..9ad45f2e6 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -6,7 +6,6 @@ use Aws\S3\S3Client; use Gaufrette\Util; - /** * Amazon S3 adapter using the AWS SDK for PHP v2.x. * @@ -134,22 +133,17 @@ public function write($key, $content) * it to prevent everything being served up as binary/octet-stream. */ if (!isset($options['ContentType']) && $this->detectContentType) { - $fileInfo = new \finfo(FILEINFO_MIME_TYPE); - if (is_resource($content)) { - $contentType = $fileInfo->file(stream_get_meta_data($content)['uri']); - } else { - $contentType = $fileInfo->buffer($content); - } - $options['ContentType'] = $contentType; + $options['ContentType'] = $this->guessContentType($content); } try { $this->service->putObject($options); + if (is_resource($content)) { return Util\Size::fromResource($content); - } else { - return Util\Size::fromContent($content); } + + return Util\Size::fromContent($content); } catch (\Exception $e) { return false; } @@ -320,4 +314,20 @@ protected function computeKey($path) { return ltrim(substr($path, strlen($this->options['directory'])), '/'); } + + /** + * @param string $content + * + * @return string + */ + private function guessContentType($content) + { + $fileInfo = new \finfo(FILEINFO_MIME_TYPE); + + if (is_resource($content)) { + return $fileInfo->file(stream_get_meta_data($content)['uri']); + } + + return $fileInfo->buffer($content); + } } diff --git a/src/Gaufrette/Adapter/AzureBlobStorage.php b/src/Gaufrette/Adapter/AzureBlobStorage.php index 488003d68..4a7955371 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage.php @@ -147,12 +147,8 @@ public function write($key, $content) $options = new CreateBlobOptions(); if ($this->detectContentType) { - $fileInfo = new \finfo(FILEINFO_MIME_TYPE); - if (is_resource($content)) { - $contentType = $fileInfo->file(stream_get_meta_data($content)['uri']); - } else { - $contentType = $fileInfo->buffer($content); - } + $contentType = $this->guessContentType($content); + $options->setBlobContentType($contentType); } @@ -160,9 +156,9 @@ public function write($key, $content) if (is_resource($content)) { return Util\Size::fromResource($content); - } else { - return Util\Size::fromContent($content); } + + return Util\Size::fromContent($content); } catch (ServiceException $e) { $this->failIfContainerNotFound($e, sprintf('write content for key "%s"', $key)); @@ -391,4 +387,20 @@ protected function getErrorCodeFromServiceException(ServiceException $exception) return $exception->getErrorReason(); } + + /** + * @param string $content + * + * @return string + */ + private function guessContentType($content) + { + $fileInfo = new \finfo(FILEINFO_MIME_TYPE); + + if (is_resource($content)) { + return $fileInfo->file(stream_get_meta_data($content)['uri']); + } + + return $fileInfo->buffer($content); + } } diff --git a/src/Gaufrette/Adapter/GoogleCloudStorage.php b/src/Gaufrette/Adapter/GoogleCloudStorage.php index 214ab2423..946e23200 100644 --- a/src/Gaufrette/Adapter/GoogleCloudStorage.php +++ b/src/Gaufrette/Adapter/GoogleCloudStorage.php @@ -136,8 +136,7 @@ public function write($key, $content) * it to prevent everything being served up as application/octet-stream. */ if (!isset($metadata['ContentType']) && $this->detectContentType) { - $finfo = new \finfo(FILEINFO_MIME_TYPE); - $options['mimeType'] = $finfo->buffer($content); + $options['mimeType'] = $this->guessContentType($content); unset($metadata['ContentType']); } elseif (isset($metadata['ContentType'])) { $options['mimeType'] = $metadata['ContentType']; @@ -381,4 +380,20 @@ private function getObjectData($path, $options = array()) return false; } } + + /** + * @param string $content + * + * @return string + */ + private function guessContentType($content) + { + $fileInfo = new \finfo(FILEINFO_MIME_TYPE); + + if (is_resource($content)) { + return $fileInfo->file(stream_get_meta_data($content)['uri']); + } + + return $fileInfo->buffer($content); + } } From 9ad673ba4ebc91fe8fe19b48e18f4702edb41775 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Thu, 2 Mar 2017 01:22:47 +0100 Subject: [PATCH 230/426] Add container for every supported php version --- .dockerignore | 14 ++++++++++++++ .editorconfig | 3 +++ .gitignore | 11 ++++++----- .travis.yml | 4 ++-- README.md | 22 +++++++--------------- appveyor.yml | 4 ++-- bin/tests | 14 ++++++++++++++ bin/tests-all | 10 ++++++++++ composer.json | 3 --- docker-compose.yml | 32 ++++++++++++++++++++++++++++++++ docker/Dockerfile-hhvm | 16 ++++++++++++++++ docker/Dockerfile-php54 | 22 ++++++++++++++++++++++ docker/Dockerfile-php55 | 22 ++++++++++++++++++++++ docker/Dockerfile-php56 | 23 +++++++++++++++++++++++ docker/Dockerfile-php70 | 28 ++++++++++++++++++++++++++++ docker/Dockerfile-php71 | 29 +++++++++++++++++++++++++++++ 16 files changed, 230 insertions(+), 27 deletions(-) create mode 100644 .dockerignore create mode 100755 bin/tests create mode 100755 bin/tests-all create mode 100644 docker-compose.yml create mode 100644 docker/Dockerfile-hhvm create mode 100644 docker/Dockerfile-php54 create mode 100644 docker/Dockerfile-php55 create mode 100644 docker/Dockerfile-php56 create mode 100644 docker/Dockerfile-php70 create mode 100644 docker/Dockerfile-php71 diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..b9f6106f2 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +.idea/ +bin/tests-all +doc/ +docker/ +vendor/ +.dockerignore +.editorconfig +.gitignore +.travis.yml +appveyor.yml +composer.lock +docker-compose.yml +LICENSE +README.md diff --git a/.editorconfig b/.editorconfig index 779f99a12..fed584a77 100644 --- a/.editorconfig +++ b/.editorconfig @@ -10,3 +10,6 @@ insert_final_newline = true [*.md] trim_trailing_whitespace = false + +[*.yml] +indent_size = 2 diff --git a/.gitignore b/.gitignore index 81c6ed309..3648dbec2 100644 --- a/.gitignore +++ b/.gitignore @@ -1,8 +1,9 @@ +bin/* +!bin/tests +!bin/tests-all +doc/.couscous +tests/adapters/* +!tests/adapters/*.dist vendor/ composer.lock phpunit.xml -tests/adapters/* -!tests/adapters/*.dist -bin/* -!bin/configure_test_env.sh -doc/.couscous diff --git a/.travis.yml b/.travis.yml index fdcf0279d..acff5521d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -28,5 +28,5 @@ before_script: - if [[ ${TRAVIS_PHP_VERSION:0:3} != "5.4" ]]; then composer require microsoft/windowsazure:^0.4.3; fi script: - - php bin/phpspec run -fpretty --verbose - - php bin/phpunit + - php vendor/bin/phpspec run -fpretty --verbose + - php vendor/bin/phpunit diff --git a/README.md b/README.md index b409c4928..5798da93d 100644 --- a/README.md +++ b/README.md @@ -33,27 +33,19 @@ Read the official [Gaufrette documentation](http://knplabs.github.io/Gaufrette/) Symfony integration is available through [KnpLabs/KnpGaufretteBundle](https://github.com/KnpLabs/KnpGaufretteBundle). -### Setup the vendor libraries - -As some filesystem adapters use vendor libraries, you should install the vendors: - - $ composer install - $ composer require microsoft/windowsazure # Needs php >= 5.5 - $ sh bin/configure_test_env.sh - -It will avoid skip a lot of tests. - ### Launch the Test Suite -In the Gaufrette root directory: +Requires: + * docker + * docker-compose -To check if classes specification pass: +Build images: - $ php bin/phpspec run + $ docker-compose build -To check basic functionality of the adapters (adapters should be configured you will see many skipped tests): +Launch the tests: - $ bin/phpunit + $ bin/tests-all Is it green? diff --git a/appveyor.yml b/appveyor.yml index 959fb9c49..60b74d682 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -27,5 +27,5 @@ install: test_script: - cd C:\projects\gaufrette - - php bin\phpspec.bat run -fpretty --verbose - - php bin\phpunit.bat + - php vendor\bin\phpspec.bat run -fpretty --verbose + - php vendor\bin\phpunit.bat diff --git a/bin/tests b/bin/tests new file mode 100755 index 000000000..4180c0cb4 --- /dev/null +++ b/bin/tests @@ -0,0 +1,14 @@ +#!/bin/bash + +set -o nounset +set -o errexit +set -o xtrace + +DIR=$(dirname ${BASH_SOURCE[0]})/.. + +cp \ + ${DIR}/tests/Gaufrette/Functional/adapters/DoctrineDbal.php.dist \ + ${DIR}/tests/Gaufrette/Functional/adapters/DoctrineDbal.php + +${DIR}/vendor/bin/phpspec run --format=pretty +${DIR}/vendor/bin/phpunit diff --git a/bin/tests-all b/bin/tests-all new file mode 100755 index 000000000..565f84ffe --- /dev/null +++ b/bin/tests-all @@ -0,0 +1,10 @@ +#!/bin/bash + +set -o xtrace + +docker-compose run --rm php54 +docker-compose run --rm php55 +docker-compose run --rm php56 +docker-compose run --rm php70 +docker-compose run --rm php71 +docker-compose run --rm hhvm diff --git a/composer.json b/composer.json index 5a839cdf6..84bb1557c 100644 --- a/composer.json +++ b/composer.json @@ -68,8 +68,5 @@ "branch-alias": { "dev-master": "0.4.x-dev" } - }, - "config": { - "bin-dir": "bin" } } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..6a931f114 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,32 @@ +version: '2' + +services: + php54: + build: + context: . + dockerfile: docker/Dockerfile-php54 + + php55: + build: + context: . + dockerfile: docker/Dockerfile-php55 + + php56: + build: + context: . + dockerfile: docker/Dockerfile-php56 + + php70: + build: + context: . + dockerfile: docker/Dockerfile-php70 + + php71: + build: + context: . + dockerfile: docker/Dockerfile-php71 + + hhvm: + build: + context: . + dockerfile: docker/Dockerfile-hhvm diff --git a/docker/Dockerfile-hhvm b/docker/Dockerfile-hhvm new file mode 100644 index 000000000..6c9c08a89 --- /dev/null +++ b/docker/Dockerfile-hhvm @@ -0,0 +1,16 @@ +FROM webdevops/hhvm:ubuntu-16.04 + +ENV SRC_DIR /usr/src/gaufrette +WORKDIR ${SRC_DIR} + +COPY composer.json ${SRC_DIR}/ +RUN composer global require hirak/prestissimo && \ + composer remove --dev --no-update herzult/php-ssh && \ + composer update --prefer-lowest --prefer-stable && \ + composer require microsoft/windowsazure:~0.5 && \ + rm -rf ~/.composer/cache/* + +VOLUME ${SRC_DIR}/vendor + +COPY . ${SRC_DIR}/ +CMD "${SRC_DIR}/bin/tests" diff --git a/docker/Dockerfile-php54 b/docker/Dockerfile-php54 new file mode 100644 index 000000000..82af1f820 --- /dev/null +++ b/docker/Dockerfile-php54 @@ -0,0 +1,22 @@ +FROM jolicode/php54:latest + +ENV SRC_DIR /usr/src/gaufrette +WORKDIR ${SRC_DIR}/ + +RUN sudo apt-get update && \ + sudo apt-get install -y libssh2-1-dev && \ + sudo rm -rf /var/lib/apt/lists/* && \ + pecl install mongo ssh2 + +COPY composer.json ${SRC_DIR}/ +RUN sudo chown -R travis ${SRC_DIR} && \ + composer global require hirak/prestissimo && \ + composer install --prefer-dist && \ + rm -rf ~/.composer/cache/* + +VOLUME ${SRC_DIR}/vendor + +COPY . ${SRC_DIR}/ +RUN sudo chown -R travis ${SRC_DIR} + +CMD "${SRC_DIR}/bin/tests" diff --git a/docker/Dockerfile-php55 b/docker/Dockerfile-php55 new file mode 100644 index 000000000..3e9e3f27b --- /dev/null +++ b/docker/Dockerfile-php55 @@ -0,0 +1,22 @@ +FROM jolicode/php55:latest + +ENV SRC_DIR /usr/src/gaufrette +WORKDIR ${SRC_DIR}/ + +RUN sudo apt-get update && \ + sudo apt-get install -y libssh2-1-dev && \ + sudo rm -rf /var/lib/apt/lists/* && \ + pecl install mongo ssh2 + +COPY composer.json ${SRC_DIR}/ +RUN sudo chown -R travis ${SRC_DIR} && \ + composer global require hirak/prestissimo && \ + composer install --prefer-dist && \ + rm -rf ~/.composer/cache/* + +VOLUME ${SRC_DIR}/vendor + +COPY . ${SRC_DIR}/ +RUN sudo chown -R travis ${SRC_DIR} + +CMD "${SRC_DIR}/bin/tests" diff --git a/docker/Dockerfile-php56 b/docker/Dockerfile-php56 new file mode 100644 index 000000000..3ccd45fb0 --- /dev/null +++ b/docker/Dockerfile-php56 @@ -0,0 +1,23 @@ +FROM jolicode/php56:latest + +ENV SRC_DIR /usr/src/gaufrette +WORKDIR ${SRC_DIR}/ + +RUN sudo apt-get update && \ + sudo apt-get install -y libssh2-1-dev && \ + sudo rm -rf /var/lib/apt/lists/* && \ + pecl install mongo ssh2 + +COPY composer.json ${SRC_DIR}/ +RUN sudo chown -R travis ${SRC_DIR} && \ + composer global require hirak/prestissimo && \ + composer update --prefer-lowest --prefer-stable && \ + composer require microsoft/windowsazure:~0.4.3 && \ + rm -rf ~/.composer/cache/* + +VOLUME ${SRC_DIR}/vendor + +COPY . ${SRC_DIR}/ +RUN sudo chown -R travis ${SRC_DIR} + +CMD "${SRC_DIR}/bin/tests" diff --git a/docker/Dockerfile-php70 b/docker/Dockerfile-php70 new file mode 100644 index 000000000..a66fe404b --- /dev/null +++ b/docker/Dockerfile-php70 @@ -0,0 +1,28 @@ +FROM php:7.0-cli + +ENV SRC_DIR /usr/src/gaufrette +WORKDIR ${SRC_DIR} + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + git \ + libssh2-1-dev \ + libssl-dev \ + unzip \ + zlib1g-dev \ + && rm -rf /var/lib/apt/lists/* && \ + pecl install mongodb ssh2-alpha zip && \ + echo 'extension=/usr/local/lib/php/extensions/no-debug-non-zts-20151012/mongodb.so' >> /usr/local/etc/php/conf.d/mongodb.ini && \ + echo 'extension=/usr/local/lib/php/extensions/no-debug-non-zts-20151012/ssh2.so' >> /usr/local/etc/php/conf.d/ssh2.ini && \ + echo 'extension=/usr/local/lib/php/extensions/no-debug-non-zts-20151012/zip.so' >> /usr/local/etc/php/conf.d/zip.ini + +COPY composer.json ${SRC_DIR}/ +RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer && \ + composer install && \ + composer require microsoft/windowsazure:dev-master && \ + rm -rf ~/.composer/cache/* + +VOLUME ${SRC_DIR}/vendor + +COPY . ${SRC_DIR}/ +CMD "${SRC_DIR}/bin/tests" diff --git a/docker/Dockerfile-php71 b/docker/Dockerfile-php71 new file mode 100644 index 000000000..5a809201b --- /dev/null +++ b/docker/Dockerfile-php71 @@ -0,0 +1,29 @@ +FROM php:7.1-cli + +ENV SRC_DIR /usr/src/gaufrette +WORKDIR ${SRC_DIR} + +RUN apt-get update && \ + apt-get install -y --no-install-recommends \ + git \ + libssh2-1-dev \ + libssl-dev \ + unzip \ + zlib1g-dev \ + && rm -rf /var/lib/apt/lists/* && \ + pecl install mongodb ssh2-alpha zip && \ + echo 'extension=/usr/local/lib/php/extensions/no-debug-non-zts-20160303/mongodb.so' >> /usr/local/etc/php/conf.d/mongodb.ini && \ + echo 'extension=/usr/local/lib/php/extensions/no-debug-non-zts-20160303/ssh2.so' >> /usr/local/etc/php/conf.d/ssh2.ini && \ + echo 'extension=/usr/local/lib/php/extensions/no-debug-non-zts-20160303/zip.so' >> /usr/local/etc/php/conf.d/zip.ini + +COPY composer.json ${SRC_DIR}/ +RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer && \ + composer global require hirak/prestissimo && \ + composer update --prefer-lowest --prefer-stable && \ + composer require microsoft/windowsazure:~0.5 && \ + rm -rf ~/.composer/cache/* + +VOLUME ${SRC_DIR}/vendor + +COPY . ${SRC_DIR}/ +CMD "${SRC_DIR}/bin/tests" From f3d80db155adff7330512f59a1664d6ed3d8a966 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Thu, 2 Mar 2017 03:04:36 +0100 Subject: [PATCH 231/426] Fix build issues on php56 > Class 'OpenCloud\ObjectStore\Exception\ObjectNotFoundException' not found in /usr/src/gaufrette/spec/Gaufrette/Adapter/LazyOpenCloudSpec.php on line 38 > PHP Fatal error: Call to a member function getMethod() on null in /usr/src/gaufrette/vendor/aws/aws-sdk-php/src/Aws/S3/Exception/Parser/S3ExceptionParser.php on line 55 --- composer.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/composer.json b/composer.json index 84bb1557c..aee87d127 100644 --- a/composer.json +++ b/composer.json @@ -29,9 +29,9 @@ "microsoft/windowsazure": "<0.4.3" }, "require-dev": { - "aws/aws-sdk-php": "~2", + "aws/aws-sdk-php": "^2.4.12", "amazonwebservices/aws-sdk-for-php": "1.5.*", - "rackspace/php-opencloud" : "1.9.*", + "rackspace/php-opencloud" : "^1.9.2", "google/apiclient": "~1.1.3", "phpspec/phpspec": "~2.4", "phpseclib/phpseclib": "^2.0", From 8d277a1bd3e5ee2bbdff73948db4e23d309b34a3 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Sun, 12 Mar 2017 18:02:11 +0100 Subject: [PATCH 232/426] Check if keys are valid before using them See #432. --- src/Gaufrette/Filesystem.php | 70 ++++++++++++++++++++++++++++++++---- 1 file changed, 63 insertions(+), 7 deletions(-) diff --git a/src/Gaufrette/Filesystem.php b/src/Gaufrette/Filesystem.php index 8d23932ee..3cbfb980d 100644 --- a/src/Gaufrette/Filesystem.php +++ b/src/Gaufrette/Filesystem.php @@ -45,9 +45,13 @@ public function getAdapter() * @param string $key * * @return bool TRUE if the file exists, FALSE otherwise + * + * @throws \InvalidArgumentException If $key is invalid */ public function has($key) { + self::assertValidKey($key); + return $this->adapter->exists($key); } @@ -59,12 +63,16 @@ public function has($key) * * @return bool TRUE if the rename was successful * - * @throws Exception\FileNotFound when sourceKey does not exist - * @throws Exception\UnexpectedFile when targetKey exists - * @throws \RuntimeException when cannot rename + * @throws Exception\FileNotFound when sourceKey does not exist + * @throws Exception\UnexpectedFile when targetKey exists + * @throws \RuntimeException when cannot rename + * @throws \InvalidArgumentException If $sourceKey or $targetKey are invalid */ public function rename($sourceKey, $targetKey) { + self::assertValidKey($sourceKey); + self::assertValidKey($targetKey); + $this->assertHasFile($sourceKey); if ($this->has($targetKey)) { @@ -90,11 +98,14 @@ public function rename($sourceKey, $targetKey) * @param bool $create Whether to create the file if it does not exist * * @throws Exception\FileNotFound + * @throws \InvalidArgumentException If $key is invalid * * @return File */ public function get($key, $create = false) { + self::assertValidKey($key); + if (!$create) { $this->assertHasFile($key); } @@ -111,11 +122,14 @@ public function get($key, $create = false) * * @throws Exception\FileAlreadyExists When file already exists and overwrite is false * @throws \RuntimeException When for any reason content could not be written + * @throws \InvalidArgumentException If $key is invalid * * @return int The number of bytes that were written into the file */ public function write($key, $content, $overwrite = false) { + self::assertValidKey($key); + if (!$overwrite && $this->has($key)) { throw new Exception\FileAlreadyExists($key); } @@ -134,13 +148,16 @@ public function write($key, $content, $overwrite = false) * * @param string $key Key of the file * - * @throws Exception\FileNotFound when file does not exist - * @throws \RuntimeException when cannot read file + * @throws Exception\FileNotFound when file does not exist + * @throws \RuntimeException when cannot read file + * @throws \InvalidArgumentException If $key is invalid * * @return string */ public function read($key) { + self::assertValidKey($key); + $this->assertHasFile($key); $content = $this->adapter->read($key); @@ -157,12 +174,15 @@ public function read($key) * * @param string $key * - * @throws \RuntimeException when cannot read file + * @throws \RuntimeException when cannot read file + * @throws \InvalidArgumentException If $key is invalid * * @return bool */ public function delete($key) { + self::assertValidKey($key); + $this->assertHasFile($key); if ($this->adapter->delete($key)) { @@ -226,9 +246,13 @@ public function listKeys($prefix = '') * @param string $key * * @return int An UNIX like timestamp + * + * @throws \InvalidArgumentException If $key is invalid */ public function mtime($key) { + self::assertValidKey($key); + $this->assertHasFile($key); return $this->adapter->mtime($key); @@ -240,9 +264,13 @@ public function mtime($key) * @param string $key * * @return string A MD5 hash + * + * @throws \InvalidArgumentException If $key is invalid */ public function checksum($key) { + self::assertValidKey($key); + $this->assertHasFile($key); if ($this->adapter instanceof Adapter\ChecksumCalculator) { @@ -258,9 +286,13 @@ public function checksum($key) * @param string $key * * @return int File size in Bytes + * + * @throws \InvalidArgumentException If $key is invalid */ public function size($key) { + self::assertValidKey($key); + $this->assertHasFile($key); if ($this->adapter instanceof Adapter\SizeCalculator) { @@ -276,9 +308,13 @@ public function size($key) * @param $key * * @return Stream|Stream\InMemoryBuffer + * + * @throws \InvalidArgumentException If $key is invalid */ public function createStream($key) { + self::assertValidKey($key); + if ($this->adapter instanceof Adapter\StreamFactory) { return $this->adapter->createStream($key); } @@ -292,9 +328,13 @@ public function createStream($key) * @param $key * * @return File + * + * @throws \InvalidArgumentException If $key is invalid */ public function createFile($key) { + self::assertValidKey($key); + if (false === $this->isFileInRegister($key)) { if ($this->adapter instanceof Adapter\FileFactory) { $this->fileRegister[$key] = $this->adapter->createFile($key, $this); @@ -312,9 +352,13 @@ public function createFile($key) * @param string $key * * @return string + * + * @throws \InvalidArgumentException If $key is invalid */ public function mimeType($key) { + self::assertValidKey($key); + $this->assertHasFile($key); if ($this->adapter instanceof Adapter\MimeTypeProvider) { @@ -339,7 +383,7 @@ public function mimeType($key) */ private function assertHasFile($key) { - if (!empty($key) && !$this->has($key)) { + if (!$this->has($key)) { throw new Exception\FileNotFound($key); } } @@ -385,4 +429,16 @@ public function isDirectory($key) { return $this->adapter->isDirectory($key); } + + /** + * @param string $key + * + * @throws \InvalidArgumentException Given $key should not be empty + */ + private static function assertValidKey($key) + { + if (empty($key)) { + throw new \InvalidArgumentException('Object path is empty.'); + } + } } From 4cf76bb944df3f22960be72ca8fddae3a88af83b Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Tue, 14 Mar 2017 01:11:06 +0100 Subject: [PATCH 233/426] Implements SizeCalculator interface in FTP adapter See #417 --- spec/Gaufrette/Adapter/FtpSpec.php | 12 ++++++++++++ spec/Gaufrette/Adapter/functions.php | 9 +++++++++ src/Gaufrette/Adapter/Ftp.php | 21 ++++++++++++++++++++- 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/spec/Gaufrette/Adapter/FtpSpec.php b/spec/Gaufrette/Adapter/FtpSpec.php index 1d02e8af8..4148cf0ef 100644 --- a/spec/Gaufrette/Adapter/FtpSpec.php +++ b/spec/Gaufrette/Adapter/FtpSpec.php @@ -213,4 +213,16 @@ function it_fetches_keys_for_windows() $this->keys()->shouldReturn(array('archive', 'file1.zip', 'file2.zip')); } + + function it_supports_sizecalculator() + { + $this->shouldImplement('Gaufrette\Adapter\SizeCalculator'); + + $this->size('/path')->shouldReturn(5000); + } + + function it_throws_an_exception_when_size_cant_be_fetched() + { + $this->shouldThrow('RuntimeException')->during('size', ['/erroneous']); + } } diff --git a/spec/Gaufrette/Adapter/functions.php b/spec/Gaufrette/Adapter/functions.php index 2aee4371e..4e3a464ec 100644 --- a/spec/Gaufrette/Adapter/functions.php +++ b/spec/Gaufrette/Adapter/functions.php @@ -284,3 +284,12 @@ function apc_exists($path) return true; } + +function ftp_size($connection, $path) +{ + if ($path === '/erroneous') { + return -1; + } + + return 5000; +} diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index 713b4374a..e3f3b6544 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -13,7 +13,8 @@ */ class Ftp implements Adapter, FileFactory, - ListKeysAware + ListKeysAware, + SizeCalculator { protected $connection = null; protected $directory; @@ -292,6 +293,24 @@ public function createFile($key, Filesystem $filesystem) return $file; } + /** + * @param string $key + * + * @return int + * + * @throws \RuntimeException + */ + public function size($key) + { + $this->ensureDirectoryExists($this->directory, $this->create); + + if (-1 === $size = ftp_size($this->connection, $key)) { + throw new \RuntimeException(sprintf('Unable to fetch the size of "%s".', $key)); + } + + return $size; + } + /** * Ensures the specified directory exists. If it does not, and the create * parameter is set to TRUE, it tries to create it. From b136828b22058c2e63c11938dfaba9c3180603a2 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Tue, 14 Mar 2017 00:20:34 +0100 Subject: [PATCH 234/426] Don't use the address returned by FTP servers when switching to passive mode See #434 --- src/Gaufrette/Adapter/Ftp.php | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index e3f3b6544..6c7ef124c 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -517,16 +517,21 @@ private function getConnection() */ private function connect() { + if ($this->ssl && !function_exists('ftp_ssl_connect')) { + throw new \RuntimeException('This Server Has No SSL-FTP Available.'); + } + // open ftp connection if (!$this->ssl) { $this->connection = ftp_connect($this->host, $this->port, $this->timeout); } else { - if (function_exists('ftp_ssl_connect')) { - $this->connection = ftp_ssl_connect($this->host, $this->port, $this->timeout); - } else { - throw new \RuntimeException('This Server Has No SSL-FTP Available.'); - } + $this->connection = ftp_ssl_connect($this->host, $this->port, $this->timeout); } + + if (PHP_VERSION_ID >= 50600) { + ftp_set_option($this->connection, FTP_USEPASVADDRESS, false); + } + if (!$this->connection) { throw new \RuntimeException(sprintf('Could not connect to \'%s\' (port: %s).', $this->host, $this->port)); } From 771ad16f4b2e7f9d35f44b201956e83c6fbf5dde Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Sun, 19 Mar 2017 18:27:22 +0100 Subject: [PATCH 235/426] Bind-mount spec/src/tests directories into containers --- docker-compose.yml | 44 ++++++++++++++++++++++++++++++++++---------- 1 file changed, 34 insertions(+), 10 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 6a931f114..bc5bcc763 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,30 +3,54 @@ version: '2' services: php54: build: - context: . - dockerfile: docker/Dockerfile-php54 + context: . + dockerfile: docker/Dockerfile-php54 + volumes: + - './spec/:/usr/src/gaufrette/spec/' + - './src/:/usr/src/gaufrette/src/' + - './tests/:/usr/src/gaufrette/tests/' php55: build: - context: . - dockerfile: docker/Dockerfile-php55 + context: . + dockerfile: docker/Dockerfile-php55 + volumes: + - './spec/:/usr/src/gaufrette/spec/' + - './src/:/usr/src/gaufrette/src/' + - './tests/:/usr/src/gaufrette/tests/' php56: build: - context: . - dockerfile: docker/Dockerfile-php56 + context: . + dockerfile: docker/Dockerfile-php56 + volumes: + - './spec/:/usr/src/gaufrette/spec/' + - './src/:/usr/src/gaufrette/src/' + - './tests/:/usr/src/gaufrette/tests/' php70: build: - context: . - dockerfile: docker/Dockerfile-php70 + context: . + dockerfile: docker/Dockerfile-php70 + volumes: + - './spec/:/usr/src/gaufrette/spec/' + - './src/:/usr/src/gaufrette/src/' + - './tests/:/usr/src/gaufrette/tests/' php71: build: - context: . - dockerfile: docker/Dockerfile-php71 + context: . + dockerfile: docker/Dockerfile-php71 + volumes: + - './spec/:/usr/src/gaufrette/spec/' + - './src/:/usr/src/gaufrette/src/' + - './tests/:/usr/src/gaufrette/tests/' hhvm: build: context: . dockerfile: docker/Dockerfile-hhvm + volumes: + - './spec/:/usr/src/gaufrette/spec/' + - './src/:/usr/src/gaufrette/src/' + - './tests/:/usr/src/gaufrette/tests/' From 972bf68b6093dcb5d445fb86946ab09496af7f06 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Mon, 20 Mar 2017 01:49:52 +0100 Subject: [PATCH 236/426] Replace unmaintained mongo ext with newer mongodb extension --- CHANGELOG.md | 4 + composer.json | 8 +- doc/adapters/grid-fs.md | 21 +- docker/Dockerfile-php56 | 2 +- spec/Gaufrette/Adapter/GridFSSpec.php | 284 +++++++------------------- src/Gaufrette/Adapter/GridFS.php | 115 ++++++----- 6 files changed, 159 insertions(+), 275 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..719deac03 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,4 @@ +v0.4 +==== + +* Unmaintained `mongo` extension replaced with newer `mongodb` extension diff --git a/composer.json b/composer.json index aee87d127..2006362e8 100644 --- a/composer.json +++ b/composer.json @@ -40,7 +40,8 @@ "herzult/php-ssh": "*", "phpunit/phpunit": "3.7.*", "mikey179/vfsStream": "~1.2.0", - "league/flysystem": "~1.0" + "league/flysystem": "~1.0", + "mongodb/mongodb": "~1.1.4" }, "suggest": { "knplabs/knp-gaufrette-bundle": "to use with Symfony2", @@ -58,8 +59,9 @@ "ext-apc": "to use the APC adapter", "ext-curl": "*", "ext-mbstring": "*", - "ext-mongo": "*", - "ext-fileinfo": "This extension is used to automatically detect the content-type of a file in the AwsS3, OpenCloud, AzureBlogStorage and GoogleCloudStorage adapters" + "ext-mongodb": "*", + "ext-fileinfo": "This extension is used to automatically detect the content-type of a file in the AwsS3, OpenCloud, AzureBlogStorage and GoogleCloudStorage adapters", + "mongodb/mongodb": "*" }, "autoload": { "psr-0": { "Gaufrette": "src/" } diff --git a/doc/adapters/grid-fs.md b/doc/adapters/grid-fs.md index a24615406..1ba765d1b 100644 --- a/doc/adapters/grid-fs.md +++ b/doc/adapters/grid-fs.md @@ -6,22 +6,27 @@ currentMenu: grid-fs ## Prerequisites -In order to use GridFS adapter, you should have accesible MongoDB instance and [Mongo PHP driver](http://docs.php.net/manual/en/book.mongo.php) installed. +In order to use GridFS adapter, you should have accesible MongoDB instance, [MongoDB PHP driver](http://docs.php.net/manual/en/book.mongodb.php) and +the [`mongodb/mongodb`](https://docs.mongodb.com/php-library/master/) library installed. -You can install the Mongo extension with +You can install the MongoDB extension with ```bash -pecl install mongo +pecl install mongodb ``` -*N.B.* `mongo` php extension is deprecated in favor of [MongoDB PHP driver](http://mongodb.github.io/mongo-php-driver/#installation). We will switch to it right after [new API supports GridFS](http://mongodb.github.io/mongo-php-library/#mongodb-php-library]). +And the PHP library with + +```bash +composer require mongodb/mongodb +``` ## Usage ```php -$client = new \MongoClient('mongodb://localhost:27007'); -$db = $client->selectDB('dbname'); -$gridFs = new \MongoGridFs($db); +$client = new \MongoDB\Client('mongodb://localhost:27017'); +$db = $client->selectDatabase('dbname'); -$adapter = new \Gaufrette\Adapter\GridFS($gridFs); +$adapter = new \Gaufrette\Adapter\GridFS($db->selectGridFSBucket()); +``` diff --git a/docker/Dockerfile-php56 b/docker/Dockerfile-php56 index 3ccd45fb0..7c6273c06 100644 --- a/docker/Dockerfile-php56 +++ b/docker/Dockerfile-php56 @@ -6,7 +6,7 @@ WORKDIR ${SRC_DIR}/ RUN sudo apt-get update && \ sudo apt-get install -y libssh2-1-dev && \ sudo rm -rf /var/lib/apt/lists/* && \ - pecl install mongo ssh2 + pecl install mongodb ssh2 COPY composer.json ${SRC_DIR}/ RUN sudo chown -R travis ${SRC_DIR} && \ diff --git a/spec/Gaufrette/Adapter/GridFSSpec.php b/spec/Gaufrette/Adapter/GridFSSpec.php index 9c2c20893..1d5bb064f 100644 --- a/spec/Gaufrette/Adapter/GridFSSpec.php +++ b/spec/Gaufrette/Adapter/GridFSSpec.php @@ -2,17 +2,27 @@ namespace spec\Gaufrette\Adapter; +use MongoDB\BSON\UTCDateTime; +use MongoDB\GridFS\Bucket; +use MongoDB\GridFS\Exception\FileNotFoundException; +use MongoDB\Model\BSONDocument; use PhpSpec\ObjectBehavior; use Prophecy\Argument; class GridFSSpec extends ObjectBehavior { - /** - * @param \MongoGridFS $gridFs - */ - function let($gridFs) + private $resources = []; + + function let(Bucket $bucket) + { + $this->beConstructedWith($bucket); + } + + function letGo() { - $this->beConstructedWith($gridFs); + array_map(function ($res) { + @fclose($res); + }, $this->resources); } function it_is_adapter() @@ -35,51 +45,36 @@ function it_supports_native_list_keys() $this->shouldHaveType('Gaufrette\Adapter\ListKeysAware'); } - /** - * @param \MongoGridFS $gridFs - * @param \MongoGridFSFile $file - */ - function it_reads_file($gridFs, $file) + function it_reads_file($bucket) { - $file - ->getBytes() - ->willReturn('some content') - ; - $gridFs - ->findOne('filename', array()) + $this->resources[] = $readable = fopen('php://memory', 'rw'); + fwrite($readable, 'some content'); + fseek($readable, 0); + + $bucket + ->openDownloadStreamByName('filename') ->shouldBeCalled() - ->willReturn($file) + ->willReturn($readable) ; $this->read('filename')->shouldReturn('some content'); } - /** - * @param \MongoGridFS $gridFs - */ - function it_does_not_fail_when_cannot_read($gridFs) + function it_does_not_fail_when_cannot_read($bucket) { - $gridFs - ->findOne('filename', array()) - ->shouldBeCalled() - ->willReturn(null) - ; + $bucket->openDownloadStreamByName('filename')->willThrow(FileNotFoundException::class); $this->read('filename')->shouldReturn(false); } - /** - * @param \MongoGridFS $gridFs - * @param \MongoGridFSFile $file - */ - function it_checks_if_file_exists($gridFs, $file) + function it_checks_if_file_exists($bucket, BSONDocument $file) { - $gridFs - ->findOne('filename', array()) + $bucket + ->findOne(['filename' => 'filename']) ->willReturn($file) ; - $gridFs - ->findOne('filename2', array()) + $bucket + ->findOne(['filename' => 'filename2']) ->willReturn(null) ; @@ -87,228 +82,87 @@ function it_checks_if_file_exists($gridFs, $file) $this->exists('filename2')->shouldReturn(false); } - /** - * @param \MongoGridFS $gridFs - */ - function it_deletes_file($gridFs) + function it_deletes_file($bucket) { - $file = new \stdClass; - $file->file = array('_id' => 123); - $gridFs - ->findOne('filename', array('_id')) - ->willReturn($file) - ; - $gridFs - ->delete(123) - ->willReturn(true) + $bucket + ->findOne(['filename' => 'filename'], ['projection' => ['_id' => 1]]) + ->willReturn($file = new BSONDocument(['_id' => 123])) ; + $bucket->delete(123)->shouldBeCalled(); $this->delete('filename')->shouldReturn(true); } - /** - * @param \MongoGridFS $gridFs - */ - function it_does_not_delete_file($gridFs) + function it_does_not_delete_file($bucket) { - $file = new \stdClass; - $file->file = array('_id' => 123); - $gridFs - ->findOne('filename', array('_id')) - ->willReturn($file) - ; - $gridFs - ->delete(123) - ->willReturn(false) - ; - $this->delete('filename')->shouldReturn(false); + $bucket->findOne(['filename' => 'filename'], ['projection' => ['_id' => 1]])->willReturn(null); - $gridFs - ->findOne('filename', array('_id')) - ->willReturn(null) - ; $this->delete('filename')->shouldReturn(false); } - /** - * @param \MongoGridFS $gridFs - * @param \MongoGridFSFile $file - */ - function it_writes_file($gridFs, $file) - { - $file - ->getSize() - ->willReturn(12) - ; - $gridFs - ->findOne('filename', array()) - ->willReturn(null) - ; - $gridFs - ->storeBytes('some content', array('date' => 1234, 'someother' => 'metadata', 'filename' => 'filename')) - ->willReturn('someId') - ; - $gridFs - ->findOne(array('_id' => 'someId')) - ->willReturn($file) - ; - - $this->setMetadata('filename', array('date' => 1234, 'someother' => 'metadata')); - $this - ->write('filename', 'some content') - ->shouldReturn(12) - ; - } - - /** - * @param \MongoGridFS $gridFs - * @param \MongoGridFSFile $file - */ - function it_updates_file($gridFs, $file) + function it_writes_file($bucket) { - $someFile = new \stdClass; - $someFile->file = array('_id' => 123); - $gridFs - ->findOne('filename', array('_id')) - ->shouldBeCalled() - ->willReturn($someFile) - ; - $gridFs - ->delete(123) - ->shouldBeCalled() - ->willReturn(true) - ; + $this->resources[] = $writable = fopen('php://memory', 'rw'); - $file - ->getSize() - ->willReturn(12) - ; - $gridFs - ->findOne('filename', array()) - ->willReturn($file) - ; - $gridFs - ->storeBytes(Argument::any(), Argument::any()) - ->willReturn('someId') - ; - $gridFs - ->findOne(array('_id' => 'someId')) - ->willReturn($file) + $bucket + ->openUploadStream('filename', ['metadata' => ['someother' => 'metadata']]) + ->willReturn($writable) ; + $this->setMetadata('filename', ['someother' => 'metadata']); $this ->write('filename', 'some content') ->shouldReturn(12) ; } - /** - * @param \MongoGridFS $gridFs - * @param \MongoGridFSFile $file - */ - function it_renames_file($gridFs, $file) + function it_renames_file($bucket) { - $file - ->getBytes() - ->willReturn('some content') - ; - $file - ->getSize() - ->willReturn(12) - ; - $gridFs - ->findOne('otherFilename', array()) - ->willReturn(null) - ; - $gridFs - ->findOne('filename', array()) - ->shouldBeCalled() - ->willReturn($file) - ; - $gridFs - ->storeBytes('some content', array('date' => 1234, 'filename' => 'otherFilename')) - ->shouldBeCalled() - ->willReturn('someId') - ; + $this->resources[] = $writable = fopen('php://memory', 'rw'); + $this->resources[] = $readable = fopen('php://memory', 'rw'); + fwrite($readable, 'some content'); + fseek($readable, 0); - $fileToDelete = new \stdClass; - $fileToDelete->file = array('_id' => 123); - $gridFs - ->findOne('filename', array('_id')) - ->willReturn($fileToDelete) - ; - $gridFs - ->findOne(array('_id' => 'someId')) - ->willReturn($file) - ; - $gridFs - ->delete(123) - ->shouldBeCalled() - ->willReturn(true) + $bucket->openUploadStream('otherFilename', ['metadata' => ['some' => 'metadata']])->willReturn($writable); + $bucket->downloadToStreamByName('filename', $writable)->shouldBeCalled(); + + $bucket + ->findOne(['filename' => 'filename'], ['projection' => ['_id' => 1]]) + ->willReturn($toDelete = new BSONDocument(['_id' => 1234])) ; + $bucket->delete(1234)->shouldBeCalled(); - $this->setMetadata('otherFilename', array('date' => 1234)); + $this->setMetadata('filename', ['some' => 'metadata']); $this->rename('filename', 'otherFilename')->shouldReturn(true); } - /** - * @param \MongoGridFS $gridFs - */ - function it_fetches_keys($gridFs, $file, $otherFile) + function it_fetches_keys($bucket) { - $gridFs - ->find(array(), array('filename')) - ->willReturn(array(new TestFile('filename'), new TestFile('otherFilename'))) + $bucket + ->find([], ['projection' => ['filename' => 1]]) + ->willReturn([new BSONDocument(['filename' => 'filename']), new BSONDocument(['filename' => 'otherFilename'])]) ; - $this->keys()->shouldReturn(array('filename', 'otherFilename')); + $this->keys()->shouldReturn(['filename', 'otherFilename']); } - /** - * @param \MongoGridFS $gridFs - */ - function it_fetches_mtime($gridFs) + function it_fetches_mtime($bucket) { - $time = new \stdClass; - $time->sec = 12345; - - $someFile = new \stdClass; - $someFile->file = array('date' => $time); - $gridFs - ->findOne('filename', array('date')) - ->willReturn($someFile) + $bucket + ->findOne(['filename' => 'filename'], ['projection' => ['uploadDate' => 1]]) + ->willReturn(new BSONDocument(['uploadDate' => new UTCDateTime(12345000)])) ; $this->mtime('filename')->shouldReturn(12345); } - /** - * @param \MongoGridFS $gridFs - */ - function it_calculates_checksum($gridFs) + function it_calculates_checksum($bucket) { - $someFile = new \stdClass; - $someFile->file = array('md5' => 'md5123'); - $gridFs - ->findOne('filename', array('md5')) - ->willReturn($someFile) + $bucket + ->findOne(['filename' => 'filename'], ['projection' => ['md5' => 1]]) + ->willReturn(new BSONDocument(['md5' => 'md5123'])) ; $this->checksum('filename')->shouldReturn('md5123'); } } - -class TestFile -{ - private $filename; - - public function __construct($filename) - { - $this->filename = $filename; - } - - public function getFilename() - { - return $this->filename; - } -} diff --git a/src/Gaufrette/Adapter/GridFS.php b/src/Gaufrette/Adapter/GridFS.php index c6b9a97b6..29983a935 100644 --- a/src/Gaufrette/Adapter/GridFS.php +++ b/src/Gaufrette/Adapter/GridFS.php @@ -3,8 +3,9 @@ namespace Gaufrette\Adapter; use Gaufrette\Adapter; -use MongoGridFS as MongoGridFs; -use MongoDate; +use MongoDB\BSON\Regex; +use MongoDB\GridFS\Bucket; +use MongoDB\GridFS\Exception\FileNotFoundException; /** * Adapter for the GridFS filesystem on MongoDB database. @@ -18,15 +19,18 @@ class GridFS implements Adapter, MetadataSupporter, ListKeysAware { - private $metadata = array(); - protected $gridFS = null; + /** @var array */ + private $metadata = []; + + /** @var Bucket */ + private $bucket; /** - * @param \MongoGridFS $gridFS + * @param Bucket $bucket */ - public function __construct(MongoGridFs $gridFS) + public function __construct(Bucket $bucket) { - $this->gridFS = $gridFS; + $this->bucket = $bucket; } /** @@ -34,9 +38,17 @@ public function __construct(MongoGridFs $gridFS) */ public function read($key) { - $file = $this->find($key); + try { + $stream = $this->bucket->openDownloadStreamByName($key); + } catch (FileNotFoundException $e) { + return false; + } - return ($file) ? $file->getBytes() : false; + try { + return stream_get_contents($stream); + } finally { + fclose($stream); + } } /** @@ -44,15 +56,15 @@ public function read($key) */ public function write($key, $content) { - if ($this->exists($key)) { - $this->delete($key); - } + $stream = $this->bucket->openUploadStream($key, ['metadata' => $this->getMetadata($key)]); - $metadata = array_replace_recursive(array('date' => new MongoDate()), $this->getMetadata($key), array('filename' => $key)); - $id = $this->gridFS->storeBytes($content, $metadata); - $file = $this->gridFS->findOne(array('_id' => $id)); + try { + return fwrite($stream, $content); + } finally { + fclose($stream); + } - return $file->getSize(); + return false; } /** @@ -68,10 +80,20 @@ public function isDirectory($key) */ public function rename($sourceKey, $targetKey) { - $bytes = $this->write($targetKey, $this->read($sourceKey)); - $this->delete($sourceKey); + $metadata = $this->getMetadata($sourceKey); + $writable = $this->bucket->openUploadStream($targetKey, ['metadata' => $metadata]); + + try { + $this->bucket->downloadToStreamByName($sourceKey, $writable); + $this->setMetadata($targetKey, $metadata); + $this->delete($sourceKey); + } catch (FileNotFoundException $e) { + return false; + } finally { + fclose($writable); + } - return (boolean) $bytes; + return true; } /** @@ -79,7 +101,7 @@ public function rename($sourceKey, $targetKey) */ public function exists($key) { - return (boolean) $this->find($key); + return (boolean) $this->bucket->findOne(['filename' => $key]); } /** @@ -87,11 +109,11 @@ public function exists($key) */ public function keys() { - $keys = array(); - $cursor = $this->gridFS->find(array(), array('filename')); + $keys = []; + $cursor = $this->bucket->find([], ['projection' => ['filename' => 1]]); foreach ($cursor as $file) { - $keys[] = $file->getFilename(); + $keys[] = $file['filename']; } return $keys; @@ -102,9 +124,9 @@ public function keys() */ public function mtime($key) { - $file = $this->find($key, array('date')); + $file = $this->bucket->findOne(['filename' => $key], ['projection' => ['uploadDate' => 1]]); - return ($file) ? $file->file['date']->sec : false; + return $file ? (int) $file['uploadDate']->toDateTime()->format('U') : false; } /** @@ -112,9 +134,9 @@ public function mtime($key) */ public function checksum($key) { - $file = $this->find($key, array('md5')); + $file = $this->bucket->findOne(['filename' => $key], ['projection' => ['md5' => 1]]); - return ($file) ? $file->file['md5'] : false; + return $file ? $file['md5'] : false; } /** @@ -122,9 +144,13 @@ public function checksum($key) */ public function delete($key) { - $file = $this->find($key, array('_id')); + if (null === $file = $this->bucket->findOne(['filename' => $key], ['projection' => ['_id' => 1]])) { + return false; + } + + $this->bucket->delete($file['_id']); - return $file && $this->gridFS->delete($file->file['_id']); + return true; } /** @@ -143,11 +169,6 @@ public function getMetadata($key) return isset($this->metadata[$key]) ? $this->metadata[$key] : array(); } - private function find($key, array $fields = array()) - { - return $this->gridFS->findOne($key, $fields); - } - /** * {@inheritdoc} */ @@ -155,24 +176,22 @@ public function listKeys($prefix = '') { $prefix = trim($prefix); - if ('' == $prefix) { - return array( - 'dirs' => array(), + if ($prefix === '') { + return [ + 'dirs' => [], 'keys' => $this->keys(), - ); + ]; } - $result = array( - 'dirs' => array(), - 'keys' => array(), - ); - - $gridFiles = $this->gridFS->find(array( - 'filename' => new \MongoRegex(sprintf('/^%s/', $prefix)), - )); + $regex = new Regex(sprintf('^%s', $prefix), ''); + $files = $this->bucket->find(['filename' => $regex], ['projection' => ['filename' => 1]]); + $result = [ + 'dirs' => [], + 'keys' => [], + ]; - foreach ($gridFiles as $file) { - $result['keys'][] = $file->getFilename(); + foreach ($files as $file) { + $result['keys'][] = $file['filename']; } return $result; From 7340052b1a530f854c1c2310f42ad65c3f5aac97 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Mon, 20 Mar 2017 01:52:52 +0100 Subject: [PATCH 237/426] Add a mongodb container to execute functional tests --- docker-compose.yml | 26 +++++++++++++++++++ phpunit.xml.dist | 17 +++--------- .../Functional/Adapter/GridFSTest.php | 19 ++++++++++++++ .../Functional/adapters/GridFS.php.dist | 10 ------- 4 files changed, 48 insertions(+), 24 deletions(-) delete mode 100644 tests/Gaufrette/Functional/adapters/GridFS.php.dist diff --git a/docker-compose.yml b/docker-compose.yml index bc5bcc763..b0ba2525c 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,6 +27,11 @@ services: - './spec/:/usr/src/gaufrette/spec/' - './src/:/usr/src/gaufrette/src/' - './tests/:/usr/src/gaufrette/tests/' + environment: + MONGO_URI: "mongodb://mongodb:27017" + MONGO_DBNAME: "gridfs_test" + depends_on: + - mongodb php70: build: @@ -36,6 +41,12 @@ services: - './spec/:/usr/src/gaufrette/spec/' - './src/:/usr/src/gaufrette/src/' - './tests/:/usr/src/gaufrette/tests/' + environment: + MONGO_URI: "mongodb://mongodb:27017" + MONGO_DBNAME: "gridfs_test" + depends_on: + - mongodb + php71: build: @@ -45,6 +56,11 @@ services: - './spec/:/usr/src/gaufrette/spec/' - './src/:/usr/src/gaufrette/src/' - './tests/:/usr/src/gaufrette/tests/' + environment: + MONGO_URI: "mongodb://mongodb:27017" + MONGO_DBNAME: "gridfs_test" + depends_on: + - mongodb hhvm: build: @@ -54,3 +70,13 @@ services: - './spec/:/usr/src/gaufrette/spec/' - './src/:/usr/src/gaufrette/src/' - './tests/:/usr/src/gaufrette/tests/' + environment: + MONGO_URI: "mongodb://mongodb:27017" + MONGO_DBNAME: "gridfs_test" + depends_on: + - mongodb + + mongodb: + image: mongo + ports: + - "27017:27017" diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f23020bc1..a6cbdcbb8 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -13,20 +13,9 @@ > - - + + diff --git a/tests/Gaufrette/Functional/Adapter/GridFSTest.php b/tests/Gaufrette/Functional/Adapter/GridFSTest.php index 3e2022b08..531d15e5e 100644 --- a/tests/Gaufrette/Functional/Adapter/GridFSTest.php +++ b/tests/Gaufrette/Functional/Adapter/GridFSTest.php @@ -3,9 +3,28 @@ namespace Gaufrette\Functional\Adapter; use Gaufrette\Adapter\GridFS; +use Gaufrette\Filesystem; +use MongoDB\Client; class GridFSTest extends FunctionalTestCase { + public function setUp() + { + $uri = getenv('MONGO_URI'); + $dbname = getenv('MONGO_DBNAME'); + + if ($uri === false || $dbname === false) { + $this->markTestSkipped('Either MONGO_URI or MONGO_DBNAME env variables are not defined.'); + } + + $client = new Client($uri); + $db = $client->selectDatabase($dbname); + $bucket = $db->selectGridFSBucket(); + $bucket->drop(); + + $this->filesystem = new Filesystem(new GridFS($bucket)); + } + /** * @test */ diff --git a/tests/Gaufrette/Functional/adapters/GridFS.php.dist b/tests/Gaufrette/Functional/adapters/GridFS.php.dist deleted file mode 100644 index 8c2310582..000000000 --- a/tests/Gaufrette/Functional/adapters/GridFS.php.dist +++ /dev/null @@ -1,10 +0,0 @@ -selectDB('gaufrette'); - -$grid = $db->getGridFS(); -$grid->remove(); - -return new Gaufrette\Adapter\GridFS($grid); From 890ed3231de2794e25e4972385b72ca375106f93 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Mon, 20 Mar 2017 01:57:36 +0100 Subject: [PATCH 238/426] Enable functional tests for GridFS adapter on travis --- .travis.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index acff5521d..7820f2dba 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,18 +7,23 @@ cache: - $HOME/.composer/cache/files php: - - 5.4 - - 5.5 - 5.6 - hhvm +services: + - mongodb + +env: + - MONGO_URI="mongodb://127.0.0.1:27017" + MONGO_DBNAME="gridfs_test" + matrix: allow_failures: - php: hhvm fast_finish: true before_script: - - if [[ ${TRAVIS_PHP_VERSION:0:1} == "5" ]]; then echo "extension = mongo.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini && phpenv config-rm xdebug.ini; fi + - if [[ ${TRAVIS_PHP_VERSION:0:1} == "5" ]]; then echo "extension = mongodb.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini && phpenv config-rm xdebug.ini; fi - if [[ ${TRAVIS_PHP_VERSION:0:4} == "hhvm" ]]; then composer require mongofill/mongofill --ignore-platform-reqs; fi - sudo apt-get update - sudo apt-get install -y libssh2-1-dev @@ -29,4 +34,4 @@ before_script: script: - php vendor/bin/phpspec run -fpretty --verbose - - php vendor/bin/phpunit + - php vendor/bin/phpunit --verbose From d981086cea9e40a6e0d5a84dbf2ea6c074dceb7c Mon Sep 17 00:00:00 2001 From: Will Bradley Date: Tue, 21 Mar 2017 17:10:28 -0700 Subject: [PATCH 239/426] Moving content-type accessors inside try-catch New style array accessors --- src/Gaufrette/Adapter/AwsS3.php | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index 438bd81c1..991c5a95f 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -92,14 +92,16 @@ public function read($key) $this->ensureBucketExists(); $options = $this->getOptions($key); - // Copy ContentType metadata to local metadata - $object = $this->service->getObject($options); - if (!array_key_exists($key, $this->metadata) || !is_array($this->metadata[$key])) { - $this->metadata[$key] = array(); - } - $this->metadata[$key]['ContentType'] = $object->get('ContentType'); - try { + // Get remote object + $object = $this->service->getObject($options); + // If there's no metadata array set up for this object, set it up + if (!array_key_exists($key, $this->metadata) || !is_array($this->metadata[$key])) { + $this->metadata[$key] = []; + } + // Make remote ContentType metadata available locally + $this->metadata[$key]['ContentType'] = $object->get('ContentType'); + return (string) $object->get('Body'); } catch (\Exception $e) { return false; From 30084902b36d9ac1680463fd36db2c17c4846f9e Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Thu, 16 Mar 2017 01:44:46 +0100 Subject: [PATCH 240/426] Upgrade aws-sdk-php to v3 We're stuck with guzzle/guzzle, which is abandoned, because of aws-sdk-php v2. Hence we need to upgrade aws-sdk-php to v3 which relies on guzzlehttp/guzzle v5/v6. --- composer.json | 2 +- doc/adapters/aws-s3.md | 4 +- src/Gaufrette/Adapter/AwsS3.php | 50 +++++---- .../Functional/Adapter/AwsS3Test.php | 102 +++++++++--------- 4 files changed, 85 insertions(+), 73 deletions(-) diff --git a/composer.json b/composer.json index 2006362e8..c4790d3eb 100644 --- a/composer.json +++ b/composer.json @@ -29,7 +29,7 @@ "microsoft/windowsazure": "<0.4.3" }, "require-dev": { - "aws/aws-sdk-php": "^2.4.12", + "aws/aws-sdk-php": "^2.4.12||~3", "amazonwebservices/aws-sdk-for-php": "1.5.*", "rackspace/php-opencloud" : "^1.9.2", "google/apiclient": "~1.1.3", diff --git a/doc/adapters/aws-s3.md b/doc/adapters/aws-s3.md index 9166ea603..852438dc1 100644 --- a/doc/adapters/aws-s3.md +++ b/doc/adapters/aws-s3.md @@ -18,14 +18,14 @@ use Aws\S3\S3Client; use Gaufrette\Adapter\AwsS3 as AwsS3Adapter; use Gaufrette\Filesystem; -$s3client = S3Client::factory(array( +$s3client = new S3Client([ 'credentials' => array( 'key' => 'your_key_here', 'secret' => 'your_secret', ), 'version' => 'latest', 'region' => 'eu-west-1', -)); +]); $adapter = new AwsS3Adapter($s3client,'your-bucket-name'); $filesystem = new Filesystem($adapter); ``` diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index 2fc23472a..e7754a6a2 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -16,23 +16,35 @@ class AwsS3 implements Adapter, ListKeysAware, SizeCalculator { + /** @var S3Client */ protected $service; + /** @var string */ protected $bucket; + /** @var array */ protected $options; + /** @var bool */ protected $bucketExists; - protected $metadata = array(); + /** @var array */ + protected $metadata = []; + /** @var bool */ protected $detectContentType; - public function __construct(S3Client $service, $bucket, array $options = array(), $detectContentType = false) + /** + * @param S3Client $service + * @param string $bucket + * @param array $options + * @param bool $detectContentType + */ + public function __construct(S3Client $service, $bucket, array $options = [], $detectContentType = false) { $this->service = $service; $this->bucket = $bucket; $this->options = array_replace( - array( + [ 'create' => false, 'directory' => '', 'acl' => 'private', - ), + ], $options ); @@ -51,7 +63,7 @@ public function __construct(S3Client $service, $bucket, array $options = array() * * @return string */ - public function getUrl($key, array $options = array()) + public function getUrl($key, array $options = []) { return $this->service->getObjectUrl( $this->bucket, @@ -80,7 +92,7 @@ public function setMetadata($key, $metadata) */ public function getMetadata($key) { - return isset($this->metadata[$key]) ? $this->metadata[$key] : array(); + return isset($this->metadata[$key]) ? $this->metadata[$key] : []; } /** @@ -115,9 +127,7 @@ public function rename($sourceKey, $targetKey) $this->ensureBucketExists(); $options = $this->getOptions( $targetKey, - array( - 'CopySource' => $this->bucket.'/'.$this->computePath($sourceKey), - ) + ['CopySource' => $this->bucket.'/'.$this->computePath($sourceKey)] ); try { @@ -135,7 +145,7 @@ public function rename($sourceKey, $targetKey) public function write($key, $content) { $this->ensureBucketExists(); - $options = $this->getOptions($key, array('Body' => $content)); + $options = $this->getOptions($key, ['Body' => $content]); /* * If the ContentType was not already set in the metadata, then we autodetect @@ -207,14 +217,14 @@ public function keys() */ public function listKeys($prefix = '') { - $options = array('Bucket' => $this->bucket); + $options = ['Bucket' => $this->bucket]; if ((string) $prefix != '') { $options['Prefix'] = $this->computePath($prefix); } elseif (!empty($this->options['directory'])) { $options['Prefix'] = $this->options['directory']; } - $keys = array(); + $keys = []; $iter = $this->service->getIterator('ListObjects', $options); foreach ($iter as $file) { $keys[] = $this->computeKey($file['Key']); @@ -242,11 +252,11 @@ public function delete($key) */ public function isDirectory($key) { - $result = $this->service->listObjects(array( + $result = $this->service->listObjects([ 'Bucket' => $this->bucket, 'Prefix' => rtrim($this->computePath($key), '/').'/', 'MaxKeys' => 1, - )); + ]); return count($result['Contents']) > 0; } @@ -277,18 +287,16 @@ protected function ensureBucketExists() )); } - $options = array('Bucket' => $this->bucket); - if ($this->service->getRegion() != 'us-east-1') { - $options['LocationConstraint'] = $this->service->getRegion(); - } - - $this->service->createBucket($options); + $this->service->createBucket([ + 'Bucket' => $this->bucket, + 'LocationConstraint' => $this->service->getRegion() + ]); $this->bucketExists = true; return true; } - protected function getOptions($key, array $options = array()) + protected function getOptions($key, array $options = []) { $options['ACL'] = $this->options['acl']; $options['Bucket'] = $this->bucket; diff --git a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php index 6d01ab373..e2fbb8109 100644 --- a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php +++ b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php @@ -2,41 +2,50 @@ namespace Gaufrette\Functional\Adapter; -use Gaufrette\Adapter\AwsS3; +use Aws\CommandInterface; +use Aws\Result; +use Aws\S3\Exception\S3Exception; use Aws\S3\S3Client; -use Guzzle\Plugin\Mock\MockPlugin; -use Guzzle\Http\Message\Response; +use Aws\MockHandler; +use Gaufrette\Adapter\AwsS3; /** * @todo move to phpspec */ class AwsS3Test extends \PHPUnit_Framework_TestCase { - protected function getClient() + protected function getClient(MockHandler $mock = null) + { + return new S3Client([ + 'handler' => $mock, + 'region' => 'eu-west-1', + 'version' => 'latest', + 'credentials' => [ + 'key' => 'foo', + 'secret' => 'bar', + ], + ]); + } + + private function mockException(array $context = []) { - return S3Client::factory(array( - 'key' => 'foo', - 'secret' => 'bar' - )); + return function (CommandInterface $command) use ($context) { + return new S3Exception('Mock exception', $command, $context); + }; } public function testCreatesBucketIfMissing() { - $mock = new MockPlugin(array( - new Response(404), // Head bucket response - new Response(200), // Create bucket response - new Response(200, array(), 'foo') // Get object response - )); - $client = $this->getClient(); - $client->addSubscriber($mock); + $mock = new MockHandler([ + $this->mockException(['code' => 'NoSuchBucket']), + new Result(), + new Result(['Body' => 'foo']), + ]); + $client = $this->getClient($mock); $adapter = new AwsS3($client, 'bucket', array('create' => true)); - $this->assertEquals('foo', $adapter->read('foo')); - $requests = $mock->getReceivedRequests(); - $this->assertEquals('HEAD', $requests[0]->getMethod()); - $this->assertEquals('PUT', $requests[1]->getMethod()); - $this->assertEquals('GET', $requests[2]->getMethod()); - $this->assertEquals('bucket.s3.amazonaws.com', $requests[0]->getHost()); + $this->assertEquals('foo', $adapter->read('foo')); + $this->assertEquals(0, $mock->count()); } /** @@ -44,66 +53,61 @@ public function testCreatesBucketIfMissing() */ public function testThrowsExceptionIfBucketMissingAndNotCreating() { - $mock = new MockPlugin(array(new Response(404))); - $client = $this->getClient(); - $client->addSubscriber($mock); + $mock = new MockHandler([ + $this->mockException(['code' => 'NoSuchBucket']) + ]); + $client = $this->getClient($mock); $adapter = new AwsS3($client, 'bucket'); + $adapter->read('foo'); + } public function testWritesObjects() { - $mock = new MockPlugin(array( - new Response(200), // HEAD bucket response - new Response(201) // PUT object response - )); - $client = $this->getClient(); - $client->addSubscriber($mock); + $mock = new MockHandler([ + new Result(), + new Result(), + ]); + $client = $this->getClient($mock); $adapter = new AwsS3($client, 'bucket'); + $this->assertEquals(7, $adapter->write('foo', 'testing')); - $requests = $mock->getReceivedRequests(); - $this->assertEquals('bucket.s3.amazonaws.com', $requests[1]->getHost()); - $this->assertEquals('PUT', $requests[1]->getMethod()); + $this->assertEquals(0, $mock->count()); } public function testChecksForObjectExistence() { - $mock = new MockPlugin(array(new Response(200))); - $client = $this->getClient(); - $client->addSubscriber($mock); + $mock = new MockHandler([new Result()]); + $client = $this->getClient($mock); $adapter = new AwsS3($client, 'bucket'); + $this->assertTrue($adapter->exists('foo')); - $requests = $mock->getReceivedRequests(); - $this->assertEquals('bucket.s3.amazonaws.com', $requests[0]->getHost()); - $this->assertEquals('HEAD', $requests[0]->getMethod()); - $this->assertEquals('/foo', $requests[0]->getResource()); + $this->assertEquals(0, $mock->count()); } public function testGetsObjectUrls() { $client = $this->getClient(); $adapter = new AwsS3($client, 'bucket'); - $this->assertEquals('https://bucket.s3.amazonaws.com/foo', $adapter->getUrl('foo')); + $this->assertEquals('https://s3-eu-west-1.amazonaws.com/bucket/foo', $adapter->getUrl('foo')); } public function testChecksForObjectExistenceWithDirectory() { - $mock = new MockPlugin(array(new Response(200))); - $client = $this->getClient(); - $client->addSubscriber($mock); + $mock = new MockHandler([new Result()]); + $client = $this->getClient($mock); $adapter = new AwsS3($client, 'bucket', array('directory' => 'bar')); + $this->assertTrue($adapter->exists('foo')); - $requests = $mock->getReceivedRequests(); - $this->assertEquals('bucket.s3.amazonaws.com', $requests[0]->getHost()); - $this->assertEquals('HEAD', $requests[0]->getMethod()); - $this->assertEquals('/bar/foo', $requests[0]->getResource()); + $this->assertEquals(0, $mock->count()); } public function testGetsObjectUrlsWithDirectory() { $client = $this->getClient(); $adapter = new AwsS3($client, 'bucket', array('directory' => 'bar')); - $this->assertEquals('https://bucket.s3.amazonaws.com/bar/foo', $adapter->getUrl('foo')); + $this->assertEquals('https://s3-eu-west-1.amazonaws.com/bucket/bar/foo', $adapter->getUrl('foo')); } public function shouldListKeysWithoutDirectory() From 291e3a4141d1b53ad2fcb3afdee229cb110f9bc0 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Wed, 22 Mar 2017 02:33:04 +0100 Subject: [PATCH 241/426] Remove mocked http responses from functional tests for aws adapter --- .env.dist | 6 + .gitignore | 1 + .travis.yml | 5 + appveyor.yml | 7 + docker-compose.yml | 20 +-- phpunit.xml.dist | 4 + .../Functional/Adapter/AwsS3Test.php | 158 ++++++++++-------- 7 files changed, 121 insertions(+), 80 deletions(-) create mode 100644 .env.dist diff --git a/.env.dist b/.env.dist new file mode 100644 index 000000000..c62e2c1f8 --- /dev/null +++ b/.env.dist @@ -0,0 +1,6 @@ +AWS_KEY= +AWS_SECRET= +AWS_BUCKET= + +MONGO_URI=mongodb://mongodb:27017 +MONGO_DBNAME=gridfs_test diff --git a/.gitignore b/.gitignore index 3648dbec2..d5e9e375a 100644 --- a/.gitignore +++ b/.gitignore @@ -5,5 +5,6 @@ doc/.couscous tests/adapters/* !tests/adapters/*.dist vendor/ +.env composer.lock phpunit.xml diff --git a/.travis.yml b/.travis.yml index 7820f2dba..97ecadc45 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,11 @@ cache: directories: - $HOME/.composer/cache/files +env: + global: + - AWS_KEY: + - AWS_SECRET: + php: - 5.6 - hhvm diff --git a/appveyor.yml b/appveyor.yml index 60b74d682..f5d892c5c 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -8,6 +8,13 @@ branches: cache: - '%LOCALAPPDATA%\Composer\files' +environment: + AWS_KEY: + secure: nMVfvhojgVc9VBlrYXeddaYIQHlVAlupWmxNjXMlsB0= + AWS_SECRET: + secure: MiAf2q7mnqSIZSoChd9v3pwUrXHeAEpZP8Zc0E0qjiOBXPvplzJVtrkWRwTNlQl3 + AWS_BUCKET: gaufretteci + init: - SET PATH=C:\Program Files\OpenSSL;c:\tools\php;%PATH% diff --git a/docker-compose.yml b/docker-compose.yml index b0ba2525c..64f9500d2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,3 +1,4 @@ + version: '2' services: @@ -9,6 +10,7 @@ services: - './spec/:/usr/src/gaufrette/spec/' - './src/:/usr/src/gaufrette/src/' - './tests/:/usr/src/gaufrette/tests/' + env_file: .env php55: build: @@ -18,6 +20,7 @@ services: - './spec/:/usr/src/gaufrette/spec/' - './src/:/usr/src/gaufrette/src/' - './tests/:/usr/src/gaufrette/tests/' + env_file: .env php56: build: @@ -27,11 +30,9 @@ services: - './spec/:/usr/src/gaufrette/spec/' - './src/:/usr/src/gaufrette/src/' - './tests/:/usr/src/gaufrette/tests/' - environment: - MONGO_URI: "mongodb://mongodb:27017" - MONGO_DBNAME: "gridfs_test" depends_on: - mongodb + env_file: .env php70: build: @@ -41,12 +42,9 @@ services: - './spec/:/usr/src/gaufrette/spec/' - './src/:/usr/src/gaufrette/src/' - './tests/:/usr/src/gaufrette/tests/' - environment: - MONGO_URI: "mongodb://mongodb:27017" - MONGO_DBNAME: "gridfs_test" depends_on: - mongodb - + env_file: .env php71: build: @@ -56,9 +54,7 @@ services: - './spec/:/usr/src/gaufrette/spec/' - './src/:/usr/src/gaufrette/src/' - './tests/:/usr/src/gaufrette/tests/' - environment: - MONGO_URI: "mongodb://mongodb:27017" - MONGO_DBNAME: "gridfs_test" + env_file: .env depends_on: - mongodb @@ -70,9 +66,7 @@ services: - './spec/:/usr/src/gaufrette/spec/' - './src/:/usr/src/gaufrette/src/' - './tests/:/usr/src/gaufrette/tests/' - environment: - MONGO_URI: "mongodb://mongodb:27017" - MONGO_DBNAME: "gridfs_test" + env_file: .env depends_on: - mongodb diff --git a/phpunit.xml.dist b/phpunit.xml.dist index a6cbdcbb8..f813e2d40 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -16,6 +16,10 @@ + + + diff --git a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php index e2fbb8109..33b7c8ae7 100644 --- a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php +++ b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php @@ -2,50 +2,84 @@ namespace Gaufrette\Functional\Adapter; -use Aws\CommandInterface; -use Aws\Result; -use Aws\S3\Exception\S3Exception; use Aws\S3\S3Client; -use Aws\MockHandler; use Gaufrette\Adapter\AwsS3; +use Gaufrette\Filesystem; /** * @todo move to phpspec */ class AwsS3Test extends \PHPUnit_Framework_TestCase { - protected function getClient(MockHandler $mock = null) + static private $SDK_VERSION = 3; + + /** @var string */ + private $bucket; + + /** @var S3Client */ + private $client; + + public static function setUpBeforeClass() { - return new S3Client([ - 'handler' => $mock, - 'region' => 'eu-west-1', - 'version' => 'latest', - 'credentials' => [ - 'key' => 'foo', - 'secret' => 'bar', - ], - ]); + $installed = json_decode(file_get_contents(__DIR__.'/../../../../vendor/composer/installed.json'), true); + $sdk = current(array_filter($installed, function ($dependency) { + return $dependency['name'] === 'aws/aws-sdk-php'; + })); + + if (version_compare($sdk['version'], '3.0.0') === -1) { + self::$SDK_VERSION = 2; + } } - private function mockException(array $context = []) + public function setUp() { - return function (CommandInterface $command) use ($context) { - return new S3Exception('Mock exception', $command, $context); - }; + $key = getenv('AWS_KEY'); + $secret = getenv('AWS_SECRET'); + + if (empty($key) || empty($secret)) { + $this->markTestSkipped(); + } + + $this->bucket = uniqid(getenv('AWS_BUCKET')); + + if (self::$SDK_VERSION === 3) { + $this->client = new S3Client([ + 'region' => 'eu-west-1', + 'version' => 'latest', + 'credentials' => [ + 'key' => $key, + 'secret' => $secret, + ], + ]); + } else { + $this->client = S3Client::factory([ + 'region' => 'eu-west-1', + 'version' => '2006-03-01', + 'key' => $key, + 'secret' => $secret, + ]); + } + } + + public function tearDown() + { + if ($this->bucket === null) { + return; + } + + $result = $this->client->listObjects(['Bucket' => $this->bucket]); + $staleObjects = $result->get('Contents'); + + foreach ($staleObjects as $staleObject) { + $this->client->deleteObject(['Bucket' => $this->bucket, 'Key' => $staleObject['Key']]); + } + + $this->client->deleteBucket(['Bucket' => $this->bucket]); } - public function testCreatesBucketIfMissing() + private function getFilesystem(array $adapterOptions = []) { - $mock = new MockHandler([ - $this->mockException(['code' => 'NoSuchBucket']), - new Result(), - new Result(['Body' => 'foo']), - ]); - $client = $this->getClient($mock); - $adapter = new AwsS3($client, 'bucket', array('create' => true)); - - $this->assertEquals('foo', $adapter->read('foo')); - $this->assertEquals(0, $mock->count()); + return new Filesystem(new AwsS3($this->client, $this->bucket, $adapterOptions)); } /** @@ -53,69 +87,59 @@ public function testCreatesBucketIfMissing() */ public function testThrowsExceptionIfBucketMissingAndNotCreating() { - $mock = new MockHandler([ - $this->mockException(['code' => 'NoSuchBucket']) - ]); - $client = $this->getClient($mock); - $adapter = new AwsS3($client, 'bucket'); - - $adapter->read('foo'); - + $this->bucket = null; + $filesystem = $this->getFilesystem(); + $filesystem->read('foo'); } public function testWritesObjects() { - $mock = new MockHandler([ - new Result(), - new Result(), - ]); - $client = $this->getClient($mock); - $adapter = new AwsS3($client, 'bucket'); - - $this->assertEquals(7, $adapter->write('foo', 'testing')); - $this->assertEquals(0, $mock->count()); + $filesystem = $this->getFilesystem(['create' => true]); + $this->assertEquals(7, $filesystem->write('foo', 'testing')); } public function testChecksForObjectExistence() { - $mock = new MockHandler([new Result()]); - $client = $this->getClient($mock); - $adapter = new AwsS3($client, 'bucket'); - - $this->assertTrue($adapter->exists('foo')); - $this->assertEquals(0, $mock->count()); + $filesystem = $this->getFilesystem(['create' => true]); + $filesystem->write('foo', ''); + $this->assertTrue($filesystem->has('foo')); } public function testGetsObjectUrls() { - $client = $this->getClient(); - $adapter = new AwsS3($client, 'bucket'); - $this->assertEquals('https://s3-eu-west-1.amazonaws.com/bucket/foo', $adapter->getUrl('foo')); + $filesystem = $this->getFilesystem(['create' => true]); + + $this->assertEquals( + sprintf('https://%s.s3-eu-west-1.amazonaws.com/foo', $this->bucket), + $filesystem->getAdapter()->getUrl('foo') + ); + $this->bucket = null; } public function testChecksForObjectExistenceWithDirectory() { - $mock = new MockHandler([new Result()]); - $client = $this->getClient($mock); - $adapter = new AwsS3($client, 'bucket', array('directory' => 'bar')); + $filesystem = $this->getFilesystem(['directory' => 'bar', 'create' => true]); + $filesystem->write('foo', ''); - $this->assertTrue($adapter->exists('foo')); - $this->assertEquals(0, $mock->count()); + $this->assertTrue($filesystem->has('foo')); } public function testGetsObjectUrlsWithDirectory() { - $client = $this->getClient(); - $adapter = new AwsS3($client, 'bucket', array('directory' => 'bar')); - $this->assertEquals('https://s3-eu-west-1.amazonaws.com/bucket/bar/foo', $adapter->getUrl('foo')); + $filesystem = $this->getFilesystem(['directory' => 'bar']); + + $this->assertEquals( + sprintf('https://%s.s3-eu-west-1.amazonaws.com/bar/foo', $this->bucket), + $filesystem->getAdapter()->getUrl('foo') + ); + $this->bucket = null; } public function shouldListKeysWithoutDirectory() { - $client = $this->getClient(); - $adapter = new AwsS3($client, 'bucket', array('directory' => 'bar')); - $adapter->write('test.txt', 'some content'); - $keys = $adapter->listKeys(); + $filesystem = $this->getFilesystem(); + $filesystem->write('test.txt', 'some content'); + $keys = $filesystem->listKeys(); $this->assertEquals('test.txt', $keys['key']); } } From 684e9d5dcf2239cf61aaa782951696a6dad7de39 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Thu, 23 Mar 2017 23:22:45 +0100 Subject: [PATCH 242/426] Detect aws sdk version based on getArguments method Remove the dependency on installed.json created by composer to detect the version of aws sdk. In this way we only depend on S3Client. --- phpunit.xml.dist | 5 +++-- .../Functional/Adapter/AwsS3Test.php | 20 +++++++------------ 2 files changed, 10 insertions(+), 15 deletions(-) diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f813e2d40..4ac3fb545 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -18,8 +18,9 @@ --> - + diff --git a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php index 33b7c8ae7..5293abfe6 100644 --- a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php +++ b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php @@ -11,7 +11,8 @@ */ class AwsS3Test extends \PHPUnit_Framework_TestCase { - static private $SDK_VERSION = 3; + /** @var int */ + static private $SDK_VERSION; /** @var string */ private $bucket; @@ -19,18 +20,6 @@ class AwsS3Test extends \PHPUnit_Framework_TestCase /** @var S3Client */ private $client; - public static function setUpBeforeClass() - { - $installed = json_decode(file_get_contents(__DIR__.'/../../../../vendor/composer/installed.json'), true); - $sdk = current(array_filter($installed, function ($dependency) { - return $dependency['name'] === 'aws/aws-sdk-php'; - })); - - if (version_compare($sdk['version'], '3.0.0') === -1) { - self::$SDK_VERSION = 2; - } - } - public function setUp() { $key = getenv('AWS_KEY'); @@ -40,9 +29,14 @@ public function setUp() $this->markTestSkipped(); } + if (self::$SDK_VERSION === null) { + self::$SDK_VERSION = method_exists(S3Client::class, 'getArguments') ? 3 : 2; + } + $this->bucket = uniqid(getenv('AWS_BUCKET')); if (self::$SDK_VERSION === 3) { + // New way of instantiating S3Client for aws-sdk-php v3 $this->client = new S3Client([ 'region' => 'eu-west-1', 'version' => 'latest', From 76971653d3ae983241b71923e5ca7ee48718b7b2 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Fri, 24 Mar 2017 00:21:27 +0100 Subject: [PATCH 243/426] Fix expected file url for aws sdk v3 --- .../Functional/Adapter/AwsS3Test.php | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php index 5293abfe6..c98eb3586 100644 --- a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php +++ b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php @@ -57,7 +57,7 @@ public function setUp() public function tearDown() { - if ($this->bucket === null) { + if (!$this->client->doesBucketExist($this->bucket)) { return; } @@ -81,7 +81,6 @@ private function getFilesystem(array $adapterOptions = []) */ public function testThrowsExceptionIfBucketMissingAndNotCreating() { - $this->bucket = null; $filesystem = $this->getFilesystem(); $filesystem->read('foo'); } @@ -102,12 +101,16 @@ public function testChecksForObjectExistence() public function testGetsObjectUrls() { $filesystem = $this->getFilesystem(['create' => true]); + $expected = sprintf('https://%s.s3-eu-west-1.amazonaws.com/foo', $this->bucket); + + if (self::$SDK_VERSION === 3) { + $expected = sprintf('https://s3-eu-west-1.amazonaws.com/%s/foo', $this->bucket); + } $this->assertEquals( - sprintf('https://%s.s3-eu-west-1.amazonaws.com/foo', $this->bucket), + $expected, $filesystem->getAdapter()->getUrl('foo') ); - $this->bucket = null; } public function testChecksForObjectExistenceWithDirectory() @@ -121,12 +124,16 @@ public function testChecksForObjectExistenceWithDirectory() public function testGetsObjectUrlsWithDirectory() { $filesystem = $this->getFilesystem(['directory' => 'bar']); + $expected = sprintf('https://%s.s3-eu-west-1.amazonaws.com/bar/foo', $this->bucket); + + if (self::$SDK_VERSION === 3) { + $expected = sprintf('https://s3-eu-west-1.amazonaws.com/%s/bar/foo', $this->bucket); + } $this->assertEquals( - sprintf('https://%s.s3-eu-west-1.amazonaws.com/bar/foo', $this->bucket), + $expected, $filesystem->getAdapter()->getUrl('foo') ); - $this->bucket = null; } public function shouldListKeysWithoutDirectory() From 0af0e360c0c0f12f41b429a98101bef6c7bdbfd7 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Tue, 28 Mar 2017 23:27:52 +0200 Subject: [PATCH 244/426] Add recommended IAM policy --- doc/adapters/aws-s3.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/doc/adapters/aws-s3.md b/doc/adapters/aws-s3.md index 852438dc1..838e59cfa 100644 --- a/doc/adapters/aws-s3.md +++ b/doc/adapters/aws-s3.md @@ -9,6 +9,39 @@ First, you will need to install AWS SDK for PHP: composer require aws/aws-sdk-php ``` +In order to use this adapter you'll need an access key and a secret key. +We **strongly recommend** you to create a dedicated IAM user with the most restrictive policy: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:CreateBucket", + "s3:DeleteBucket", + "s3:ListBucket" + ], + "Resource": [ + "arn:aws:s3:::bucket_name" + ] + }, + { + "Effect": "Allow", + "Action": [ + "s3:PutObject", + "s3:GetObject", + "s3:DeleteObject" + ], + "Resource": [ + "arn:aws:s3:::bucket_name/*" + ] + } + ] +} +``` + ## Example ```php From 2e5d969ef9732a2c335489d6a758b46f8bb52676 Mon Sep 17 00:00:00 2001 From: Antoine Lelaisant Date: Fri, 31 Mar 2017 14:35:10 +0200 Subject: [PATCH 245/426] Fixed missing leading "\" before Google_Http_Request --- src/Gaufrette/Adapter/GoogleCloudStorage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/GoogleCloudStorage.php b/src/Gaufrette/Adapter/GoogleCloudStorage.php index 946e23200..00ba518c9 100644 --- a/src/Gaufrette/Adapter/GoogleCloudStorage.php +++ b/src/Gaufrette/Adapter/GoogleCloudStorage.php @@ -95,7 +95,7 @@ public function read($key) } if (class_exists('Google_Http_Request')) { - $request = new Google_Http_Request($object->getMediaLink()); + $request = new \Google_Http_Request($object->getMediaLink()); $this->service->getClient()->getAuth()->sign($request); $response = $this->service->getClient()->getIo()->executeRequest($request); if ($response[2] == 200) { From c33a07f6f0fc92caa444eeb1f6ec619a433e1468 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Tue, 11 Apr 2017 01:17:46 +0200 Subject: [PATCH 246/426] Fix AwsS3 tests --- tests/Gaufrette/Functional/Adapter/AwsS3Test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php index c98eb3586..9b2b45fc0 100644 --- a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php +++ b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php @@ -57,7 +57,7 @@ public function setUp() public function tearDown() { - if (!$this->client->doesBucketExist($this->bucket)) { + if ($this->client === null || !$this->client->doesBucketExist($this->bucket)) { return; } From 6c0578d2261f36afcf1b149d4d53130abb4c9cce Mon Sep 17 00:00:00 2001 From: Konstantin Scheumann Date: Thu, 16 Feb 2017 13:49:10 +0100 Subject: [PATCH 247/426] =?UTF-8?q?Ftp=20Adapter:=20Use=20=E2=80=9ECreate?= =?UTF-8?q?=20Dir=E2=80=9C=20Parameter=20for=20Rename?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/Gaufrette/Adapter/Ftp.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index 6c7ef124c..856a18e38 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -111,7 +111,7 @@ public function rename($sourceKey, $targetKey) $sourcePath = $this->computePath($sourceKey); $targetPath = $this->computePath($targetKey); - $this->ensureDirectoryExists(\Gaufrette\Util\Path::dirname($targetPath)); + $this->ensureDirectoryExists(\Gaufrette\Util\Path::dirname($targetPath), true); return ftp_rename($this->getConnection(), $sourcePath, $targetPath); } From 967307b7481d87a60856e34055244060f8bda21c Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Tue, 11 Apr 2017 09:29:44 +0200 Subject: [PATCH 248/426] Don't use FTP_USEPASSVADDRESS before php 5.6.18 See https://github.com/php/php-src/commit/90a26a4844d1acfecb3bd0842da333f8bddd45c6 --- src/Gaufrette/Adapter/Ftp.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index 6c7ef124c..eff8e1153 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -528,7 +528,7 @@ private function connect() $this->connection = ftp_ssl_connect($this->host, $this->port, $this->timeout); } - if (PHP_VERSION_ID >= 50600) { + if (PHP_VERSION_ID >= 50618) { ftp_set_option($this->connection, FTP_USEPASVADDRESS, false); } From 1e63a4fb205730d6537a2a037ae46258d742b78e Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Tue, 28 Mar 2017 23:36:55 +0200 Subject: [PATCH 249/426] Recommend to manually create bucket for prod env --- doc/adapters/aws-s3.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/doc/adapters/aws-s3.md b/doc/adapters/aws-s3.md index 838e59cfa..b05bdc683 100644 --- a/doc/adapters/aws-s3.md +++ b/doc/adapters/aws-s3.md @@ -10,7 +10,8 @@ composer require aws/aws-sdk-php ``` In order to use this adapter you'll need an access key and a secret key. -We **strongly recommend** you to create a dedicated IAM user with the most restrictive policy: +We **strongly recommend** you to create a dedicated IAM user with the most restrictive policy +(in such case you can remove `s3:CreateBucket` action): ```json { @@ -20,7 +21,6 @@ We **strongly recommend** you to create a dedicated IAM user with the most restr "Effect": "Allow", "Action": [ "s3:CreateBucket", - "s3:DeleteBucket", "s3:ListBucket" ], "Resource": [ @@ -42,6 +42,9 @@ We **strongly recommend** you to create a dedicated IAM user with the most restr } ``` +You can even skip `s3:CreateBucket` role if you create your bucket first, which is also recommended +for production environment. + ## Example ```php @@ -51,6 +54,7 @@ use Aws\S3\S3Client; use Gaufrette\Adapter\AwsS3 as AwsS3Adapter; use Gaufrette\Filesystem; +// For aws-sdk-php v3 $s3client = new S3Client([ 'credentials' => array( 'key' => 'your_key_here', From 0406627e9dc2c197ffb8feb159df6d0087fdf5d3 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Thu, 13 Apr 2017 22:35:06 +0200 Subject: [PATCH 250/426] Enable FTP_USEPASVADDRESS option only if the constant exists As stated by @stof in #477, FTP_USEPASVADDRESS is also not supported by PHP 7.0.0 & 7.0.1. See php/php-src@90a26a4. --- src/Gaufrette/Adapter/Ftp.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index eff8e1153..c62706d83 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -528,7 +528,7 @@ private function connect() $this->connection = ftp_ssl_connect($this->host, $this->port, $this->timeout); } - if (PHP_VERSION_ID >= 50618) { + if (defined('FTP_USEPASVADDRESS')) { ftp_set_option($this->connection, FTP_USEPASVADDRESS, false); } From f1996fad2fa316c8a6c76fcfebc9dabf3fe7e60a Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Wed, 12 Apr 2017 15:22:39 +0200 Subject: [PATCH 251/426] Use newly created aws-s3-adapter metapackage in doc --- doc/adapters/aws-s3.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/doc/adapters/aws-s3.md b/doc/adapters/aws-s3.md index 852438dc1..027473e46 100644 --- a/doc/adapters/aws-s3.md +++ b/doc/adapters/aws-s3.md @@ -4,7 +4,12 @@ currentMenu: aws-s3 # AWS S3 -First, you will need to install AWS SDK for PHP: +First, you will need to install the adapter: +```bash +composer require gaufrette/aws-s3-adapter +``` + +If you want a specific version of AWS SDK (both v2 and v3 are supported), you can require it: ```bash composer require aws/aws-sdk-php ``` From 665d49a34beb9708edff29b076f93952de93a13c Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Tue, 28 Mar 2017 23:55:37 +0200 Subject: [PATCH 252/426] Add rename method to File --- doc/basic-usage.md | 3 +++ spec/Gaufrette/FileSpec.php | 6 ++++++ src/Gaufrette/File.php | 12 ++++++++++++ src/Gaufrette/Filesystem.php | 4 ++++ 4 files changed, 25 insertions(+) diff --git a/doc/basic-usage.md b/doc/basic-usage.md index 50d2f0ea6..e87793cd7 100644 --- a/doc/basic-usage.md +++ b/doc/basic-usage.md @@ -24,4 +24,7 @@ $filesystem->write('myFile', 'Hello world!'); $file = $filesystem->get('myFile'); echo sprintf('%s (modified %s): %s', $file->getKey(), date('d/m/Y, H:i:s', $file->getMtime()), $file->getContent()); // Will print something like: "myFile (modified 17/01/2016 18:40:36): Hello world!" + +// You can also rename your file like this: +$file->rename('my/new/file'); ``` diff --git a/spec/Gaufrette/FileSpec.php b/spec/Gaufrette/FileSpec.php index 89cb79865..422de463b 100644 --- a/spec/Gaufrette/FileSpec.php +++ b/spec/Gaufrette/FileSpec.php @@ -173,6 +173,12 @@ function it_deletes_file_from_filesystem($filesystem) $filesystem->delete('filename')->shouldBeCalled()->willReturn(true); $this->delete()->shouldReturn(true); } + + function it_renames_file_from_filesystem($filesystem) + { + $filesystem->rename('filename', 'newname')->shouldBeCalled(); + $this->rename('newname'); + } } interface MetadataAdapter extends \Gaufrette\Adapter, diff --git a/src/Gaufrette/File.php b/src/Gaufrette/File.php index 4947c1939..615d76940 100644 --- a/src/Gaufrette/File.php +++ b/src/Gaufrette/File.php @@ -193,6 +193,18 @@ public function createStream() return $this->filesystem->createStream($this->key); } + /** + * Rename the file and move it to its new location. + * + * @param string $newKey + */ + public function rename($newKey) + { + $this->filesystem->rename($this->key, $newKey); + + $this->key = $newKey; + } + /** * Sets the metadata array to be stored in adapters that can support it. * diff --git a/src/Gaufrette/Filesystem.php b/src/Gaufrette/Filesystem.php index 3cbfb980d..a897e8f50 100644 --- a/src/Gaufrette/Filesystem.php +++ b/src/Gaufrette/Filesystem.php @@ -58,6 +58,8 @@ public function has($key) /** * Renames a file. * + * File::rename should be preferred or you may face bad filesystem consistency. + * * @param string $sourceKey * @param string $targetKey * @@ -67,6 +69,8 @@ public function has($key) * @throws Exception\UnexpectedFile when targetKey exists * @throws \RuntimeException when cannot rename * @throws \InvalidArgumentException If $sourceKey or $targetKey are invalid + * + * @see File::rename */ public function rename($sourceKey, $targetKey) { From 64a9991d00d9fc2d0241ff53f729c8c01151fd1a Mon Sep 17 00:00:00 2001 From: Nicolas MURE Date: Fri, 21 Apr 2017 18:01:23 +0200 Subject: [PATCH 253/426] deprecating adapters --- src/Gaufrette/Adapter/AclAwareAmazonS3.php | 4 ++++ src/Gaufrette/Adapter/AmazonS3.php | 4 ++++ src/Gaufrette/Adapter/Apc.php | 4 ++++ src/Gaufrette/Adapter/Cache.php | 4 ++++ src/Gaufrette/Adapter/Dropbox.php | 4 ++++ src/Gaufrette/Adapter/GoogleCloudStorage.php | 4 ++++ src/Gaufrette/Adapter/LazyOpenCloud.php | 4 ++++ src/Gaufrette/Adapter/MogileFS.php | 4 ++++ src/Gaufrette/Adapter/Sftp.php | 5 +++++ 9 files changed, 37 insertions(+) diff --git a/src/Gaufrette/Adapter/AclAwareAmazonS3.php b/src/Gaufrette/Adapter/AclAwareAmazonS3.php index 6f7ee361e..f307922b1 100644 --- a/src/Gaufrette/Adapter/AclAwareAmazonS3.php +++ b/src/Gaufrette/Adapter/AclAwareAmazonS3.php @@ -5,6 +5,8 @@ use AmazonS3 as AmazonClient; use Gaufrette\Adapter; +@trigger_error('The '.__NAMESPACE__.'\AclAwareAmazonS3 adapter is deprecated since version 0.4 and will be removed in 1.0. Use the AwsS3 adapter instead.', E_USER_DEPRECATED); + /** * Makes the AmazonS3 adapter ACL aware. Uses the AWS SDK for PHP v1.x. * @@ -13,6 +15,8 @@ * and regular adapter. * * @author Johannes M. Schmitt + * + * @deprecated The AclAwareAmazonS3 adapter is deprecated since version 0.4 and will be removed in 1.0. Use the AwsS3 adapter instead. */ class AclAwareAmazonS3 implements Adapter, MetadataSupporter diff --git a/src/Gaufrette/Adapter/AmazonS3.php b/src/Gaufrette/Adapter/AmazonS3.php index 401ce3732..e085c7f8d 100644 --- a/src/Gaufrette/Adapter/AmazonS3.php +++ b/src/Gaufrette/Adapter/AmazonS3.php @@ -5,6 +5,8 @@ use AmazonS3 as AmazonClient; use Gaufrette\Adapter; +@trigger_error('The '.__NAMESPACE__.'\AmazonS3 adapter is deprecated since version 0.4 and will be removed in 1.0. Use the AwsS3 adapter instead.', E_USER_DEPRECATED); + /** * Amazon S3 adapter using the AWS SDK for PHP v1.x. * @@ -12,6 +14,8 @@ * * @author Antoine Hérault * @author Leszek Prabucki + * + * @deprecated The AmazonS3 adapter is deprecated since version 0.4 and will be removed in 1.0. Use the AwsS3 adapter instead. */ class AmazonS3 implements Adapter, MetadataSupporter diff --git a/src/Gaufrette/Adapter/Apc.php b/src/Gaufrette/Adapter/Apc.php index f7271bf9e..6025dba21 100644 --- a/src/Gaufrette/Adapter/Apc.php +++ b/src/Gaufrette/Adapter/Apc.php @@ -5,12 +5,16 @@ use Gaufrette\Adapter; use Gaufrette\Util; +@trigger_error('The '.__NAMESPACE__.'\Apc adapter is deprecated since version 0.4 and will be removed in 1.0.', E_USER_DEPRECATED); + /** * Apc adapter, a non-persistent adapter for when this sort of thing is appropriate. * * @author Alexander Deruwe * @author Antoine Hérault * @author Leszek Prabucki + * + * @deprecated The Apc adapter is deprecated since version 0.4 and will be removed in 1.0. */ class Apc implements Adapter { diff --git a/src/Gaufrette/Adapter/Cache.php b/src/Gaufrette/Adapter/Cache.php index f54f73656..4dba02cb4 100644 --- a/src/Gaufrette/Adapter/Cache.php +++ b/src/Gaufrette/Adapter/Cache.php @@ -6,10 +6,14 @@ use Gaufrette\Adapter; use Gaufrette\Adapter\InMemory as InMemoryAdapter; +@trigger_error('The '.__NAMESPACE__.'\Cache adapter is deprecated since version 0.4 and will be removed in 1.0.', E_USER_DEPRECATED); + /** * Cache adapter. * * @author Antoine Hérault + * + * @deprecated The Cache adapter is deprecated since version 0.4 and will be removed in 1.0. */ class Cache implements Adapter, MetadataSupporter diff --git a/src/Gaufrette/Adapter/Dropbox.php b/src/Gaufrette/Adapter/Dropbox.php index 827f4fa1f..7379d75cd 100644 --- a/src/Gaufrette/Adapter/Dropbox.php +++ b/src/Gaufrette/Adapter/Dropbox.php @@ -8,12 +8,16 @@ use Dropbox_API as DropboxApi; use Dropbox_Exception_NotFound as DropboxNotFoundException; +@trigger_error('The '.__NAMESPACE__.'\Dropbox adapter is deprecated since version 0.4 and will be removed in 1.0.', E_USER_DEPRECATED); + /** * Dropbox adapter. * * @author Markus Bachmann * @author Antoine Hérault * @author Leszek Prabucki + * + * @deprecated The Dropbox adapter is deprecated since version 0.4 and will be removed in 1.0. */ class Dropbox implements Adapter { diff --git a/src/Gaufrette/Adapter/GoogleCloudStorage.php b/src/Gaufrette/Adapter/GoogleCloudStorage.php index 00ba518c9..0d7509347 100644 --- a/src/Gaufrette/Adapter/GoogleCloudStorage.php +++ b/src/Gaufrette/Adapter/GoogleCloudStorage.php @@ -5,10 +5,14 @@ use Gaufrette\Adapter; use GuzzleHttp; +@trigger_error('The '.__NAMESPACE__.'\GoogleCloudStorage adapter is deprecated since version 0.4 and will be removed in 1.0.', E_USER_DEPRECATED); + /** * Google Cloud Storage adapter using the Google APIs Client Library for PHP. * * @author Patrik Karisch + * + * @deprecated The GoogleCloudStorage adapter is deprecated since version 0.4 and will be removed in 1.0. */ class GoogleCloudStorage implements Adapter, MetadataSupporter, diff --git a/src/Gaufrette/Adapter/LazyOpenCloud.php b/src/Gaufrette/Adapter/LazyOpenCloud.php index b37c5a02c..190f790d6 100644 --- a/src/Gaufrette/Adapter/LazyOpenCloud.php +++ b/src/Gaufrette/Adapter/LazyOpenCloud.php @@ -4,10 +4,14 @@ use Gaufrette\Adapter\OpenStackCloudFiles\ObjectStoreFactoryInterface; +@trigger_error('The '.__NAMESPACE__.'\LazyOpenCloud adapter is deprecated since version 0.4 and will be removed in 1.0. Use the OpenCloud adapter instead.', E_USER_DEPRECATED); + /** * LazyOpenCloud. * * @author Daniel Richter + * + * @deprecated The LazyOpenCloud adapter is deprecated since version 0.4 and will be removed in 1.0. Use the OpenCloud adapter instead. */ class LazyOpenCloud extends OpenCloud { diff --git a/src/Gaufrette/Adapter/MogileFS.php b/src/Gaufrette/Adapter/MogileFS.php index f56278cbc..813ea7206 100644 --- a/src/Gaufrette/Adapter/MogileFS.php +++ b/src/Gaufrette/Adapter/MogileFS.php @@ -5,12 +5,16 @@ use Gaufrette\Adapter; use Gaufrette\Util; +@trigger_error('The '.__NAMESPACE__.'\MogileFS adapter is deprecated since version 0.4 and will be removed in 1.0.', E_USER_DEPRECATED); + /** * Adapter for the MogileFS filesystem. * * @author Mikko Tarvainen 2011 * @author Antoine Hérault * + * @deprecated The MogileFS adapter is deprecated since version 0.4 and will be removed in 1.0. + * * Bases partly on Wikimedia MogileFS client code by Jens Frank and Domas Mituzas, 2007. * See more: http://svn.wikimedia.org/viewvc/mediawiki/trunk/extensions/MogileClient/ */ diff --git a/src/Gaufrette/Adapter/Sftp.php b/src/Gaufrette/Adapter/Sftp.php index 669b8b4eb..bc40e447b 100644 --- a/src/Gaufrette/Adapter/Sftp.php +++ b/src/Gaufrette/Adapter/Sftp.php @@ -5,6 +5,11 @@ use Gaufrette\Adapter; use Ssh\Sftp as SftpClient; +@trigger_error('The '.__NAMESPACE__.'\Sftp adapter is deprecated since version 0.4 and will be removed in 1.0.', E_USER_DEPRECATED); + +/** + * @deprecated The Sftp adapter is deprecated since version 0.4 and will be removed in 1.0. + */ class Sftp implements Adapter, ChecksumCalculator { From 25625587c77d1914c09912e3b0a0f94325432eb1 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Wed, 26 Apr 2017 01:15:59 +0200 Subject: [PATCH 254/426] Add sftp service --- .env.dist | 6 ++++++ docker-compose.yml | 11 +++++++++++ docker/Dockerfile-sftp | 13 +++++++++++++ 3 files changed, 30 insertions(+) create mode 100644 docker/Dockerfile-sftp diff --git a/.env.dist b/.env.dist index c62e2c1f8..cc9a8ae3e 100644 --- a/.env.dist +++ b/.env.dist @@ -4,3 +4,9 @@ AWS_BUCKET= MONGO_URI=mongodb://mongodb:27017 MONGO_DBNAME=gridfs_test + +SFTP_HOST=sftp +SFTP_PORT=22 +SFTP_USER=gaufrette +SFTP_PASSWORD=gaufrette +SFTP_BASE_DIR=/home/gaufrette diff --git a/docker-compose.yml b/docker-compose.yml index 64f9500d2..631cb4fc3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -32,6 +32,7 @@ services: - './tests/:/usr/src/gaufrette/tests/' depends_on: - mongodb + - sftp env_file: .env php70: @@ -44,6 +45,7 @@ services: - './tests/:/usr/src/gaufrette/tests/' depends_on: - mongodb + - sftp env_file: .env php71: @@ -57,6 +59,7 @@ services: env_file: .env depends_on: - mongodb + - sftp hhvm: build: @@ -69,8 +72,16 @@ services: env_file: .env depends_on: - mongodb + - sftp mongodb: image: mongo ports: - "27017:27017" + + sftp: + build: + context: docker + dockerfile: Dockerfile-sftp + ports: + - "22:22" diff --git a/docker/Dockerfile-sftp b/docker/Dockerfile-sftp new file mode 100644 index 000000000..005e5b0b4 --- /dev/null +++ b/docker/Dockerfile-sftp @@ -0,0 +1,13 @@ +FROM ubuntu:precise + +RUN apt-get update && \ + apt-get install -y --no-install-recommends openssh-server && \ + rm -rf /var/lib/apt/lists/* + +RUN mkdir /var/run/sshd && \ + useradd -d /home/gaufrette gaufrette && \ + mkdir /home/gaufrette && \ + chown gaufrette:gaufrette /home/gaufrette && \ + echo "gaufrette:gaufrette" | chpasswd + +CMD ["/usr/sbin/sshd", "-D"] From 652ef2c4ae279b103edc42fe297bdb708480c54d Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Wed, 26 Apr 2017 01:23:52 +0200 Subject: [PATCH 255/426] Use env vars to trigger functional tests for PhpseclibSftp adapter --- phpunit.xml.dist | 7 ++++ .../Functional/Adapter/PhpseclibSftpTest.php | 37 +++++++++++++++++++ .../adapters/PhpseclibSftp.php.dist | 10 ----- 3 files changed, 44 insertions(+), 10 deletions(-) delete mode 100644 tests/Gaufrette/Functional/adapters/PhpseclibSftp.php.dist diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 4ac3fb545..3191035db 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -21,6 +21,13 @@ + + + diff --git a/tests/Gaufrette/Functional/Adapter/PhpseclibSftpTest.php b/tests/Gaufrette/Functional/Adapter/PhpseclibSftpTest.php index 60cfed698..35b427e97 100644 --- a/tests/Gaufrette/Functional/Adapter/PhpseclibSftpTest.php +++ b/tests/Gaufrette/Functional/Adapter/PhpseclibSftpTest.php @@ -3,7 +3,44 @@ namespace Gaufrette\Functional\Adapter; use Gaufrette\Adapter\PhpseclibSftp; +use Gaufrette\Filesystem; +use phpseclib\Net\SFTP; class PhpseclibSftpTest extends FunctionalTestCase { + /** @var SFTP */ + private $sftp; + + /** @var string */ + private $baseDir; + + public function setUp() + { + $host = getenv('SFTP_HOST'); + $port = getenv('SFTP_PORT') ?: 22; + $user = getenv('SFTP_USER'); + $password = getenv('SFTP_PASSWORD'); + $baseDir = getenv('SFTP_BASE_DIR'); + + if ($host === false || $user === false || $password === false || $baseDir === false) { + $this->markTestSkipped('Either SFTP_HOST, SFTP_USER, SFTP_PASSWORD and/or SFTP_BASE_DIR env variables are not defined.'); + } + + $this->baseDir = rtrim($baseDir, '/') . '/' . uniqid(); + + $this->sftp = new SFTP($host, $port); + $this->sftp->login($user, $password); + $this->sftp->mkdir($this->baseDir); + + $this->filesystem = new Filesystem(new PhpseclibSftp($this->sftp, $this->baseDir)); + } + + public function tearDown() + { + if (!isset($this->sftp)) { + return; + } + + $this->sftp->rmdir($this->baseDir); + } } diff --git a/tests/Gaufrette/Functional/adapters/PhpseclibSftp.php.dist b/tests/Gaufrette/Functional/adapters/PhpseclibSftp.php.dist deleted file mode 100644 index 4c4816b1e..000000000 --- a/tests/Gaufrette/Functional/adapters/PhpseclibSftp.php.dist +++ /dev/null @@ -1,10 +0,0 @@ -login('username', 'password'); - -$directory = '/some/directory'; -$sftp->delete($directory, true); -$adapter = new Gaufrette\Adapter\PhpseclibSftp($sftp, $directory, true); - -return $adapter; From c4522e90c4a3c9b6381d2592842c738fe64fd17f Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Wed, 26 Apr 2017 01:27:30 +0200 Subject: [PATCH 256/426] Run PhpseclibSftp tests on travis --- .travis.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.travis.yml b/.travis.yml index 97ecadc45..6b7c623f4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -21,6 +21,10 @@ services: env: - MONGO_URI="mongodb://127.0.0.1:27017" MONGO_DBNAME="gridfs_test" + SFTP_HOST=localhost + SFTP_USER=gaufrette + SFTP_PASSWORD=gaufrette + SFTP_BASE_DIR=/home/gaufrette matrix: allow_failures: @@ -32,6 +36,9 @@ before_script: - if [[ ${TRAVIS_PHP_VERSION:0:4} == "hhvm" ]]; then composer require mongofill/mongofill --ignore-platform-reqs; fi - sudo apt-get update - sudo apt-get install -y libssh2-1-dev + - sudo useradd -d /home/gaufrette gaufrette + - echo "gaufrette:gaufrette" | sudo chpasswd + - sudo mkdir /home/gaufrette && sudo chown gaufrette:gaufrette /home/gaufrette - if [[ ${TRAVIS_PHP_VERSION:0:4} != "hhvm" ]]; then yes | pecl install ssh2 ; fi - cp tests/Gaufrette/Functional/adapters/DoctrineDbal.php.dist tests/Gaufrette/Functional/adapters/DoctrineDbal.php -f - composer self-update && composer install --prefer-source --no-interaction From ef8a34d77efc8ecd7e223b7c3759dd7b6c0c253a Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Wed, 26 Apr 2017 03:46:49 +0200 Subject: [PATCH 257/426] Move IAM policy under Example section --- doc/adapters/aws-s3.md | 46 ++++++++++++++++++++++-------------------- 1 file changed, 24 insertions(+), 22 deletions(-) diff --git a/doc/adapters/aws-s3.md b/doc/adapters/aws-s3.md index b05bdc683..823e3e979 100644 --- a/doc/adapters/aws-s3.md +++ b/doc/adapters/aws-s3.md @@ -9,6 +9,30 @@ First, you will need to install AWS SDK for PHP: composer require aws/aws-sdk-php ``` +## Example + +```php + [ + 'key' => 'your_key_here', + 'secret' => 'your_secret', + ], + 'version' => 'latest', + 'region' => 'eu-west-1', +]); +$adapter = new AwsS3Adapter($s3client,'your-bucket-name'); +$filesystem = new Filesystem($adapter); +``` + +## IAM policy + In order to use this adapter you'll need an access key and a secret key. We **strongly recommend** you to create a dedicated IAM user with the most restrictive policy (in such case you can remove `s3:CreateBucket` action): @@ -44,25 +68,3 @@ We **strongly recommend** you to create a dedicated IAM user with the most restr You can even skip `s3:CreateBucket` role if you create your bucket first, which is also recommended for production environment. - -## Example - -```php - array( - 'key' => 'your_key_here', - 'secret' => 'your_secret', - ), - 'version' => 'latest', - 'region' => 'eu-west-1', -]); -$adapter = new AwsS3Adapter($s3client,'your-bucket-name'); -$filesystem = new Filesystem($adapter); -``` From 25f4b587615cd1574808fd9694dcf926e26bf12c Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Wed, 26 Apr 2017 03:47:04 +0200 Subject: [PATCH 258/426] Add an example for aws-sdk-php v2 --- doc/adapters/aws-s3.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/adapters/aws-s3.md b/doc/adapters/aws-s3.md index 823e3e979..c67ee6da4 100644 --- a/doc/adapters/aws-s3.md +++ b/doc/adapters/aws-s3.md @@ -27,6 +27,15 @@ $s3client = new S3Client([ 'version' => 'latest', 'region' => 'eu-west-1', ]); +// For aws-sdk-php v2 +$s3client = S3Client::factory([ + 'credentials' => [ + 'key' => 'your_key_here', + 'secret' => 'your_secret', + ], + 'version' => '2006-03-01', + 'region' => 'eu-west-1', +]); $adapter = new AwsS3Adapter($s3client,'your-bucket-name'); $filesystem = new Filesystem($adapter); ``` From e7ac96b097373dba34c98f03f943b23e7845a332 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Wed, 26 Apr 2017 03:53:54 +0200 Subject: [PATCH 259/426] Add two links to AWS docs about IAM and IAM policies --- doc/adapters/aws-s3.md | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/doc/adapters/aws-s3.md b/doc/adapters/aws-s3.md index c67ee6da4..4537b01c6 100644 --- a/doc/adapters/aws-s3.md +++ b/doc/adapters/aws-s3.md @@ -9,6 +9,8 @@ First, you will need to install AWS SDK for PHP: composer require aws/aws-sdk-php ``` +In order to use this adapter you'll need an access key and a secret key. + ## Example ```php @@ -41,10 +43,15 @@ $filesystem = new Filesystem($adapter); ``` ## IAM policy + +If you are not familiar with AWS, here are the key concepts: +* [IAM, stands for *Identity and Access Management*](http://docs.aws.amazon.com/IAM/latest/UserGuide/introduction.html) +* [IAM policies, are the way to grant access to your IAM users/groups](http://docs.aws.amazon.com/IAM/latest/UserGuide/introduction_access-management.html) -In order to use this adapter you'll need an access key and a secret key. -We **strongly recommend** you to create a dedicated IAM user with the most restrictive policy -(in such case you can remove `s3:CreateBucket` action): +We **strongly recommend** you to create a dedicated IAM user with the most restrictive policy. + +You can even skip `s3:CreateBucket` role if you manually create your bucket first, which is also recommended +for production environment. ```json { @@ -74,6 +81,3 @@ We **strongly recommend** you to create a dedicated IAM user with the most restr ] } ``` - -You can even skip `s3:CreateBucket` role if you create your bucket first, which is also recommended -for production environment. From c8489996cd322ab300062035739ed41cd0194e1e Mon Sep 17 00:00:00 2001 From: schevalier Date: Tue, 16 May 2017 16:57:38 +0200 Subject: [PATCH 260/426] Updated documentation for metapackages (resolves #478) --- doc/adapters/azure-blob-storage.md | 5 +++++ doc/adapters/doctrine-dbal.md | 5 ++--- doc/adapters/flysystem.md | 5 +++++ doc/adapters/ftp.md | 5 +++++ doc/adapters/grid-fs.md | 7 +++---- doc/adapters/in-memory.md | 5 +++++ doc/adapters/local.md | 5 +++++ doc/adapters/open-cloud.md | 5 +++++ doc/adapters/phpseclib-sftp.md | 7 ++----- doc/adapters/zip.md | 5 ++++- 10 files changed, 41 insertions(+), 13 deletions(-) diff --git a/doc/adapters/azure-blob-storage.md b/doc/adapters/azure-blob-storage.md index cbff2a36f..1e6734846 100644 --- a/doc/adapters/azure-blob-storage.md +++ b/doc/adapters/azure-blob-storage.md @@ -4,6 +4,11 @@ currentMenu: azure-blob-storage # AzureBlobStorage +First, you will need to install the adapter: +```bash +composer require gaufrette/azure-blob-storage-adapter +``` + Azure Blob Storage is the storage service provided by Microsoft Windows Azure cloud environment. To use this adapter you need to install the [Azure SDK for php](http://www.windowsazure.com/en-us/develop/php/common-tasks/download-php-sdk/) into your project. diff --git a/doc/adapters/doctrine-dbal.md b/doc/adapters/doctrine-dbal.md index f6c8774b8..e629ec6df 100644 --- a/doc/adapters/doctrine-dbal.md +++ b/doc/adapters/doctrine-dbal.md @@ -4,10 +4,9 @@ currentMenu: doctrine-dbal # Doctrine DBAL -If you aren't already using Doctrine, you should install DBAL through: - +First, you will need to install the adapter: ```bash -$ composer require doctrine/dbal +composer require gaufrette/doctrine-dbal-adapter ``` In order to use the adapter, you will need to prepare the table with the following columns: diff --git a/doc/adapters/flysystem.md b/doc/adapters/flysystem.md index d714e8570..23fec8bec 100644 --- a/doc/adapters/flysystem.md +++ b/doc/adapters/flysystem.md @@ -4,6 +4,11 @@ currentMenu: flysystem # Flysystem +First, you will need to install the adapter: +```bash +composer require gaufrette/flysystem-adapter +``` + Folks from [thephpleague](http://thephpleague.com/) have built extraordinary [Flysystem](https://github.com/thephpleague/flysystem) package which does exactly the same thing as Gaufrette, but with slightly different API. We wanted to make Gaufrette compatible with as many systems as possible, and didn't want to reinvent the wheel. diff --git a/doc/adapters/ftp.md b/doc/adapters/ftp.md index 129cb0683..767c979bb 100644 --- a/doc/adapters/ftp.md +++ b/doc/adapters/ftp.md @@ -4,6 +4,11 @@ currentMenu: ftp # FTP +First, you will need to install the adapter: +```bash +composer require gaufrette/ftp-adapter +``` + `ftp` extension should be enabled in order to use this adapter. Also, some FTP servers need valid configuration so Gaufrette can work with them as expected. diff --git a/doc/adapters/grid-fs.md b/doc/adapters/grid-fs.md index 1ba765d1b..68dae7d89 100644 --- a/doc/adapters/grid-fs.md +++ b/doc/adapters/grid-fs.md @@ -9,16 +9,15 @@ currentMenu: grid-fs In order to use GridFS adapter, you should have accesible MongoDB instance, [MongoDB PHP driver](http://docs.php.net/manual/en/book.mongodb.php) and the [`mongodb/mongodb`](https://docs.mongodb.com/php-library/master/) library installed. -You can install the MongoDB extension with +First can install the MongoDB extension with: ```bash pecl install mongodb ``` -And the PHP library with - +Then, install the adapter: ```bash -composer require mongodb/mongodb +composer require gaufrette/gridfs-adapter ``` ## Usage diff --git a/doc/adapters/in-memory.md b/doc/adapters/in-memory.md index 1468397c6..e36412984 100644 --- a/doc/adapters/in-memory.md +++ b/doc/adapters/in-memory.md @@ -6,6 +6,11 @@ currentMenu: in-memory This adapter is useful in test environments where you don't want to depend on external filesystems. +First, you will need to install the adapter: +```bash +composer require gaufrette/in-memory-adapter +``` + ## Example `InMemory` adapter only takes an array of available files as argument. diff --git a/doc/adapters/local.md b/doc/adapters/local.md index 0c4c939c6..af742e17d 100644 --- a/doc/adapters/local.md +++ b/doc/adapters/local.md @@ -6,6 +6,11 @@ currentMenu: local Those two adapters aims to use local filesystem. The second one will encode in base64 the filename before storing/retrieving. +First, you will need to install the adapter: +```bash +composer require gaufrette/local-adapter +``` + ## Example ```php diff --git a/doc/adapters/open-cloud.md b/doc/adapters/open-cloud.md index b95b9822e..f2791252e 100644 --- a/doc/adapters/open-cloud.md +++ b/doc/adapters/open-cloud.md @@ -4,6 +4,11 @@ currentMenu: open-cloud # OpenCloud & LazyOpenCloud +First, you will need to install the adapter: +```bash +composer require gaufrette/opencloud-adapter +``` + To use the OpenCloud adapter you will need to create a connection using the [OpenCloud SDK](https://github.com/rackspace/php-opencloud). You can then fetch the ObjectStore which is required for the OpenCloud adapter. diff --git a/doc/adapters/phpseclib-sftp.md b/doc/adapters/phpseclib-sftp.md index c08ed9116..f987018d5 100644 --- a/doc/adapters/phpseclib-sftp.md +++ b/doc/adapters/phpseclib-sftp.md @@ -8,12 +8,9 @@ currentMenu: phpseclib-sftp ## Prerequisites -* [phpseclib](https://github.com/phpseclib/phpseclib) - -You can install it via: - +First, you will need to install the adapter: ```bash -composer require phpseclib/phpseclib:^2.0 +composer require gaufrette/phpseclib-sftp-adapter ``` ## Configuration diff --git a/doc/adapters/zip.md b/doc/adapters/zip.md index 4d886cbec..af5e6c2a2 100644 --- a/doc/adapters/zip.md +++ b/doc/adapters/zip.md @@ -4,7 +4,10 @@ currentMenu: zip # ZIP -If you want to use this `Adapter`, first you should be sure `zip` extension is installed. +First, you will need to install the adapter: +```bash +composer require gaufrette/zip-adapter +``` ## Example From d009748c2f8d06e8cde38ed66213bda02105a95d Mon Sep 17 00:00:00 2001 From: Antoine Lelaisant Date: Thu, 13 Apr 2017 14:10:22 +0200 Subject: [PATCH 261/426] Replace file_exists with is_file. Fixes #473 --- spec/Gaufrette/Adapter/LocalSpec.php | 2 +- src/Gaufrette/Adapter/Local.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/spec/Gaufrette/Adapter/LocalSpec.php b/spec/Gaufrette/Adapter/LocalSpec.php index 77bc3705e..cf68ea5e8 100644 --- a/spec/Gaufrette/Adapter/LocalSpec.php +++ b/spec/Gaufrette/Adapter/LocalSpec.php @@ -156,6 +156,6 @@ function it_creates_directory_when_does_not_exists() { $this->beConstructedWith(vfsStream::url('test/other'), true); - $this->exists('/')->shouldReturn(true); + $this->isDirectory('/')->shouldReturn(true); } } diff --git a/src/Gaufrette/Adapter/Local.php b/src/Gaufrette/Adapter/Local.php index 1df50e2fd..e53f43abb 100644 --- a/src/Gaufrette/Adapter/Local.php +++ b/src/Gaufrette/Adapter/Local.php @@ -79,7 +79,7 @@ public function rename($sourceKey, $targetKey) */ public function exists($key) { - return file_exists($this->computePath($key)); + return is_file($this->computePath($key)); } /** From bf85dc4a06e8095319d5d8bfac6dc970e80a1469 Mon Sep 17 00:00:00 2001 From: Richard Fullmer Date: Thu, 8 Jan 2015 16:09:01 -0800 Subject: [PATCH 262/426] Allow Local adapter mkdir mode to cascade to it's Stream --- src/Gaufrette/Adapter/Local.php | 2 +- src/Gaufrette/Stream/Local.php | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Gaufrette/Adapter/Local.php b/src/Gaufrette/Adapter/Local.php index a6d8166f6..9dbb28713 100644 --- a/src/Gaufrette/Adapter/Local.php +++ b/src/Gaufrette/Adapter/Local.php @@ -145,7 +145,7 @@ public function isDirectory($key) */ public function createStream($key) { - return new Stream\Local($this->computePath($key)); + return new Stream\Local($this->computePath($key), $this->mode); } /** diff --git a/src/Gaufrette/Stream/Local.php b/src/Gaufrette/Stream/Local.php index 446f8786e..6b1c49283 100644 --- a/src/Gaufrette/Stream/Local.php +++ b/src/Gaufrette/Stream/Local.php @@ -15,13 +15,16 @@ class Local implements Stream private $path; private $mode; private $fileHandle; + private $mkdirMode; /** * @param string $path + * @param int $mkdirMode */ - public function __construct($path) + public function __construct($path, $mkdirMode = 0755) { $this->path = $path; + $this->mkdirMode = $mkdirMode; } /** @@ -31,7 +34,7 @@ public function open(StreamMode $mode) { $baseDirPath = \Gaufrette\Util\Path::dirname($this->path); if ($mode->allowsWrite() && !is_dir($baseDirPath)) { - @mkdir($baseDirPath, 0755, true); + @mkdir($baseDirPath, $this->mkdirMode, true); } try { $fileHandle = @fopen($this->path, $mode->getMode()); From c04e82edd917266d3312ee0a34a48578026c8b4d Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Tue, 18 Apr 2017 23:59:26 +0200 Subject: [PATCH 263/426] Remove port bound locally --- docker-compose.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 631cb4fc3..3ac206543 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -76,12 +76,8 @@ services: mongodb: image: mongo - ports: - - "27017:27017" sftp: build: context: docker dockerfile: Dockerfile-sftp - ports: - - "22:22" From 405d91644cac98ab08c0bd63423a92e3bb62a5cb Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Wed, 19 Apr 2017 00:01:21 +0200 Subject: [PATCH 264/426] Add ftp service, based on vsftpd server --- docker-compose.yml | 5 +++++ docker/Dockerfile-ftp | 17 +++++++++++++++++ docker/vsftpd.conf | 11 +++++++++++ 3 files changed, 33 insertions(+) create mode 100644 docker/Dockerfile-ftp create mode 100644 docker/vsftpd.conf diff --git a/docker-compose.yml b/docker-compose.yml index 3ac206543..e2ab1aeea 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -81,3 +81,8 @@ services: build: context: docker dockerfile: Dockerfile-sftp + + vsftpd: + build: + context: docker + dockerfile: Dockerfile-ftp diff --git a/docker/Dockerfile-ftp b/docker/Dockerfile-ftp new file mode 100644 index 000000000..96a57e877 --- /dev/null +++ b/docker/Dockerfile-ftp @@ -0,0 +1,17 @@ +FROM ubuntu:precise + +RUN apt-get update && \ + apt-get install -y --no-install-recommends vsftpd && \ + rm -rf /var/lib/apt/lists/* + +COPY vsftpd.conf /etc/vsftpd.conf + +# Removing "./" from homedir would break vsftpd config +RUN chown root:root /etc/vsftpd.conf && \ + useradd -d /home/./gaufrette gaufrette && \ + echo "gaufrette:gaufrette" | chpasswd && \ + mkdir /home/gaufrette /srv/empty && \ + chown gaufrette:gaufrette /home/gaufrette /srv/ftp + +EXPOSE 20-21 +CMD ["vsftpd"] diff --git a/docker/vsftpd.conf b/docker/vsftpd.conf new file mode 100644 index 000000000..c8d224c8d --- /dev/null +++ b/docker/vsftpd.conf @@ -0,0 +1,11 @@ +listen=YES +local_enable=YES +write_enable=YES +connect_from_port_20=YES +chroot_local_user=YES +secure_chroot_dir=/srv/empty +log_ftp_protocol=YES +xferlog_enable=YES +vsftpd_log_file=/var/log/vsftpd.log +passwd_chroot_enable=YES +local_root=/srv/ftp From 5248324d8d4fc3324918ae521394fdbced094741 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Wed, 19 Apr 2017 00:04:01 +0200 Subject: [PATCH 265/426] Execute functional tests for Ftp adapter when FTP_* env vars are declared --- .env.dist | 5 +++ phpunit.xml.dist | 7 +++- .../Gaufrette/Functional/Adapter/FtpTest.php | 37 +++++++++++++++++++ .../Functional/adapters/Ftp.php.dist | 16 -------- 4 files changed, 48 insertions(+), 17 deletions(-) delete mode 100644 tests/Gaufrette/Functional/adapters/Ftp.php.dist diff --git a/.env.dist b/.env.dist index cc9a8ae3e..5446efc57 100644 --- a/.env.dist +++ b/.env.dist @@ -10,3 +10,8 @@ SFTP_PORT=22 SFTP_USER=gaufrette SFTP_PASSWORD=gaufrette SFTP_BASE_DIR=/home/gaufrette + +FTP_HOST=vsftpd +FTP_USER=gaufrette +FTP_PASSWORD=gaufrette +FTP_BASE_DIR=/gaufrette/ diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 3191035db..f8ca08a45 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -26,7 +26,12 @@ + + + diff --git a/tests/Gaufrette/Functional/Adapter/FtpTest.php b/tests/Gaufrette/Functional/Adapter/FtpTest.php index 40ddf8d6e..408ca1922 100644 --- a/tests/Gaufrette/Functional/Adapter/FtpTest.php +++ b/tests/Gaufrette/Functional/Adapter/FtpTest.php @@ -3,7 +3,44 @@ namespace Gaufrette\Functional\Adapter; use Gaufrette\Adapter\Ftp; +use Gaufrette\Filesystem; class FtpTest extends FunctionalTestCase { + public function setUp() + { + $host = getenv('FTP_HOST'); + $user = getenv('FTP_USER'); + $password = getenv('FTP_PASSWORD'); + $baseDir = getenv('FTP_BASE_DIR'); + + if ($user === false || $password === false || $host === false || $baseDir === false) { + $this->markTestSkipped('Either FTP_HOST, FTP_USER, FTP_PASSWORD and/or FTP_BASE_DIR env variables are not defined.'); + } + + $adapter = new Ftp($baseDir, $host, ['username' => $user, 'password' => $password, 'passive' => true]); + $this->filesystem = new Filesystem($adapter); + } + + public function tearDown() + { + if (null === $this->filesystem) { + return; + } + + $adapter = $this->filesystem->getAdapter(); + + foreach ($adapter->keys() as $key) { + if (!$adapter->isDirectory($key)) { + $adapter->delete($key); + } + } + + $keys = $adapter->keys(); + rsort($keys); + foreach ($keys as $key) { + $adapter->delete($key); + } + + } } diff --git a/tests/Gaufrette/Functional/adapters/Ftp.php.dist b/tests/Gaufrette/Functional/adapters/Ftp.php.dist deleted file mode 100644 index 7d6fb4fa6..000000000 --- a/tests/Gaufrette/Functional/adapters/Ftp.php.dist +++ /dev/null @@ -1,16 +0,0 @@ -keys() as $key) { - if (!$adapter->isDirectory($key)) { - $adapter->delete($key); - } -} -$keys = $adapter->keys(); -rsort($keys); -foreach ($keys as $key) { - $adapter->delete($key); -} - -return $adapter; From 4ae3a79b617ac1e85d4a51aad475f625c4be4926 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Wed, 19 Apr 2017 02:45:41 +0200 Subject: [PATCH 266/426] Set FTP_USEPASVADDRESS option only on valid ftp connections --- src/Gaufrette/Adapter/Ftp.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index d24c70a2c..09b49a482 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -528,14 +528,14 @@ private function connect() $this->connection = ftp_ssl_connect($this->host, $this->port, $this->timeout); } - if (defined('FTP_USEPASVADDRESS')) { - ftp_set_option($this->connection, FTP_USEPASVADDRESS, false); - } - if (!$this->connection) { throw new \RuntimeException(sprintf('Could not connect to \'%s\' (port: %s).', $this->host, $this->port)); } + if (defined('FTP_USEPASVADDRESS')) { + ftp_set_option($this->connection, FTP_USEPASVADDRESS, false); + } + $username = $this->username ?: 'anonymous'; $password = $this->password ?: ''; From 9e4b2eef6016474432f2c1cf91560613be2db832 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Wed, 19 Apr 2017 01:34:26 +0200 Subject: [PATCH 267/426] Run functional tests for Ftp adapter on travis --- .travis.yml | 13 ++++++++++--- .travis/vsftpd.conf | 12 ++++++++++++ tests/Gaufrette/Functional/Adapter/FtpTest.php | 1 - 3 files changed, 22 insertions(+), 4 deletions(-) create mode 100644 .travis/vsftpd.conf diff --git a/.travis.yml b/.travis.yml index 6b7c623f4..a0404cbae 100644 --- a/.travis.yml +++ b/.travis.yml @@ -25,6 +25,10 @@ env: SFTP_USER=gaufrette SFTP_PASSWORD=gaufrette SFTP_BASE_DIR=/home/gaufrette + FTP_HOST=localhost + FTP_USER=gaufrette + FTP_PASSWORD=gaufrette + FTP_BASE_DIR=/gaufrette/ matrix: allow_failures: @@ -35,10 +39,13 @@ before_script: - if [[ ${TRAVIS_PHP_VERSION:0:1} == "5" ]]; then echo "extension = mongodb.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini && phpenv config-rm xdebug.ini; fi - if [[ ${TRAVIS_PHP_VERSION:0:4} == "hhvm" ]]; then composer require mongofill/mongofill --ignore-platform-reqs; fi - sudo apt-get update - - sudo apt-get install -y libssh2-1-dev - - sudo useradd -d /home/gaufrette gaufrette + - sudo apt-get install -y libssh2-1-dev vsftpd + - sudo useradd -d /home/./gaufrette gaufrette # Removing "./" from this path would break any further Travis builds - echo "gaufrette:gaufrette" | sudo chpasswd - - sudo mkdir /home/gaufrette && sudo chown gaufrette:gaufrette /home/gaufrette + - sudo mkdir /home/gaufrette /srv/empty + - sudo chown gaufrette:gaufrette /home/gaufrette /srv/ftp + - sudo cp .travis/vsftpd.conf /etc/vsftpd.conf + - sudo service vsftpd restart - if [[ ${TRAVIS_PHP_VERSION:0:4} != "hhvm" ]]; then yes | pecl install ssh2 ; fi - cp tests/Gaufrette/Functional/adapters/DoctrineDbal.php.dist tests/Gaufrette/Functional/adapters/DoctrineDbal.php -f - composer self-update && composer install --prefer-source --no-interaction diff --git a/.travis/vsftpd.conf b/.travis/vsftpd.conf new file mode 100644 index 000000000..002036831 --- /dev/null +++ b/.travis/vsftpd.conf @@ -0,0 +1,12 @@ +listen=YES +local_enable=YES +write_enable=YES +connect_from_port_20=YES +chroot_local_user=YES +secure_chroot_dir=/srv/empty +log_ftp_protocol=YES +xferlog_enable=YES +vsftpd_log_file=/var/log/vsftpd.log +passwd_chroot_enable=YES +local_root=/srv/ftp +background=YES diff --git a/tests/Gaufrette/Functional/Adapter/FtpTest.php b/tests/Gaufrette/Functional/Adapter/FtpTest.php index 408ca1922..8da9d7bfd 100644 --- a/tests/Gaufrette/Functional/Adapter/FtpTest.php +++ b/tests/Gaufrette/Functional/Adapter/FtpTest.php @@ -41,6 +41,5 @@ public function tearDown() foreach ($keys as $key) { $adapter->delete($key); } - } } From 27af667e6a6c342b8c8622e722c3420782b4b00b Mon Sep 17 00:00:00 2001 From: Vangest Olivier Date: Sat, 20 May 2017 09:55:16 +0200 Subject: [PATCH 268/426] Refactor Adapter Local --- src/Gaufrette/Adapter/Local.php | 85 +++++++++++++++++++++++++-------- 1 file changed, 66 insertions(+), 19 deletions(-) diff --git a/src/Gaufrette/Adapter/Local.php b/src/Gaufrette/Adapter/Local.php index a6d8166f6..198824e0d 100644 --- a/src/Gaufrette/Adapter/Local.php +++ b/src/Gaufrette/Adapter/Local.php @@ -14,10 +14,10 @@ * @author Leszek Prabucki */ class Local implements Adapter, - StreamFactory, - ChecksumCalculator, - SizeCalculator, - MimeTypeProvider + StreamFactory, + ChecksumCalculator, + SizeCalculator, + MimeTypeProvider { protected $directory; private $create; @@ -29,7 +29,7 @@ class Local implements Adapter, * exist (default FALSE) * @param int $mode Mode for mkdir * - * @throws RuntimeException if the specified directory does not exist and + * @throws \RuntimeException if the specified directory does not exist and * could not be created */ public function __construct($directory, $create = false, $mode = 0777) @@ -46,6 +46,10 @@ public function __construct($directory, $create = false, $mode = 0777) /** * {@inheritdoc} + * + * @throws \OutOfBoundsException If the computed path is out of the directory + * @throws \InvalidArgumentException if the directory already exists + * @throws \RuntimeException if the directory could not be created */ public function read($key) { @@ -54,6 +58,10 @@ public function read($key) /** * {@inheritdoc} + * + * @throws \OutOfBoundsException If the computed path is out of the directory + * @throws \InvalidArgumentException if the directory already exists + * @throws \RuntimeException if the directory could not be created */ public function write($key, $content) { @@ -65,6 +73,10 @@ public function write($key, $content) /** * {@inheritdoc} + * + * @throws \OutOfBoundsException If the computed path is out of the directory + * @throws \InvalidArgumentException if the directory already exists + * @throws \RuntimeException if the directory could not be created */ public function rename($sourceKey, $targetKey) { @@ -84,6 +96,10 @@ public function exists($key) /** * {@inheritdoc} + * + * @throws \OutOfBoundsException If the computed path is out of the directory + * @throws \InvalidArgumentException if the directory already exists + * @throws \RuntimeException if the directory could not be created */ public function keys() { @@ -101,7 +117,7 @@ public function keys() $files = new \EmptyIterator(); } - $keys = array(); + $keys = []; foreach ($files as $file) { $keys[] = $this->computeKey($file); } @@ -112,6 +128,10 @@ public function keys() /** * {@inheritdoc} + * + * @throws \OutOfBoundsException If the computed path is out of the directory + * @throws \InvalidArgumentException if the directory already exists + * @throws \RuntimeException if the directory could not be created */ public function mtime($key) { @@ -120,6 +140,10 @@ public function mtime($key) /** * {@inheritdoc} + * + * @throws \OutOfBoundsException If the computed path is out of the directory + * @throws \InvalidArgumentException if the directory already exists + * @throws \RuntimeException if the directory could not be created */ public function delete($key) { @@ -134,6 +158,10 @@ public function delete($key) * @param string $key * * @return bool + * + * @throws \OutOfBoundsException If the computed path is out of the directory + * @throws \InvalidArgumentException if the directory already exists + * @throws \RuntimeException if the directory could not be created */ public function isDirectory($key) { @@ -142,6 +170,10 @@ public function isDirectory($key) /** * {@inheritdoc} + * + * @throws \OutOfBoundsException If the computed path is out of the directory + * @throws \InvalidArgumentException if the directory already exists + * @throws \RuntimeException if the directory could not be created */ public function createStream($key) { @@ -150,6 +182,10 @@ public function createStream($key) /** * {@inheritdoc} + * + * @throws \OutOfBoundsException If the computed path is out of the directory + * @throws \InvalidArgumentException if the directory already exists + * @throws \RuntimeException if the directory could not be created */ public function checksum($key) { @@ -158,6 +194,10 @@ public function checksum($key) /** * {@inheritdoc} + * + * @throws \OutOfBoundsException If the computed path is out of the directory + * @throws \InvalidArgumentException if the directory already exists + * @throws \RuntimeException if the directory could not be created */ public function size($key) { @@ -166,6 +206,10 @@ public function size($key) /** * {@inheritdoc} + * + * @throws \OutOfBoundsException If the computed path is out of the directory + * @throws \InvalidArgumentException if the directory already exists + * @throws \RuntimeException if the directory could not be created */ public function mimeType($key) { @@ -177,9 +221,12 @@ public function mimeType($key) /** * Computes the key from the specified path. * - * @param string $path + * @param $path + * @return string * - * return string + * @throws \OutOfBoundsException If the computed path is out of the directory + * @throws \InvalidArgumentException if the directory already exists + * @throws \RuntimeException if the directory could not be created */ public function computeKey($path) { @@ -195,9 +242,10 @@ public function computeKey($path) * * @return string A path * - * @throws OutOfBoundsException If the computed path is out of the + * @throws \InvalidArgumentException if the directory already exists + * @throws \OutOfBoundsException If the computed path is out of the * directory - * @throws RuntimeException If directory does not exists and cannot be created + * @throws \RuntimeException If directory does not exists and cannot be created */ protected function computePath($key) { @@ -212,6 +260,8 @@ protected function computePath($key) * @param string $path * * @return string + * @throws \OutOfBoundsException If the computed path is out of the + * directory */ protected function normalizePath($path) { @@ -231,7 +281,8 @@ protected function normalizePath($path) * @param bool $create Whether to create the directory if it does * not exist * - * @throws RuntimeException if the directory does not exists and could not + * @throws \InvalidArgumentException if the directory already exists + * @throws \RuntimeException if the directory does not exists and could not * be created */ protected function ensureDirectoryExists($directory, $create = false) @@ -250,17 +301,13 @@ protected function ensureDirectoryExists($directory, $create = false) * * @param string $directory Path of the directory to create * - * @throws InvalidArgumentException if the directory already exists - * @throws RuntimeException if the directory could not be created + * @throws \InvalidArgumentException if the directory already exists + * @throws \RuntimeException if the directory could not be created */ protected function createDirectory($directory) { - $created = @mkdir($directory, $this->mode, true); - - if (!$created) { - if (!is_dir($directory)) { - throw new \RuntimeException(sprintf('The directory \'%s\' could not be created.', $directory)); - } + if (!@mkdir($directory, $this->mode, true) && !is_dir($directory)) { + throw new \RuntimeException(sprintf('The directory \'%s\' could not be created.', $directory)); } } } From 943f83a717946c67dbb095b63ea5cb7a2bf58cd7 Mon Sep 17 00:00:00 2001 From: Ka Date: Wed, 13 Jan 2016 12:22:44 +0100 Subject: [PATCH 269/426] Provide mime type through AwsS3 adapter Following by https://github.com/Headoo/Gaufrette/commit/87ac6fb066703b3d7cb2b2e683fdc16cda654f58 --- src/Gaufrette/Adapter/AwsS3.php | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index e7754a6a2..d2dc9e7f8 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -14,7 +14,8 @@ class AwsS3 implements Adapter, MetadataSupporter, ListKeysAware, - SizeCalculator + SizeCalculator, + MimeTypeProvider { /** @var S3Client */ protected $service; @@ -347,4 +348,14 @@ private function guessContentType($content) return $fileInfo->buffer($content); } + + public function mimeType($key) + { + try { + $result = $this->service->headObject($this->getOptions($key)); + return ($result['ContentType']); + } catch (\Exception $e) { + return false; + } + } } From 932d3d931b375d465a0bb751e4f77452364a43f2 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Sat, 20 May 2017 12:54:18 +0200 Subject: [PATCH 270/426] Fix tests case for listKeys --- tests/Gaufrette/Functional/Adapter/AwsS3Test.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php index 9b2b45fc0..6c5605f8a 100644 --- a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php +++ b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php @@ -136,11 +136,19 @@ public function testGetsObjectUrlsWithDirectory() ); } - public function shouldListKeysWithoutDirectory() + public function testListKeysWithoutDirectory() { - $filesystem = $this->getFilesystem(); + $filesystem = $this->getFilesystem(['create' => true]); + $filesystem->write('test.txt', 'some content'); + $keys = $filesystem->listKeys(); + $this->assertEquals(['test.txt'], $keys); + } + + public function testListKeysWithDirectory() + { + $filesystem = $this->getFilesystem(['create' => true, 'directory' => 'root/']); $filesystem->write('test.txt', 'some content'); $keys = $filesystem->listKeys(); - $this->assertEquals('test.txt', $keys['key']); + $this->assertEquals(['test.txt'], $keys); } } From 4dd666e4afc541092a3c720641c7d583b2ccfdf7 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Sat, 20 May 2017 12:16:20 +0200 Subject: [PATCH 271/426] Tests mime type support for AwsS3 adapter --- spec/Gaufrette/Adapter/AwsS3Spec.php | 6 ++++++ tests/Gaufrette/Functional/Adapter/AwsS3Test.php | 12 ++++++++++++ 2 files changed, 18 insertions(+) diff --git a/spec/Gaufrette/Adapter/AwsS3Spec.php b/spec/Gaufrette/Adapter/AwsS3Spec.php index b88fa92bc..2f716dba0 100644 --- a/spec/Gaufrette/Adapter/AwsS3Spec.php +++ b/spec/Gaufrette/Adapter/AwsS3Spec.php @@ -2,6 +2,7 @@ namespace spec\Gaufrette\Adapter; +use Gaufrette\Adapter\MimeTypeProvider; use PhpSpec\ObjectBehavior; use Prophecy\Argument; @@ -34,4 +35,9 @@ function it_supports_sizecalculator() { $this->shouldHaveType('Gaufrette\Adapter\SizeCalculator'); } + + function it_provides_mime_type() + { + $this->shouldHaveType(MimeTypeProvider::class); + } } diff --git a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php index 9b2b45fc0..91325d85c 100644 --- a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php +++ b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php @@ -143,4 +143,16 @@ public function shouldListKeysWithoutDirectory() $keys = $filesystem->listKeys(); $this->assertEquals('test.txt', $keys['key']); } + + public function testUploadWithGivenContentType() + { + $filesystem = $this->getFilesystem(['create' => true]); + /** @var AwsS3 $adapter */ + $adapter = $filesystem->getAdapter(); + + $adapter->setMetadata('foo', ['ContentType' => 'text/html']); + $filesystem->write('foo', ''); + + $this->assertEquals('text/html', $filesystem->mimeType('foo')); + } } From 22ff85aaa951fd02368b2ddf1f8b6e2f8a562d90 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Sun, 21 May 2017 23:00:19 +0200 Subject: [PATCH 272/426] Add cases for keys() method of AwsS3 adapter --- .../Functional/Adapter/AwsS3Test.php | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php index 6c5605f8a..075a0b925 100644 --- a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php +++ b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php @@ -140,15 +140,28 @@ public function testListKeysWithoutDirectory() { $filesystem = $this->getFilesystem(['create' => true]); $filesystem->write('test.txt', 'some content'); - $keys = $filesystem->listKeys(); - $this->assertEquals(['test.txt'], $keys); + $this->assertEquals(['test.txt'], $filesystem->listKeys()); } public function testListKeysWithDirectory() { $filesystem = $this->getFilesystem(['create' => true, 'directory' => 'root/']); $filesystem->write('test.txt', 'some content'); - $keys = $filesystem->listKeys(); - $this->assertEquals(['test.txt'], $keys); + $this->assertEquals(['test.txt'], $filesystem->listKeys()); + $this->assertTrue($filesystem->has('test.txt')); + } + + public function testKeysWithoutDirectory() + { + $filesystem = $this->getFilesystem(['create' => true]); + $filesystem->write('test.txt', 'some content'); + $this->assertEquals(['test.txt'], $filesystem->keys()); + } + + public function testKeysWithDirectory() + { + $filesystem = $this->getFilesystem(['create' => true, 'directory' => 'root/']); + $filesystem->write('test.txt', 'some content'); + $this->assertEquals(['test.txt'], $filesystem->keys()); } } From e14f89886551ec8f39c81132fcabbb4481760c85 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Mon, 22 May 2017 00:39:43 +0200 Subject: [PATCH 273/426] Add FilesystemInterface --- spec/Gaufrette/FilesystemSpec.php | 1 + src/Gaufrette/Filesystem.php | 133 +++---------------- src/Gaufrette/FilesystemInterface.php | 183 ++++++++++++++++++++++++++ 3 files changed, 200 insertions(+), 117 deletions(-) create mode 100644 src/Gaufrette/FilesystemInterface.php diff --git a/spec/Gaufrette/FilesystemSpec.php b/spec/Gaufrette/FilesystemSpec.php index 8751eed13..d76663e5e 100644 --- a/spec/Gaufrette/FilesystemSpec.php +++ b/spec/Gaufrette/FilesystemSpec.php @@ -18,6 +18,7 @@ function let($adapter) function it_is_initializable() { $this->shouldBeAnInstanceOf('Gaufrette\Filesystem'); + $this->shouldBeAnInstanceOf('Gaufrette\FilesystemInterface'); } /** diff --git a/src/Gaufrette/Filesystem.php b/src/Gaufrette/Filesystem.php index a897e8f50..04860f573 100644 --- a/src/Gaufrette/Filesystem.php +++ b/src/Gaufrette/Filesystem.php @@ -10,7 +10,7 @@ * @author Antoine Hérault * @author Leszek Prabucki */ -class Filesystem +class Filesystem implements FilesystemInterface { protected $adapter; @@ -40,13 +40,7 @@ public function getAdapter() } /** - * Indicates whether the file matching the specified key exists. - * - * @param string $key - * - * @return bool TRUE if the file exists, FALSE otherwise - * - * @throws \InvalidArgumentException If $key is invalid + * {@inheritdoc} */ public function has($key) { @@ -56,21 +50,7 @@ public function has($key) } /** - * Renames a file. - * - * File::rename should be preferred or you may face bad filesystem consistency. - * - * @param string $sourceKey - * @param string $targetKey - * - * @return bool TRUE if the rename was successful - * - * @throws Exception\FileNotFound when sourceKey does not exist - * @throws Exception\UnexpectedFile when targetKey exists - * @throws \RuntimeException when cannot rename - * @throws \InvalidArgumentException If $sourceKey or $targetKey are invalid - * - * @see File::rename + * {@inheritdoc} */ public function rename($sourceKey, $targetKey) { @@ -96,15 +76,7 @@ public function rename($sourceKey, $targetKey) } /** - * Returns the file matching the specified key. - * - * @param string $key Key of the file - * @param bool $create Whether to create the file if it does not exist - * - * @throws Exception\FileNotFound - * @throws \InvalidArgumentException If $key is invalid - * - * @return File + * {@inheritdoc} */ public function get($key, $create = false) { @@ -118,17 +90,7 @@ public function get($key, $create = false) } /** - * Writes the given content into the file. - * - * @param string $key Key of the file - * @param string $content Content to write in the file - * @param bool $overwrite Whether to overwrite the file if exists - * - * @throws Exception\FileAlreadyExists When file already exists and overwrite is false - * @throws \RuntimeException When for any reason content could not be written - * @throws \InvalidArgumentException If $key is invalid - * - * @return int The number of bytes that were written into the file + * {@inheritdoc} */ public function write($key, $content, $overwrite = false) { @@ -148,15 +110,7 @@ public function write($key, $content, $overwrite = false) } /** - * Reads the content from the file. - * - * @param string $key Key of the file - * - * @throws Exception\FileNotFound when file does not exist - * @throws \RuntimeException when cannot read file - * @throws \InvalidArgumentException If $key is invalid - * - * @return string + * {@inheritdoc} */ public function read($key) { @@ -174,14 +128,7 @@ public function read($key) } /** - * Deletes the file matching the specified key. - * - * @param string $key - * - * @throws \RuntimeException when cannot read file - * @throws \InvalidArgumentException If $key is invalid - * - * @return bool + * {@inheritdoc} */ public function delete($key) { @@ -199,9 +146,7 @@ public function delete($key) } /** - * Returns an array of all keys. - * - * @return array + * {@inheritdoc} */ public function keys() { @@ -209,15 +154,7 @@ public function keys() } /** - * Lists keys beginning with given prefix - * (no wildcard / regex matching). - * - * if adapter implements ListKeysAware interface, adapter's implementation will be used, - * in not, ALL keys will be requested and iterated through. - * - * @param string $prefix - * - * @return array + * {@inheritdoc} */ public function listKeys($prefix = '') { @@ -245,13 +182,7 @@ public function listKeys($prefix = '') } /** - * Returns the last modified time of the specified file. - * - * @param string $key - * - * @return int An UNIX like timestamp - * - * @throws \InvalidArgumentException If $key is invalid + * {@inheritdoc} */ public function mtime($key) { @@ -263,13 +194,7 @@ public function mtime($key) } /** - * Returns the checksum of the specified file's content. - * - * @param string $key - * - * @return string A MD5 hash - * - * @throws \InvalidArgumentException If $key is invalid + * {@inheritdoc} */ public function checksum($key) { @@ -285,13 +210,7 @@ public function checksum($key) } /** - * Returns the size of the specified file's content. - * - * @param string $key - * - * @return int File size in Bytes - * - * @throws \InvalidArgumentException If $key is invalid + * {@inheritdoc} */ public function size($key) { @@ -307,13 +226,7 @@ public function size($key) } /** - * Gets a new stream instance of the specified file. - * - * @param $key - * - * @return Stream|Stream\InMemoryBuffer - * - * @throws \InvalidArgumentException If $key is invalid + * {@inheritdoc} */ public function createStream($key) { @@ -327,13 +240,7 @@ public function createStream($key) } /** - * Creates a new file in a filesystem. - * - * @param $key - * - * @return File - * - * @throws \InvalidArgumentException If $key is invalid + * {@inheritdoc} */ public function createFile($key) { @@ -351,13 +258,7 @@ public function createFile($key) } /** - * Get the mime type of the provided key. - * - * @param string $key - * - * @return string - * - * @throws \InvalidArgumentException If $key is invalid + * {@inheritdoc} */ public function mimeType($key) { @@ -425,9 +326,7 @@ public function removeFromRegister($key) } /** - * @param string $key - * - * @return bool + * {@inheritdoc} */ public function isDirectory($key) { diff --git a/src/Gaufrette/FilesystemInterface.php b/src/Gaufrette/FilesystemInterface.php new file mode 100644 index 000000000..f4bb0f443 --- /dev/null +++ b/src/Gaufrette/FilesystemInterface.php @@ -0,0 +1,183 @@ + Date: Mon, 22 May 2017 00:40:34 +0200 Subject: [PATCH 274/426] Add vsftpd as a dependency of php containers --- docker-compose.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index e2ab1aeea..8b53ed7c2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -33,6 +33,7 @@ services: depends_on: - mongodb - sftp + - vsftpd env_file: .env php70: @@ -46,6 +47,7 @@ services: depends_on: - mongodb - sftp + - vsftpd env_file: .env php71: @@ -60,6 +62,7 @@ services: depends_on: - mongodb - sftp + - vsftpd hhvm: build: @@ -73,6 +76,7 @@ services: depends_on: - mongodb - sftp + - vsftpd mongodb: image: mongo From 6ec9661464cadf2f676e698286ec18fd07646514 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Fri, 19 May 2017 01:19:31 +0200 Subject: [PATCH 275/426] Use common logic to delete local dirs during tear down --- .../Functional/Adapter/LocalTest.php | 19 ++---------- .../Functional/FileStream/LocalTest.php | 8 ++--- .../Functional/LocalDirectoryDeletor.php | 31 +++++++++++++++++++ 3 files changed, 35 insertions(+), 23 deletions(-) create mode 100644 tests/Gaufrette/Functional/LocalDirectoryDeletor.php diff --git a/tests/Gaufrette/Functional/Adapter/LocalTest.php b/tests/Gaufrette/Functional/Adapter/LocalTest.php index a5732cb7c..0730550c9 100644 --- a/tests/Gaufrette/Functional/Adapter/LocalTest.php +++ b/tests/Gaufrette/Functional/Adapter/LocalTest.php @@ -4,6 +4,7 @@ use Gaufrette\Filesystem; use Gaufrette\Adapter\Local; +use Gaufrette\Functional\LocalDirectoryDeletor; class LocalTest extends FunctionalTestCase { @@ -24,23 +25,7 @@ public function tearDown() { $this->filesystem = null; - if (file_exists($this->directory)) { - $iterator = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator( - $this->directory, - \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS - ), - \RecursiveIteratorIterator::CHILD_FIRST - ); - - foreach ($iterator as $item) { - if ($item->isDir()) { - rmdir(strval($item)); - } else { - unlink(strval($item)); - } - } - } + LocalDirectoryDeletor::deleteDirectory($this->directory); } /** diff --git a/tests/Gaufrette/Functional/FileStream/LocalTest.php b/tests/Gaufrette/Functional/FileStream/LocalTest.php index 73734d75b..7df5ceb9c 100644 --- a/tests/Gaufrette/Functional/FileStream/LocalTest.php +++ b/tests/Gaufrette/Functional/FileStream/LocalTest.php @@ -4,6 +4,7 @@ use Gaufrette\Filesystem; use Gaufrette\Adapter\Local as LocalAdapter; +use Gaufrette\Functional\LocalDirectoryDeletor; class LocalTest extends FunctionalTestCase { @@ -20,12 +21,7 @@ public function setUp() public function tearDown() { - if (is_file($file = $this->directory.DIRECTORY_SEPARATOR.'test.txt')) { - @unlink($file); - } - if (is_dir($this->directory)) { - @rmdir($this->directory); - } + LocalDirectoryDeletor::deleteDirectory($this->directory); } /** diff --git a/tests/Gaufrette/Functional/LocalDirectoryDeletor.php b/tests/Gaufrette/Functional/LocalDirectoryDeletor.php new file mode 100644 index 000000000..5974bbe71 --- /dev/null +++ b/tests/Gaufrette/Functional/LocalDirectoryDeletor.php @@ -0,0 +1,31 @@ +isDir()) { + rmdir(strval($item)); + } else { + unlink(strval($item)); + } + } + } + } +} From d3aeb05fb11892370ee8794490420008cd851770 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Fri, 19 May 2017 01:20:24 +0200 Subject: [PATCH 276/426] Test chmod of directory created by LocalStream --- tests/Gaufrette/Functional/FileStream/LocalTest.php | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/tests/Gaufrette/Functional/FileStream/LocalTest.php b/tests/Gaufrette/Functional/FileStream/LocalTest.php index 7df5ceb9c..c353ffe6a 100644 --- a/tests/Gaufrette/Functional/FileStream/LocalTest.php +++ b/tests/Gaufrette/Functional/FileStream/LocalTest.php @@ -14,11 +14,20 @@ public function setUp() { $this->directory = __DIR__.DIRECTORY_SEPARATOR.'filesystem'; @mkdir($this->directory.DIRECTORY_SEPARATOR.'subdir', 0777, true); - $this->filesystem = new Filesystem(new LocalAdapter($this->directory, true)); + $this->filesystem = new Filesystem(new LocalAdapter($this->directory, true, 0770)); $this->registerLocalFilesystemInStream(); } + public function testDirectoryChmod() + { + $r = fopen('gaufrette://filestream/foo/bar', 'a+'); + fclose($r); + + $perms = fileperms($this->directory . '/foo/'); + $this->assertEquals('0770', substr(sprintf('%o', $perms), -4)); + } + public function tearDown() { LocalDirectoryDeletor::deleteDirectory($this->directory); From 3b6729ffb6104c153ffa326a8851ffdc9fb1d95c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dalibor=20Karlovi=C4=87?= Date: Sat, 29 Apr 2017 14:12:19 +0200 Subject: [PATCH 277/426] feature(AzureBlobStorage) add multi-container mode support Multi-container mode allows for using auto-created container in scenarios where storage is dedicated to a single asset type, but allows more flexibilty (ie. using container-level access control). --- doc/adapters/azure-blob-storage.md | 23 ++ phpunit.xml.dist | 5 + src/Gaufrette/Adapter/AzureBlobStorage.php | 223 ++++++++++++++---- .../Adapter/AzureBlobStorageTest.php | 23 +- .../AzureMultiContainerBlobStorageTest.php | 195 +++++++++++++++ 5 files changed, 418 insertions(+), 51 deletions(-) create mode 100644 tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php diff --git a/doc/adapters/azure-blob-storage.md b/doc/adapters/azure-blob-storage.md index 1e6734846..cd53e0ee7 100644 --- a/doc/adapters/azure-blob-storage.md +++ b/doc/adapters/azure-blob-storage.md @@ -24,6 +24,14 @@ You should be able to find your **endpoint**, **account name** and **account key Thanks to the blob proxy factory, the adapter lazy loads the connection to the endpoint, so it will not create any connection until it's really needed (eg. when a read or write operation is issued). +## Multi-container mode + +If you specify a container name, adapter will use only that container for all blobs. + +If you omit specifying a container, it will use a so-called multi-container mode in which container name is determined +directly from key. This allows for more flexibility if you're using dedicated storage accounts per asset type +(ie. one for images, one for videos) as you get to group assets logically, use container-level privileges, etc. + ## Example ```php @@ -31,6 +39,21 @@ connection until it's really needed (eg. when a read or write operation is issue $connectionString = '...'; $factory = new Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactory($connectionString); + +// single-container mode $adapter = new Gaufrette\Adapter\AzureBlobStorage($factory, 'my-container'); $filesystem = new Gaufrette\Filesystem($adapter); +// container=my-container, path=my/stuff.txt +$filesystem->write('my/stuff.txt', 'This is my stuff'); + +// multi-container mode +$adapter = new Gaufrette\Adapter\AzureBlobStorage($factory); +// make auto-created containers public by default +$containerOptions = new MicrosoftAzure\Storage\Blob\Models\CreateContainerOptions; +$containerOptions->setPublicAccess(true); +$adapter->setCreateContainerOptions($containerOptions); +$filesystem = new Gaufrette\Filesystem($adapter); +// container=my (auto-created), path=stuff.txt +$filesystem->write('my/stuff.txt', 'This is my stuff'); + ``` diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f8ca08a45..d3101efee 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -33,6 +33,11 @@ --> + + + diff --git a/src/Gaufrette/Adapter/AzureBlobStorage.php b/src/Gaufrette/Adapter/AzureBlobStorage.php index 4a7955371..9a43291aa 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage.php @@ -5,11 +5,13 @@ use Gaufrette\Adapter; use Gaufrette\Util; use Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface; +use MicrosoftAzure\Storage\Blob\Models\Blob; +use MicrosoftAzure\Storage\Blob\Models\Container; use MicrosoftAzure\Storage\Blob\Models\CreateBlobOptions; use MicrosoftAzure\Storage\Blob\Models\CreateContainerOptions; use MicrosoftAzure\Storage\Blob\Models\DeleteContainerOptions; use MicrosoftAzure\Storage\Blob\Models\ListBlobsOptions; -use MicrosoftAzure\Storage\Common\ServiceException; +use MicrosoftAzure\Storage\Common\Exceptions\ServiceException; /** * Microsoft Azure Blob Storage adapter. @@ -46,22 +48,52 @@ class AzureBlobStorage implements Adapter, */ protected $blobProxy; + /** + * @var bool + */ + protected $multiContainerMode = false; + + /** + * @var CreateContainerOptions + */ + protected $createContainerOptions; + /** * @param AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory - * @param string $containerName + * @param string|null $containerName * @param bool $create * @param bool $detectContentType + * + * @throws \RuntimeException */ - public function __construct(BlobProxyFactoryInterface $blobProxyFactory, $containerName, $create = false, $detectContentType = true) + public function __construct(BlobProxyFactoryInterface $blobProxyFactory, $containerName = null, $create = false, $detectContentType = true) { $this->blobProxyFactory = $blobProxyFactory; $this->containerName = $containerName; $this->detectContentType = $detectContentType; - if ($create) { + if (null === $containerName) { + $this->multiContainerMode = true; + } elseif ($create) { $this->createContainer($containerName); } } + /** + * @return CreateContainerOptions + */ + public function getCreateContainerOptions() + { + return $this->createContainerOptions; + } + + /** + * @param CreateContainerOptions $options + */ + public function setCreateContainerOptions(CreateContainerOptions $options) + { + $this->createContainerOptions = $options; + } + /** * Creates a new container. * @@ -74,12 +106,16 @@ public function createContainer($containerName, CreateContainerOptions $options { $this->init(); + if (null === $options) { + $options = $this->getCreateContainerOptions(); + } + try { $this->blobProxy->createContainer($containerName, $options); } catch (ServiceException $e) { $errorCode = $this->getErrorCodeFromServiceException($e); - if ($errorCode != self::ERROR_CONTAINER_ALREADY_EXISTS) { + if ($errorCode !== self::ERROR_CONTAINER_ALREADY_EXISTS) { throw new \RuntimeException(sprintf( 'Failed to create the configured container "%s": %s (%s).', $containerName, @@ -107,7 +143,7 @@ public function deleteContainer($containerName, DeleteContainerOptions $options } catch (ServiceException $e) { $errorCode = $this->getErrorCodeFromServiceException($e); - if ($errorCode != self::ERROR_CONTAINER_NOT_FOUND) { + if ($errorCode !== self::ERROR_CONTAINER_NOT_FOUND) { throw new \RuntimeException(sprintf( 'Failed to delete the configured container "%s": %s (%s).', $containerName, @@ -120,17 +156,20 @@ public function deleteContainer($containerName, DeleteContainerOptions $options /** * {@inheritdoc} + * @throws \RuntimeException + * @throws \InvalidArgumentException */ public function read($key) { $this->init(); + list($containerName, $key) = $this->tokenizeKey($key); try { - $blob = $this->blobProxy->getBlob($this->containerName, $key); + $blob = $this->blobProxy->getBlob($containerName, $key); return stream_get_contents($blob->getContentStream()); } catch (ServiceException $e) { - $this->failIfContainerNotFound($e, sprintf('read key "%s"', $key)); + $this->failIfContainerNotFound($e, sprintf('read key "%s"', $key), $containerName); return false; } @@ -138,46 +177,55 @@ public function read($key) /** * {@inheritdoc} + * @throws \RuntimeException + * @throws \InvalidArgumentException */ public function write($key, $content) { $this->init(); + list($containerName, $key) = $this->tokenizeKey($key); - try { - $options = new CreateBlobOptions(); - - if ($this->detectContentType) { - $contentType = $this->guessContentType($content); + $options = new CreateBlobOptions(); - $options->setBlobContentType($contentType); - } + if ($this->detectContentType) { + $contentType = $this->guessContentType($content); - $this->blobProxy->createBlockBlob($this->containerName, $key, $content, $options); + $options->setContentType($contentType); + } - if (is_resource($content)) { - return Util\Size::fromResource($content); + try { + if ($this->multiContainerMode) { + $this->createContainer($containerName); } - return Util\Size::fromContent($content); + $this->blobProxy->createBlockBlob($containerName, $key, $content, $options); } catch (ServiceException $e) { - $this->failIfContainerNotFound($e, sprintf('write content for key "%s"', $key)); + $this->failIfContainerNotFound($e, sprintf('write content for key "%s"', $key), $containerName); return false; } + if (is_resource($content)) { + return Util\Size::fromResource($content); + } + + return Util\Size::fromContent($content); } /** * {@inheritdoc} + * @throws \RuntimeException + * @throws \InvalidArgumentException */ public function exists($key) { $this->init(); + list($containerName, $key) = $this->tokenizeKey($key); $listBlobsOptions = new ListBlobsOptions(); $listBlobsOptions->setPrefix($key); try { - $blobsList = $this->blobProxy->listBlobs($this->containerName, $listBlobsOptions); + $blobsList = $this->blobProxy->listBlobs($containerName, $listBlobsOptions); foreach ($blobsList->getBlobs() as $blob) { if ($key === $blob->getName()) { @@ -185,13 +233,16 @@ public function exists($key) } } } catch (ServiceException $e) { - $this->failIfContainerNotFound($e, 'check if key exists'); $errorCode = $this->getErrorCodeFromServiceException($e); + if ($this->multiContainerMode && self::ERROR_CONTAINER_NOT_FOUND === $errorCode) { + return false; + } + $this->failIfContainerNotFound($e, 'check if key exists', $containerName); throw new \RuntimeException(sprintf( 'Failed to check if key "%s" exists in container "%s": %s (%s).', $key, - $this->containerName, + $containerName, $e->getErrorText(), $errorCode ), $e->getCode()); @@ -202,22 +253,27 @@ public function exists($key) /** * {@inheritdoc} + * @throws \RuntimeException */ public function keys() { $this->init(); try { - $blobList = $this->blobProxy->listBlobs($this->containerName); - - return array_map( - function ($blob) { - return $blob->getName(); - }, - $blobList->getBlobs() - ); + if ($this->multiContainerMode) { + $containersList = $this->blobProxy->listContainers(); + return call_user_func_array('array_merge', array_map( + function(Container $container) { + $containerName = $container->getName(); + return $this->fetchBlobs($containerName, $containerName); + }, + $containersList->getContainers() + )); + } + + return $this->fetchBlobs($this->containerName); } catch (ServiceException $e) { - $this->failIfContainerNotFound($e, 'retrieve keys'); + $this->failIfContainerNotFound($e, 'retrieve keys', $this->containerName); $errorCode = $this->getErrorCodeFromServiceException($e); throw new \RuntimeException(sprintf( @@ -231,17 +287,20 @@ function ($blob) { /** * {@inheritdoc} + * @throws \RuntimeException + * @throws \InvalidArgumentException */ public function mtime($key) { $this->init(); + list($containerName, $key) = $this->tokenizeKey($key); try { - $properties = $this->blobProxy->getBlobProperties($this->containerName, $key); + $properties = $this->blobProxy->getBlobProperties($containerName, $key); return $properties->getProperties()->getLastModified()->getTimestamp(); } catch (ServiceException $e) { - $this->failIfContainerNotFound($e, sprintf('read mtime for key "%s"', $key)); + $this->failIfContainerNotFound($e, sprintf('read mtime for key "%s"', $key), $containerName); return false; } @@ -249,17 +308,20 @@ public function mtime($key) /** * {@inheritdoc} + * @throws \RuntimeException + * @throws \InvalidArgumentException */ public function delete($key) { $this->init(); + list($containerName, $key) = $this->tokenizeKey($key); try { - $this->blobProxy->deleteBlob($this->containerName, $key); + $this->blobProxy->deleteBlob($containerName, $key); return true; } catch (ServiceException $e) { - $this->failIfContainerNotFound($e, sprintf('delete key "%s"', $key)); + $this->failIfContainerNotFound($e, sprintf('delete key "%s"', $key), $containerName); return false; } @@ -267,18 +329,26 @@ public function delete($key) /** * {@inheritdoc} + * @throws \RuntimeException + * @throws \InvalidArgumentException */ public function rename($sourceKey, $targetKey) { $this->init(); + list($sourceContainerName, $sourceKey) = $this->tokenizeKey($sourceKey); + list($targetContainerName, $targetKey) = $this->tokenizeKey($targetKey); + try { - $this->blobProxy->copyBlob($this->containerName, $targetKey, $this->containerName, $sourceKey); - $this->blobProxy->deleteBlob($this->containerName, $sourceKey); + if ($this->multiContainerMode) { + $this->createContainer($targetContainerName); + } + $this->blobProxy->copyBlob($targetContainerName, $targetKey, $sourceContainerName, $sourceKey); + $this->blobProxy->deleteBlob($sourceContainerName, $sourceKey); return true; } catch (ServiceException $e) { - $this->failIfContainerNotFound($e, sprintf('rename key "%s"', $sourceKey)); + $this->failIfContainerNotFound($e, sprintf('rename key "%s"', $sourceKey), $sourceContainerName); return false; } @@ -295,20 +365,23 @@ public function isDirectory($key) /** * {@inheritdoc} + * @throws \RuntimeException + * @throws \InvalidArgumentException */ public function setMetadata($key, $content) { $this->init(); + list($containerName, $key) = $this->tokenizeKey($key); try { - $this->blobProxy->setBlobMetadata($this->containerName, $key, $content); + $this->blobProxy->setBlobMetadata($containerName, $key, $content); } catch (ServiceException $e) { $errorCode = $this->getErrorCodeFromServiceException($e); throw new \RuntimeException(sprintf( 'Failed to set metadata for blob "%s" in container "%s": %s (%s).', $key, - $this->containerName, + $containerName, $e->getErrorText(), $errorCode ), $e->getCode()); @@ -317,13 +390,16 @@ public function setMetadata($key, $content) /** * {@inheritdoc} + * @throws \RuntimeException + * @throws \InvalidArgumentException */ public function getMetadata($key) { $this->init(); + list($containerName, $key) = $this->tokenizeKey($key); try { - $properties = $this->blobProxy->getBlobProperties($this->containerName, $key); + $properties = $this->blobProxy->getBlobProperties($containerName, $key); return $properties->getMetadata(); } catch (ServiceException $e) { @@ -332,7 +408,7 @@ public function getMetadata($key) throw new \RuntimeException(sprintf( 'Failed to get metadata for blob "%s" in container "%s": %s (%s).', $key, - $this->containerName, + $containerName, $e->getErrorText(), $errorCode ), $e->getCode()); @@ -344,7 +420,7 @@ public function getMetadata($key) */ protected function init() { - if ($this->blobProxy == null) { + if ($this->blobProxy === null) { $this->blobProxy = $this->blobProxyFactory->create(); } } @@ -354,18 +430,19 @@ protected function init() * * @param ServiceException $exception * @param string $action + * @param string $containerName * * @throws \RuntimeException */ - protected function failIfContainerNotFound(ServiceException $exception, $action) + protected function failIfContainerNotFound(ServiceException $exception, $action, $containerName) { $errorCode = $this->getErrorCodeFromServiceException($exception); - if ($errorCode == self::ERROR_CONTAINER_NOT_FOUND) { + if ($errorCode === self::ERROR_CONTAINER_NOT_FOUND) { throw new \RuntimeException(sprintf( 'Failed to %s: container "%s" not found.', $action, - $this->containerName + $containerName ), $exception->getCode()); } } @@ -379,17 +456,17 @@ protected function failIfContainerNotFound(ServiceException $exception, $action) */ protected function getErrorCodeFromServiceException(ServiceException $exception) { - $xml = @simplexml_load_string($exception->getErrorReason()); + $xml = @simplexml_load_string($exception->getResponse()->getBody()); if ($xml && isset($xml->Code)) { return (string) $xml->Code; } - return $exception->getErrorReason(); + return $exception->getErrorText(); } /** - * @param string $content + * @param string|resource $content * * @return string */ @@ -403,4 +480,50 @@ private function guessContentType($content) return $fileInfo->buffer($content); } + + /** + * @param string $key + * + * @return array + * @throws \InvalidArgumentException + */ + private function tokenizeKey($key) + { + $containerName = $this->containerName; + if (false === $this->multiContainerMode) { + return [$containerName, $key]; + } + + if (false === ($index = strpos($key, '/'))) { + throw new \InvalidArgumentException(sprintf( + 'Failed to establish container name from key "%s", container name is required in multi-container mode', + $key + )); + } + $containerName = substr($key, 0, $index); + $key = substr($key, $index + 1); + + return [$containerName, $key]; + } + + /** + * @param string $containerName + * @param null $prefix + * + * @return array + */ + private function fetchBlobs($containerName, $prefix = null) + { + $blobList = $this->blobProxy->listBlobs($containerName); + return array_map( + function (Blob $blob) use ($prefix) { + $name = $blob->getName(); + if (null !== $prefix) { + $name = $prefix .'/'. $name; + } + return $name; + }, + $blobList->getBlobs() + ); + } } diff --git a/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php b/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php index c6afa3276..a62f88cf9 100644 --- a/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php +++ b/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php @@ -2,6 +2,27 @@ namespace Gaufrette\Functional\Adapter; +use Gaufrette\Adapter\AzureBlobStorage; +use Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactory; +use Gaufrette\Filesystem; + +/** + * Class AzureBlobStorageTest + * @group AzureBlobStorage + */ class AzureBlobStorageTest extends FunctionalTestCase { -} \ No newline at end of file + public function setUp() + { + $key = getenv('AZURE_KEY'); + $secret = getenv('AZURE_SECRET'); + $containerName = getenv('AZURE_CONTAINER'); + if (empty($key) || empty($secret) || empty($containerName)) { + $this->markTestSkipped(); + } + + $connection = sprintf('BlobEndpoint=http://%1$s.blob.core.windows.net/;AccountName=%1$s;AccountKey=%2$s', $key, $secret); + + $this->filesystem = new Filesystem(new AzureBlobStorage(new BlobProxyFactory($connection), $containerName, true)); + } +} diff --git a/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php new file mode 100644 index 000000000..e60a09de7 --- /dev/null +++ b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php @@ -0,0 +1,195 @@ +markTestSkipped(); + } + + $connection = sprintf('BlobEndpoint=http://%1$s.blob.core.windows.net/;AccountName=%1$s;AccountKey=%2$s', $key, $secret); + + $this->filesystem = new Filesystem(new AzureBlobStorage(new BlobProxyFactory($connection))); + } + + /** + * @test + * @group functional + */ + public function shouldWriteAndRead() + { + $this->assertEquals(12, $this->filesystem->write('container1/foo', 'Some content')); + $this->assertEquals(13, $this->filesystem->write('test/subdir/foo', 'Some content1', true)); + + $this->assertEquals('Some content', $this->filesystem->read('container1/foo')); + $this->assertEquals('Some content1', $this->filesystem->read('test/subdir/foo')); + $this->filesystem->delete('container1/foo'); + $this->filesystem->delete('test/subdir/foo'); + } + + /** + * @test + * @group functional + */ + public function shouldUpdateFileContent() + { + $this->filesystem->write('container2/foo', 'Some content'); + $this->filesystem->write('container2/foo', 'Some content updated', true); + + $this->assertEquals('Some content updated', $this->filesystem->read('container2/foo')); + $this->filesystem->delete('container2/foo'); + } + + /** + * @test + * @group functional + */ + public function shouldCheckIfFileExists() + { + $this->assertFalse($this->filesystem->has('container3/foo')); + + $this->filesystem->write('container3/foo', 'Some content'); + + $this->assertTrue($this->filesystem->has('container3/foo')); + $this->assertFalse($this->filesystem->has('test/somefile')); + $this->assertFalse($this->filesystem->has('test/somefile')); + + $this->filesystem->delete('container3/foo'); + } + + /** + * @test + * @group functional + */ + public function shouldGetMtime() + { + $this->filesystem->write('container4/foo', 'Some content'); + + $this->assertGreaterThan(0, $this->filesystem->mtime('container4/foo')); + + $this->filesystem->delete('container4/foo'); + } + + /** + * @test + * @group functional + * @expectedException \RuntimeException + * @expectedMessage Could not get mtime for the "foo" key + */ + public function shouldFailWhenTryMtimeForKeyWhichDoesNotExist() + { + $this->assertFalse($this->filesystem->mtime('container5/foo')); + } + + /** + * @test + * @group functional + */ + public function shouldRenameFile() + { + $this->filesystem->write('container6/foo', 'Some content'); + $this->filesystem->rename('container6/foo', 'container6-new/boo'); + + $this->assertFalse($this->filesystem->has('container6/foo')); + $this->assertEquals('Some content', $this->filesystem->read('container6-new/boo')); + $this->filesystem->delete('container6-new/boo'); + + $this->filesystem->write('container6/foo', 'Some content'); + $this->filesystem->rename('container6/foo', 'somedir/sub/boo'); + + $this->assertFalse($this->filesystem->has('somedir/sub/foo')); + $this->assertEquals('Some content', $this->filesystem->read('somedir/sub/boo')); + $this->filesystem->delete('somedir/sub/boo'); + } + + /** + * @test + * @group functional + */ + public function shouldDeleteFile() + { + $this->filesystem->write('container7/foo', 'Some content'); + + $this->assertTrue($this->filesystem->has('container7/foo')); + + $this->filesystem->delete('container7/foo'); + + $this->assertFalse($this->filesystem->has('container7/foo')); + } + + /** + * @test + * @group functional + */ + public function shouldFetchKeys() + { + $this->filesystem->write('container8-1/foo', 'Some content'); + $this->filesystem->write('container8-2/bar', 'Some content'); + $this->filesystem->write('container8-3/baz', 'Some content'); + + $actualKeys = $this->filesystem->keys(); + foreach (['container8-1/foo', 'container8-2/bar', 'container8-3/baz'] as $key) { + $this->assertContains($key, $actualKeys); + } + + $this->filesystem->delete('container8-1/foo'); + $this->filesystem->delete('container8-2/bar'); + $this->filesystem->delete('container8-3/baz'); + } + + /** + * @test + * @group functional + */ + public function shouldWorkWithHiddenFiles() + { + $this->filesystem->write('container9/.foo', 'hidden'); + $this->assertTrue($this->filesystem->has('container9/.foo')); + $this->assertContains('container9/.foo', $this->filesystem->keys()); + $this->filesystem->delete('container9/.foo'); + $this->assertFalse($this->filesystem->has('container9/.foo')); + } + + /** + * @test + * @group functional + */ + public function shouldKeepFileObjectInRegister() + { + $FileObjectA = $this->filesystem->createFile('container10/somefile'); + $FileObjectB = $this->filesystem->createFile('container10/somefile'); + + $this->assertTrue($FileObjectA === $FileObjectB); + } + + /** + * @test + * @group functional + */ + public function shouldWrtieToSameFile() + { + $FileObjectA = $this->filesystem->createFile('container11/somefile'); + $FileObjectA->setContent('ABC'); + + $FileObjectB = $this->filesystem->createFile('container11/somefile'); + $FileObjectB->setContent('DEF'); + + $this->assertEquals('DEF', $FileObjectB->getContent()); + + $this->filesystem->delete('container11/somefile'); + } +} From cb07e613951af1520e70f2df4a9296a7c0147113 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Fri, 2 Jun 2017 02:57:59 +0200 Subject: [PATCH 278/426] Deprecate AwsS3::getUrl() method and promote new gaufrette/extras repo Since AwsS3 is the only adapter to have such a method, and because it's a useful feature even outside of this adapter, I moved URL resolution to the newly created Gaufrette/extras. It was partialy inspired by K-Phoen/extras, so thanks to him. --- src/Gaufrette/Adapter/AwsS3.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index d2dc9e7f8..7b58a9de3 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -63,9 +63,19 @@ public function __construct(S3Client $service, $bucket, array $options = [], $de * operation may be specified. * * @return string + * + * @deprecated 1.0 Resolving object path into URLs is out of the scope of this repository since v0.4. gaufrette/extras + * provides a Filesystem decorator with a regular resolve() method. You should use it instead. + * + * @see https://github.com/Gaufrette/extras */ public function getUrl($key, array $options = []) { + @trigger_error( + E_USER_DEPRECATED, + 'Using AwsS3::getUrl() method was deprecated since v0.4. Please chek gaufrette/extras package if you want this feature' + ); + return $this->service->getObjectUrl( $this->bucket, $this->computePath($key), From 1bfaa42271701878eeefac986e6669701fcddbc7 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Fri, 2 Jun 2017 03:26:29 +0200 Subject: [PATCH 279/426] Add a doc page for gaufrette/extras --- doc/couscous.yml | 3 +++ doc/extras.md | 44 +++++++++++++++++++++++++++++++++ src/Gaufrette/Adapter/AwsS3.php | 2 +- 3 files changed, 48 insertions(+), 1 deletion(-) create mode 100644 doc/extras.md diff --git a/doc/couscous.yml b/doc/couscous.yml index 6f59946a3..75443ed74 100644 --- a/doc/couscous.yml +++ b/doc/couscous.yml @@ -29,6 +29,9 @@ menu: implementing-new-adapter: text: Implementing new Adapter relativeUrl: implementing-new-adapter.html + extras: + text: Extras + relativeUrl: extras.html adapters: name: Adapters items: diff --git a/doc/extras.md b/doc/extras.md new file mode 100644 index 000000000..0adeca737 --- /dev/null +++ b/doc/extras.md @@ -0,0 +1,44 @@ +--- +currentMenu: extras +--- + +# Gaufrette Extras + +Some extra features built on top of Gaufette live in a package separate from the core. It provides: + + * **Resolvable filesystem**: introduce `resolve()` method to transform an object path into a URI. + +In order to install it: + + composer require gaufrette/extras + + +## Resolvable filesystem + +Filesystem decorator providing `resolve()` method to resolve an object path into URI. It uses a resolver +(implementing `ResolverInterface`) to do the resolution. + +```php + 'root/dir'])); +$filesystem = new ResolvableFilesystem( + $decorated, + new AwsS3PresignedUrlResolver($client, 'my_bucket', 'root/dir') +); + +// should return something like "https://eu-west-1.blabla.aws.com/my_bucket/root/dir/foo/bar.png?token +var_dump($filesystem->resolve('foo/bar.png')); +``` + +Currently, the following resolvers are implemented. All can be found in the `Gaufrette\Extras\Resolvable\Resolver` namespace: + +* **AwsS3PublicUrlResolver**: Create a URL for an object stored on S3 with public ACL. +* **AwsS3PresignedUrlResolver**: Create a temporary URL, valid for a given amount of time. Useful when you have to share object(s) with private ACL. +* **StaticUrlResolver**: Resolves the object into an URL by concatenating a prefix with object pah. diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index 7b58a9de3..fec65a871 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -73,7 +73,7 @@ public function getUrl($key, array $options = []) { @trigger_error( E_USER_DEPRECATED, - 'Using AwsS3::getUrl() method was deprecated since v0.4. Please chek gaufrette/extras package if you want this feature' + 'Using AwsS3::getUrl() method was deprecated since v0.4. Please chek gaufrette/extras package if you want this feature' ); return $this->service->getObjectUrl( From 5d7b5c4d3a7a39c1d802867973c5052ba8ddcd5d Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Sat, 3 Jun 2017 13:57:03 +0200 Subject: [PATCH 280/426] Set umask to 0002 when asserting chmod of mkdir'ed dir equals 0770 --- tests/Gaufrette/Functional/FileStream/LocalTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Gaufrette/Functional/FileStream/LocalTest.php b/tests/Gaufrette/Functional/FileStream/LocalTest.php index c353ffe6a..b6b6b4de3 100644 --- a/tests/Gaufrette/Functional/FileStream/LocalTest.php +++ b/tests/Gaufrette/Functional/FileStream/LocalTest.php @@ -14,6 +14,7 @@ public function setUp() { $this->directory = __DIR__.DIRECTORY_SEPARATOR.'filesystem'; @mkdir($this->directory.DIRECTORY_SEPARATOR.'subdir', 0777, true); + umask(0002); $this->filesystem = new Filesystem(new LocalAdapter($this->directory, true, 0770)); $this->registerLocalFilesystemInStream(); From 708a530d577ede6cdefcf654ed95be26fcaae6f7 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Sat, 3 Jun 2017 14:01:38 +0200 Subject: [PATCH 281/426] Fix assertion on value returned by AwsS3::getUrl() --- .../Functional/Adapter/AwsS3Test.php | 22 ++----------------- 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php index 8047e2323..a3ce518d9 100644 --- a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php +++ b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php @@ -101,16 +101,7 @@ public function testChecksForObjectExistence() public function testGetsObjectUrls() { $filesystem = $this->getFilesystem(['create' => true]); - $expected = sprintf('https://%s.s3-eu-west-1.amazonaws.com/foo', $this->bucket); - - if (self::$SDK_VERSION === 3) { - $expected = sprintf('https://s3-eu-west-1.amazonaws.com/%s/foo', $this->bucket); - } - - $this->assertEquals( - $expected, - $filesystem->getAdapter()->getUrl('foo') - ); + $this->assertNotEmpty($filesystem->getAdapter()->getUrl('foo')); } public function testChecksForObjectExistenceWithDirectory() @@ -124,16 +115,7 @@ public function testChecksForObjectExistenceWithDirectory() public function testGetsObjectUrlsWithDirectory() { $filesystem = $this->getFilesystem(['directory' => 'bar']); - $expected = sprintf('https://%s.s3-eu-west-1.amazonaws.com/bar/foo', $this->bucket); - - if (self::$SDK_VERSION === 3) { - $expected = sprintf('https://s3-eu-west-1.amazonaws.com/%s/bar/foo', $this->bucket); - } - - $this->assertEquals( - $expected, - $filesystem->getAdapter()->getUrl('foo') - ); + $this->assertNotEmpty($filesystem->getAdapter()->getUrl('foo')); } public function testListKeysWithoutDirectory() From 1551c5297485cfab438f2a37c44badd12d2f7df9 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Thu, 8 Jun 2017 11:19:30 +0200 Subject: [PATCH 282/426] Add microsoft/azure-storage to dev deps --- .travis.yml | 1 - composer.json | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index a0404cbae..c8a9f928e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -49,7 +49,6 @@ before_script: - if [[ ${TRAVIS_PHP_VERSION:0:4} != "hhvm" ]]; then yes | pecl install ssh2 ; fi - cp tests/Gaufrette/Functional/adapters/DoctrineDbal.php.dist tests/Gaufrette/Functional/adapters/DoctrineDbal.php -f - composer self-update && composer install --prefer-source --no-interaction - - if [[ ${TRAVIS_PHP_VERSION:0:3} != "5.4" ]]; then composer require microsoft/windowsazure:^0.4.3; fi script: - php vendor/bin/phpspec run -fpretty --verbose diff --git a/composer.json b/composer.json index c4790d3eb..4b63bfd82 100644 --- a/composer.json +++ b/composer.json @@ -41,7 +41,9 @@ "phpunit/phpunit": "3.7.*", "mikey179/vfsStream": "~1.2.0", "league/flysystem": "~1.0", - "mongodb/mongodb": "~1.1.4" + "mongodb/mongodb": "~1.1.4", + "microsoft/windowsazure": "~0.4", + "microsoft/azure-storage": "~0.15.0" }, "suggest": { "knplabs/knp-gaufrette-bundle": "to use with Symfony2", From 7fa5818413e0a113e8ec16f5b0b821d2b3215024 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Thu, 8 Jun 2017 11:27:32 +0200 Subject: [PATCH 283/426] Add skip message for AWS & Azure tests --- tests/Gaufrette/Functional/Adapter/AwsS3Test.php | 2 +- tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php | 2 +- .../Functional/Adapter/AzureMultiContainerBlobStorageTest.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php index a3ce518d9..11921ecb4 100644 --- a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php +++ b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php @@ -26,7 +26,7 @@ public function setUp() $secret = getenv('AWS_SECRET'); if (empty($key) || empty($secret)) { - $this->markTestSkipped(); + $this->markTestSkipped('Either AWS_KEY and/or AWS_SECRET env variables are not defined.'); } if (self::$SDK_VERSION === null) { diff --git a/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php b/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php index a62f88cf9..4ca718312 100644 --- a/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php +++ b/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php @@ -18,7 +18,7 @@ public function setUp() $secret = getenv('AZURE_SECRET'); $containerName = getenv('AZURE_CONTAINER'); if (empty($key) || empty($secret) || empty($containerName)) { - $this->markTestSkipped(); + $this->markTestSkipped('Either AZURE_KEY, AZURE_SECRET and/or AZURE_CONTAINER env variables are not defined.'); } $connection = sprintf('BlobEndpoint=http://%1$s.blob.core.windows.net/;AccountName=%1$s;AccountKey=%2$s', $key, $secret); diff --git a/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php index e60a09de7..8e985bc14 100644 --- a/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php +++ b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php @@ -18,7 +18,7 @@ public function setUp() $key = getenv('AZURE_KEY'); $secret = getenv('AZURE_SECRET'); if (empty($key) || empty($secret)) { - $this->markTestSkipped(); + $this->markTestSkipped('Either AZURE_KEY and/or AZURE_SECRET env variables are not defined.'); } $connection = sprintf('BlobEndpoint=http://%1$s.blob.core.windows.net/;AccountName=%1$s;AccountKey=%2$s', $key, $secret); From c966d14bc3f017866c063e94d896fe082f5c4423 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Thu, 8 Jun 2017 11:38:49 +0200 Subject: [PATCH 284/426] Rename environment variables related to Azure to follow its semantic --- .env.dist | 4 ++++ phpunit.xml.dist | 4 ++-- .../Functional/Adapter/AzureBlobStorageTest.php | 9 +++++---- .../Adapter/AzureMultiContainerBlobStorageTest.php | 8 ++++---- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/.env.dist b/.env.dist index 5446efc57..9b5769157 100644 --- a/.env.dist +++ b/.env.dist @@ -15,3 +15,7 @@ FTP_HOST=vsftpd FTP_USER=gaufrette FTP_PASSWORD=gaufrette FTP_BASE_DIR=/gaufrette/ + +AZURE_ACCOUNT= +AZURE_KEY= +AZURE_CONTAINER= diff --git a/phpunit.xml.dist b/phpunit.xml.dist index d3101efee..3da084d85 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -35,8 +35,8 @@ --> - diff --git a/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php b/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php index 4ca718312..c12b20a15 100644 --- a/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php +++ b/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php @@ -14,14 +14,15 @@ class AzureBlobStorageTest extends FunctionalTestCase { public function setUp() { + $account = getenv('AZURE_ACCOUNT'); $key = getenv('AZURE_KEY'); - $secret = getenv('AZURE_SECRET'); $containerName = getenv('AZURE_CONTAINER'); - if (empty($key) || empty($secret) || empty($containerName)) { - $this->markTestSkipped('Either AZURE_KEY, AZURE_SECRET and/or AZURE_CONTAINER env variables are not defined.'); + + if (empty($account) || empty($key) || empty($containerName)) { + $this->markTestSkipped('Either AZURE_ACCOUNT, AZURE_KEY and/or AZURE_CONTAINER env variables are not defined.'); } - $connection = sprintf('BlobEndpoint=http://%1$s.blob.core.windows.net/;AccountName=%1$s;AccountKey=%2$s', $key, $secret); + $connection = sprintf('BlobEndpoint=http://%1$s.blob.core.windows.net/;AccountName=%1$s;AccountKey=%2$s', $account, $key); $this->filesystem = new Filesystem(new AzureBlobStorage(new BlobProxyFactory($connection), $containerName, true)); } diff --git a/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php index 8e985bc14..9fed28dcf 100644 --- a/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php +++ b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php @@ -15,13 +15,13 @@ class AzureMultiContainerBlobStorageTest extends FunctionalTestCase { public function setUp() { + $account = getenv('AZURE_ACCOUNT'); $key = getenv('AZURE_KEY'); - $secret = getenv('AZURE_SECRET'); - if (empty($key) || empty($secret)) { - $this->markTestSkipped('Either AZURE_KEY and/or AZURE_SECRET env variables are not defined.'); + if (empty($account) || empty($key)) { + $this->markTestSkipped('Either AZURE_ACCOUNT and/or AZURE_KEY env variables are not defined.'); } - $connection = sprintf('BlobEndpoint=http://%1$s.blob.core.windows.net/;AccountName=%1$s;AccountKey=%2$s', $key, $secret); + $connection = sprintf('BlobEndpoint=http://%1$s.blob.core.windows.net/;AccountName=%1$s;AccountKey=%2$s', $account, $key); $this->filesystem = new Filesystem(new AzureBlobStorage(new BlobProxyFactory($connection))); } From 6d6a99a5f33b85c0f66c9c86e91f51c9b41057a8 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Thu, 8 Jun 2017 12:09:27 +0200 Subject: [PATCH 285/426] Add a note about Flysystem adapter in the deprecation notice of Dropbox --- src/Gaufrette/Adapter/Dropbox.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/Dropbox.php b/src/Gaufrette/Adapter/Dropbox.php index 7379d75cd..aa4406956 100644 --- a/src/Gaufrette/Adapter/Dropbox.php +++ b/src/Gaufrette/Adapter/Dropbox.php @@ -8,7 +8,7 @@ use Dropbox_API as DropboxApi; use Dropbox_Exception_NotFound as DropboxNotFoundException; -@trigger_error('The '.__NAMESPACE__.'\Dropbox adapter is deprecated since version 0.4 and will be removed in 1.0.', E_USER_DEPRECATED); +@trigger_error('The '.__NAMESPACE__.'\Dropbox adapter is deprecated since version 0.4 and will be removed in 1.0. You can move to our Flysystem adapter and use their Dropbox adapter if needed.', E_USER_DEPRECATED); /** * Dropbox adapter. From ea302ca0b11b677fbf57532223040bfd9b9d4a4d Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Thu, 8 Jun 2017 12:14:47 +0200 Subject: [PATCH 286/426] Remove deprecation notice for GoogleCloudStorage It should be re-added when #427 is merged. --- src/Gaufrette/Adapter/GoogleCloudStorage.php | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/Gaufrette/Adapter/GoogleCloudStorage.php b/src/Gaufrette/Adapter/GoogleCloudStorage.php index 0d7509347..00ba518c9 100644 --- a/src/Gaufrette/Adapter/GoogleCloudStorage.php +++ b/src/Gaufrette/Adapter/GoogleCloudStorage.php @@ -5,14 +5,10 @@ use Gaufrette\Adapter; use GuzzleHttp; -@trigger_error('The '.__NAMESPACE__.'\GoogleCloudStorage adapter is deprecated since version 0.4 and will be removed in 1.0.', E_USER_DEPRECATED); - /** * Google Cloud Storage adapter using the Google APIs Client Library for PHP. * * @author Patrik Karisch - * - * @deprecated The GoogleCloudStorage adapter is deprecated since version 0.4 and will be removed in 1.0. */ class GoogleCloudStorage implements Adapter, MetadataSupporter, From eff1f937fa4371f0fa78c2eb31c290000566af3f Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Thu, 8 Jun 2017 13:05:01 +0200 Subject: [PATCH 287/426] Remove deps for deprecated adapters from suggest section --- composer.json | 4 ---- 1 file changed, 4 deletions(-) diff --git a/composer.json b/composer.json index c4790d3eb..97caf7956 100644 --- a/composer.json +++ b/composer.json @@ -45,18 +45,14 @@ }, "suggest": { "knplabs/knp-gaufrette-bundle": "to use with Symfony2", - "dropbox-php/dropbox-php": "to use the Dropbox adapter", "rackspace/php-opencloud" : "to use Opencloud adapter", "google/apiclient": "to use GoogleCloudStorage adapter", - "herzult/php-ssh": "to use SFtp adapter", "phpseclib/phpseclib": "to use PhpseclibSftp adapter", "aws/aws-sdk-php": "to use the Amazon S3 adapter", - "amazonwebservices/aws-sdk-for-php": "to use the legacy Amazon S3 adapters", "doctrine/dbal": "to use the Doctrine DBAL adapter", "microsoft/windowsazure": "to use Microsoft Azure Blob Storage adapter", "league/flysystem": "to use Flysystem adapters", "ext-zip": "to use the Zip adapter", - "ext-apc": "to use the APC adapter", "ext-curl": "*", "ext-mbstring": "*", "ext-mongodb": "*", From 4cde58493d31d2df01eb87dc3774ccb5042a792a Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Thu, 8 Jun 2017 13:43:17 +0200 Subject: [PATCH 288/426] Replace SDKs in suggest section with gaufrette metapackages --- composer.json | 22 ++++++++++++---------- 1 file changed, 12 insertions(+), 10 deletions(-) diff --git a/composer.json b/composer.json index 97caf7956..03509e870 100644 --- a/composer.json +++ b/composer.json @@ -45,19 +45,21 @@ }, "suggest": { "knplabs/knp-gaufrette-bundle": "to use with Symfony2", - "rackspace/php-opencloud" : "to use Opencloud adapter", + "gaufrette/azure-blob-storage-adapter": "to use AzureBlobStorage adapter", + "gaufrette/flysystem-adapter": "to use Flysystem adapter", + "gaufrette/doctrine-dbal-adapter": "to use DBAL adapter", + "gaufrette/phpseclib-sftp-adapter": "to use PhpseclibSftp adapter", + "gaufrette/ftp-adapter": "to use Ftp adapter", + "gaufrette/aws-s3-adapter": "to use AwsS3 adapter (supports SDK v2 and v3)", + "gaufrette/in-memory-adapter": "to use InMemory adapter", + "gaufrette/local-adapter": "to use Local adapter", + "gaufrette/opencloud-adapter": "to use Opencloud adapter", + "gaufrette/zip-adapter": "to use Zip adapter", + "gaufrette/gridfs-adapter": "to use GridFS adapter", "google/apiclient": "to use GoogleCloudStorage adapter", - "phpseclib/phpseclib": "to use PhpseclibSftp adapter", - "aws/aws-sdk-php": "to use the Amazon S3 adapter", - "doctrine/dbal": "to use the Doctrine DBAL adapter", - "microsoft/windowsazure": "to use Microsoft Azure Blob Storage adapter", - "league/flysystem": "to use Flysystem adapters", - "ext-zip": "to use the Zip adapter", "ext-curl": "*", "ext-mbstring": "*", - "ext-mongodb": "*", - "ext-fileinfo": "This extension is used to automatically detect the content-type of a file in the AwsS3, OpenCloud, AzureBlogStorage and GoogleCloudStorage adapters", - "mongodb/mongodb": "*" + "ext-fileinfo": "This extension is used to automatically detect the content-type of a file in the AwsS3, OpenCloud, AzureBlogStorage and GoogleCloudStorage adapters" }, "autoload": { "psr-0": { "Gaufrette": "src/" } From baaea1a6dece8bce6d63f91b5fb066c6bd31da90 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Thu, 8 Jun 2017 11:44:14 +0200 Subject: [PATCH 289/426] Randomise container used by Azure functional tests --- .../Adapter/AzureBlobStorageTest.php | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php b/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php index c12b20a15..9ec350f72 100644 --- a/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php +++ b/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php @@ -12,6 +12,12 @@ */ class AzureBlobStorageTest extends FunctionalTestCase { + /** @var string Name of the Azure container used */ + private $container; + + /** @var AzureBlobStorage */ + private $adapter; + public function setUp() { $account = getenv('AZURE_ACCOUNT'); @@ -24,6 +30,17 @@ public function setUp() $connection = sprintf('BlobEndpoint=http://%1$s.blob.core.windows.net/;AccountName=%1$s;AccountKey=%2$s', $account, $key); - $this->filesystem = new Filesystem(new AzureBlobStorage(new BlobProxyFactory($connection), $containerName, true)); + $this->container = uniqid($containerName); + $this->adapter = new AzureBlobStorage(new BlobProxyFactory($connection), $this->container, true); + $this->filesystem = new Filesystem($this->adapter); + } + + public function tearDown() + { + if ($this->adapter === null) { + return; + } + + $this->adapter->deleteContainer($this->container); } } From 3b3a913320db00308279ca9016b9e92cc25b07da Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Thu, 8 Jun 2017 11:45:15 +0200 Subject: [PATCH 290/426] Add default value for AZURE_ACCOUNT and AZURE_KEY --- .travis.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.travis.yml b/.travis.yml index c8a9f928e..d6b666f38 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,8 @@ env: global: - AWS_KEY: - AWS_SECRET: + - AZURE_ACCOUNT: + - AZURE_KEY: php: - 5.6 From 1b5976533ff3bbd481de009ec25955db31ddb250 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Sun, 19 Mar 2017 23:17:33 +0100 Subject: [PATCH 291/426] Remove support for php 5.4 & php 5.5 --- CHANGELOG.md | 1 + bin/tests-all | 2 -- composer.json | 2 +- docker-compose.yml | 20 -------------------- docker/Dockerfile-php54 | 22 ---------------------- docker/Dockerfile-php55 | 22 ---------------------- 6 files changed, 2 insertions(+), 67 deletions(-) delete mode 100644 docker/Dockerfile-php54 delete mode 100644 docker/Dockerfile-php55 diff --git a/CHANGELOG.md b/CHANGELOG.md index 719deac03..165b7777d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,3 +2,4 @@ v0.4 ==== * Unmaintained `mongo` extension replaced with newer `mongodb` extension +* Support for PHP 5.4 & PHP 5.5 has been dropped diff --git a/bin/tests-all b/bin/tests-all index 565f84ffe..a507fe4b3 100755 --- a/bin/tests-all +++ b/bin/tests-all @@ -2,8 +2,6 @@ set -o xtrace -docker-compose run --rm php54 -docker-compose run --rm php55 docker-compose run --rm php56 docker-compose run --rm php70 docker-compose run --rm php71 diff --git a/composer.json b/composer.json index 5297f05cc..cbef0eed1 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,7 @@ } ], "require": { - "php": ">=5.4" + "php": ">=5.6" }, "conflict": { "microsoft/windowsazure": "<0.4.3" diff --git a/docker-compose.yml b/docker-compose.yml index 8b53ed7c2..5da26da61 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,26 +2,6 @@ version: '2' services: - php54: - build: - context: . - dockerfile: docker/Dockerfile-php54 - volumes: - - './spec/:/usr/src/gaufrette/spec/' - - './src/:/usr/src/gaufrette/src/' - - './tests/:/usr/src/gaufrette/tests/' - env_file: .env - - php55: - build: - context: . - dockerfile: docker/Dockerfile-php55 - volumes: - - './spec/:/usr/src/gaufrette/spec/' - - './src/:/usr/src/gaufrette/src/' - - './tests/:/usr/src/gaufrette/tests/' - env_file: .env - php56: build: context: . diff --git a/docker/Dockerfile-php54 b/docker/Dockerfile-php54 deleted file mode 100644 index 82af1f820..000000000 --- a/docker/Dockerfile-php54 +++ /dev/null @@ -1,22 +0,0 @@ -FROM jolicode/php54:latest - -ENV SRC_DIR /usr/src/gaufrette -WORKDIR ${SRC_DIR}/ - -RUN sudo apt-get update && \ - sudo apt-get install -y libssh2-1-dev && \ - sudo rm -rf /var/lib/apt/lists/* && \ - pecl install mongo ssh2 - -COPY composer.json ${SRC_DIR}/ -RUN sudo chown -R travis ${SRC_DIR} && \ - composer global require hirak/prestissimo && \ - composer install --prefer-dist && \ - rm -rf ~/.composer/cache/* - -VOLUME ${SRC_DIR}/vendor - -COPY . ${SRC_DIR}/ -RUN sudo chown -R travis ${SRC_DIR} - -CMD "${SRC_DIR}/bin/tests" diff --git a/docker/Dockerfile-php55 b/docker/Dockerfile-php55 deleted file mode 100644 index 3e9e3f27b..000000000 --- a/docker/Dockerfile-php55 +++ /dev/null @@ -1,22 +0,0 @@ -FROM jolicode/php55:latest - -ENV SRC_DIR /usr/src/gaufrette -WORKDIR ${SRC_DIR}/ - -RUN sudo apt-get update && \ - sudo apt-get install -y libssh2-1-dev && \ - sudo rm -rf /var/lib/apt/lists/* && \ - pecl install mongo ssh2 - -COPY composer.json ${SRC_DIR}/ -RUN sudo chown -R travis ${SRC_DIR} && \ - composer global require hirak/prestissimo && \ - composer install --prefer-dist && \ - rm -rf ~/.composer/cache/* - -VOLUME ${SRC_DIR}/vendor - -COPY . ${SRC_DIR}/ -RUN sudo chown -R travis ${SRC_DIR} - -CMD "${SRC_DIR}/bin/tests" From 6470bd55dc8e3ee2690a24b0437556fa85abd89b Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Thu, 8 Jun 2017 15:31:34 +0200 Subject: [PATCH 292/426] Run tests under php 7.0 & 7.1 on Travis --- .travis.yml | 12 +++++++++--- composer.json | 1 - 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index d6b666f38..7afb7b528 100644 --- a/.travis.yml +++ b/.travis.yml @@ -15,6 +15,8 @@ env: php: - 5.6 + - 7.0 + - 7.1 - hhvm services: @@ -38,8 +40,12 @@ matrix: fast_finish: true before_script: - - if [[ ${TRAVIS_PHP_VERSION:0:1} == "5" ]]; then echo "extension = mongodb.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini && phpenv config-rm xdebug.ini; fi - - if [[ ${TRAVIS_PHP_VERSION:0:4} == "hhvm" ]]; then composer require mongofill/mongofill --ignore-platform-reqs; fi + - if [[ ${TRAVIS_PHP_VERSION:0:1} != "hhvm" ]]; then + echo "extension = mongodb.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; + phpenv config-rm xdebug.ini; + else + composer require mongofill/mongofill --ignore-platform-reqs; + fi - sudo apt-get update - sudo apt-get install -y libssh2-1-dev vsftpd - sudo useradd -d /home/./gaufrette gaufrette # Removing "./" from this path would break any further Travis builds @@ -48,9 +54,9 @@ before_script: - sudo chown gaufrette:gaufrette /home/gaufrette /srv/ftp - sudo cp .travis/vsftpd.conf /etc/vsftpd.conf - sudo service vsftpd restart - - if [[ ${TRAVIS_PHP_VERSION:0:4} != "hhvm" ]]; then yes | pecl install ssh2 ; fi - cp tests/Gaufrette/Functional/adapters/DoctrineDbal.php.dist tests/Gaufrette/Functional/adapters/DoctrineDbal.php -f - composer self-update && composer install --prefer-source --no-interaction + - if [[ ${TRAVIS_PHP_VERSION:0:1} == "5" ]]; then yes | pecl install ssh2 && composer require herzult/php-ssh; fi script: - php vendor/bin/phpspec run -fpretty --verbose diff --git a/composer.json b/composer.json index cbef0eed1..dade9cd4c 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,6 @@ "phpseclib/phpseclib": "^2.0", "doctrine/dbal": ">=2.3", "dropbox-php/dropbox-php": "*", - "herzult/php-ssh": "*", "phpunit/phpunit": "3.7.*", "mikey179/vfsStream": "~1.2.0", "league/flysystem": "~1.0", From 1317fd93e009ad69ba273c5490600944be850be7 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Thu, 8 Jun 2017 16:45:10 +0200 Subject: [PATCH 293/426] Skip tests for Ssh adapter if ssh2 ext isn't loaded --- tests/Gaufrette/Functional/Adapter/SftpTest.php | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/Gaufrette/Functional/Adapter/SftpTest.php b/tests/Gaufrette/Functional/Adapter/SftpTest.php index 536b87801..974e59029 100644 --- a/tests/Gaufrette/Functional/Adapter/SftpTest.php +++ b/tests/Gaufrette/Functional/Adapter/SftpTest.php @@ -2,8 +2,14 @@ namespace Gaufrette\Functional\Adapter; -use Gaufrette\Adapter\Sftp; - class SftpTest extends FunctionalTestCase { + public function setUp() + { + if (!extension_loaded('ssh2')) { + $this->markTestSkipped('Extension ssh2 not loaded'); + } + + return parent::setUp(); + } } From 2cb135ff1895f63555407a472cb4c6f14a563806 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Thu, 8 Jun 2017 17:38:42 +0200 Subject: [PATCH 294/426] Skip phpspec examples if herzult/php-ssh is not installed --- composer.json | 3 ++- phpspec.yml | 2 ++ spec/Gaufrette/Adapter/SftpSpec.php | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 phpspec.yml diff --git a/composer.json b/composer.json index dade9cd4c..7f9293031 100644 --- a/composer.json +++ b/composer.json @@ -42,7 +42,8 @@ "league/flysystem": "~1.0", "mongodb/mongodb": "~1.1.4", "microsoft/windowsazure": "~0.4", - "microsoft/azure-storage": "~0.15.0" + "microsoft/azure-storage": "~0.15.0", + "akeneo/phpspec-skip-example-extension": "~1.2" }, "suggest": { "knplabs/knp-gaufrette-bundle": "to use with Symfony2", diff --git a/phpspec.yml b/phpspec.yml new file mode 100644 index 000000000..feb982816 --- /dev/null +++ b/phpspec.yml @@ -0,0 +1,2 @@ +extensions: + Akeneo\SkipExampleExtension: Akeneo\SkipExampleExtension diff --git a/spec/Gaufrette/Adapter/SftpSpec.php b/spec/Gaufrette/Adapter/SftpSpec.php index 14061e847..f5d62ce0a 100644 --- a/spec/Gaufrette/Adapter/SftpSpec.php +++ b/spec/Gaufrette/Adapter/SftpSpec.php @@ -7,6 +7,9 @@ use PhpSpec\ObjectBehavior; +/** + * @require Ssh\Sftp + */ class SftpSpec extends ObjectBehavior { /** From 0a3ac9f01c5c5f3d8b81aef64d661b2cc1fbb75f Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Thu, 8 Jun 2017 20:34:44 +0200 Subject: [PATCH 295/426] Randomise container name used by AzureMultiContainerBlobStorageTest It happens from time to time to see phpunit tests broken due to conflicting usage of storage containers by the Azure adapter, especially if there're concurrent phpunit executions. --- .../AzureMultiContainerBlobStorageTest.php | 136 +++++++++++------- 1 file changed, 84 insertions(+), 52 deletions(-) diff --git a/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php index 9fed28dcf..28cb82a26 100644 --- a/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php +++ b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php @@ -13,6 +13,10 @@ */ class AzureMultiContainerBlobStorageTest extends FunctionalTestCase { + private $adapter; + + private $containers = []; + public function setUp() { $account = getenv('AZURE_ACCOUNT'); @@ -23,7 +27,8 @@ public function setUp() $connection = sprintf('BlobEndpoint=http://%1$s.blob.core.windows.net/;AccountName=%1$s;AccountKey=%2$s', $account, $key); - $this->filesystem = new Filesystem(new AzureBlobStorage(new BlobProxyFactory($connection))); + $this->adapter = new AzureBlobStorage(new BlobProxyFactory($connection)); + $this->filesystem = new Filesystem($this->adapter); } /** @@ -32,13 +37,14 @@ public function setUp() */ public function shouldWriteAndRead() { - $this->assertEquals(12, $this->filesystem->write('container1/foo', 'Some content')); - $this->assertEquals(13, $this->filesystem->write('test/subdir/foo', 'Some content1', true)); + $path1 = $this->createUniqueContainerName('container') . '/foo'; + $path2 = $this->createUniqueContainerName('test') . '/subdir/foo'; + + $this->assertEquals(12, $this->filesystem->write($path1, 'Some content')); + $this->assertEquals(13, $this->filesystem->write($path2, 'Some content1', true)); - $this->assertEquals('Some content', $this->filesystem->read('container1/foo')); - $this->assertEquals('Some content1', $this->filesystem->read('test/subdir/foo')); - $this->filesystem->delete('container1/foo'); - $this->filesystem->delete('test/subdir/foo'); + $this->assertEquals('Some content', $this->filesystem->read($path1)); + $this->assertEquals('Some content1', $this->filesystem->read($path2)); } /** @@ -47,11 +53,12 @@ public function shouldWriteAndRead() */ public function shouldUpdateFileContent() { - $this->filesystem->write('container2/foo', 'Some content'); - $this->filesystem->write('container2/foo', 'Some content updated', true); + $path = $this->createUniqueContainerName('container') . '/foo'; - $this->assertEquals('Some content updated', $this->filesystem->read('container2/foo')); - $this->filesystem->delete('container2/foo'); + $this->filesystem->write($path, 'Some content'); + $this->filesystem->write($path, 'Some content updated', true); + + $this->assertEquals('Some content updated', $this->filesystem->read($path)); } /** @@ -60,15 +67,17 @@ public function shouldUpdateFileContent() */ public function shouldCheckIfFileExists() { - $this->assertFalse($this->filesystem->has('container3/foo')); + $path1 = $this->createUniqueContainerName('container') . '/foo'; + $path2 = $this->createUniqueContainerName('test') . '/somefile'; - $this->filesystem->write('container3/foo', 'Some content'); + $this->assertFalse($this->filesystem->has($path1)); - $this->assertTrue($this->filesystem->has('container3/foo')); - $this->assertFalse($this->filesystem->has('test/somefile')); - $this->assertFalse($this->filesystem->has('test/somefile')); + $this->filesystem->write($path1, 'Some content'); - $this->filesystem->delete('container3/foo'); + $this->assertTrue($this->filesystem->has($path1)); + // @TODO: why is it done two times? + $this->assertFalse($this->filesystem->has($path2)); + $this->assertFalse($this->filesystem->has($path2)); } /** @@ -77,11 +86,11 @@ public function shouldCheckIfFileExists() */ public function shouldGetMtime() { - $this->filesystem->write('container4/foo', 'Some content'); + $path = $this->createUniqueContainerName('container') . '/foo'; - $this->assertGreaterThan(0, $this->filesystem->mtime('container4/foo')); + $this->filesystem->write($path, 'Some content'); - $this->filesystem->delete('container4/foo'); + $this->assertGreaterThan(0, $this->filesystem->mtime($path)); } /** @@ -101,19 +110,22 @@ public function shouldFailWhenTryMtimeForKeyWhichDoesNotExist() */ public function shouldRenameFile() { - $this->filesystem->write('container6/foo', 'Some content'); - $this->filesystem->rename('container6/foo', 'container6-new/boo'); + $somedir = $this->createUniqueContainerName('somedir'); + $path1 = $this->createUniqueContainerName('container') . '/foo'; + $path2 = $this->createUniqueContainerName('container-new') . '/boo'; + $path3 = $somedir . '/sub/boo'; + + $this->filesystem->write($path1, 'Some content'); + $this->filesystem->rename($path1, $path2); - $this->assertFalse($this->filesystem->has('container6/foo')); - $this->assertEquals('Some content', $this->filesystem->read('container6-new/boo')); - $this->filesystem->delete('container6-new/boo'); + $this->assertFalse($this->filesystem->has($path1)); + $this->assertEquals('Some content', $this->filesystem->read($path2)); - $this->filesystem->write('container6/foo', 'Some content'); - $this->filesystem->rename('container6/foo', 'somedir/sub/boo'); + $this->filesystem->write($path1, 'Some content'); + $this->filesystem->rename($path1, $path3); - $this->assertFalse($this->filesystem->has('somedir/sub/foo')); - $this->assertEquals('Some content', $this->filesystem->read('somedir/sub/boo')); - $this->filesystem->delete('somedir/sub/boo'); + $this->assertFalse($this->filesystem->has($somedir . '/sub/foo')); + $this->assertEquals('Some content', $this->filesystem->read($path3)); } /** @@ -122,13 +134,15 @@ public function shouldRenameFile() */ public function shouldDeleteFile() { - $this->filesystem->write('container7/foo', 'Some content'); + $path = $this->createUniqueContainerName('container') . '/foo'; - $this->assertTrue($this->filesystem->has('container7/foo')); + $this->filesystem->write($path, 'Some content'); - $this->filesystem->delete('container7/foo'); + $this->assertTrue($this->filesystem->has($path)); - $this->assertFalse($this->filesystem->has('container7/foo')); + $this->filesystem->delete($path); + + $this->assertFalse($this->filesystem->has($path)); } /** @@ -137,18 +151,18 @@ public function shouldDeleteFile() */ public function shouldFetchKeys() { - $this->filesystem->write('container8-1/foo', 'Some content'); - $this->filesystem->write('container8-2/bar', 'Some content'); - $this->filesystem->write('container8-3/baz', 'Some content'); + $path1 = $this->createUniqueContainerName('container-1') . '/foo'; + $path2 = $this->createUniqueContainerName('container-2') . '/bar'; + $path3 = $this->createUniqueContainerName('container-3') . '/baz'; + + $this->filesystem->write($path1, 'Some content'); + $this->filesystem->write($path2, 'Some content'); + $this->filesystem->write($path3, 'Some content'); $actualKeys = $this->filesystem->keys(); - foreach (['container8-1/foo', 'container8-2/bar', 'container8-3/baz'] as $key) { + foreach ([$path1, $path2, $path3] as $key) { $this->assertContains($key, $actualKeys); } - - $this->filesystem->delete('container8-1/foo'); - $this->filesystem->delete('container8-2/bar'); - $this->filesystem->delete('container8-3/baz'); } /** @@ -157,11 +171,13 @@ public function shouldFetchKeys() */ public function shouldWorkWithHiddenFiles() { - $this->filesystem->write('container9/.foo', 'hidden'); - $this->assertTrue($this->filesystem->has('container9/.foo')); - $this->assertContains('container9/.foo', $this->filesystem->keys()); - $this->filesystem->delete('container9/.foo'); - $this->assertFalse($this->filesystem->has('container9/.foo')); + $path = $this->createUniqueContainerName('container') . '/.foo'; + + $this->filesystem->write($path, 'hidden'); + $this->assertTrue($this->filesystem->has($path)); + $this->assertContains($path, $this->filesystem->keys()); + $this->filesystem->delete($path); + $this->assertFalse($this->filesystem->has($path)); } /** @@ -170,8 +186,10 @@ public function shouldWorkWithHiddenFiles() */ public function shouldKeepFileObjectInRegister() { - $FileObjectA = $this->filesystem->createFile('container10/somefile'); - $FileObjectB = $this->filesystem->createFile('container10/somefile'); + $path = $this->createUniqueContainerName('container') . '/somefile'; + + $FileObjectA = $this->filesystem->createFile($path); + $FileObjectB = $this->filesystem->createFile($path); $this->assertTrue($FileObjectA === $FileObjectB); } @@ -182,14 +200,28 @@ public function shouldKeepFileObjectInRegister() */ public function shouldWrtieToSameFile() { - $FileObjectA = $this->filesystem->createFile('container11/somefile'); + $path = $this->createUniqueContainerName('container') . '/somefile'; + + $FileObjectA = $this->filesystem->createFile($path); $FileObjectA->setContent('ABC'); - $FileObjectB = $this->filesystem->createFile('container11/somefile'); + $FileObjectB = $this->filesystem->createFile($path); $FileObjectB->setContent('DEF'); $this->assertEquals('DEF', $FileObjectB->getContent()); + } + + private function createUniqueContainerName($prefix) + { + $this->containers[] = $container = uniqid($prefix); - $this->filesystem->delete('container11/somefile'); + return $container; + } + + public function tearDown() + { + foreach ($this->containers as $container) { + $this->adapter->deleteContainer($container); + } } } From 22575428f70087e5f2d59067dd1cb5e01aaf3bf0 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Fri, 9 Jun 2017 00:07:32 +0200 Subject: [PATCH 296/426] libzip isn't bundled with php anymore --- docker/Dockerfile-php70 | 3 +-- docker/Dockerfile-php71 | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/docker/Dockerfile-php70 b/docker/Dockerfile-php70 index a66fe404b..fc6f95701 100644 --- a/docker/Dockerfile-php70 +++ b/docker/Dockerfile-php70 @@ -9,7 +9,7 @@ RUN apt-get update && \ libssh2-1-dev \ libssl-dev \ unzip \ - zlib1g-dev \ + libzip-dev \ && rm -rf /var/lib/apt/lists/* && \ pecl install mongodb ssh2-alpha zip && \ echo 'extension=/usr/local/lib/php/extensions/no-debug-non-zts-20151012/mongodb.so' >> /usr/local/etc/php/conf.d/mongodb.ini && \ @@ -19,7 +19,6 @@ RUN apt-get update && \ COPY composer.json ${SRC_DIR}/ RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer && \ composer install && \ - composer require microsoft/windowsazure:dev-master && \ rm -rf ~/.composer/cache/* VOLUME ${SRC_DIR}/vendor diff --git a/docker/Dockerfile-php71 b/docker/Dockerfile-php71 index 5a809201b..95eddd1ff 100644 --- a/docker/Dockerfile-php71 +++ b/docker/Dockerfile-php71 @@ -9,7 +9,7 @@ RUN apt-get update && \ libssh2-1-dev \ libssl-dev \ unzip \ - zlib1g-dev \ + libzip-dev \ && rm -rf /var/lib/apt/lists/* && \ pecl install mongodb ssh2-alpha zip && \ echo 'extension=/usr/local/lib/php/extensions/no-debug-non-zts-20160303/mongodb.so' >> /usr/local/etc/php/conf.d/mongodb.ini && \ @@ -20,7 +20,6 @@ COPY composer.json ${SRC_DIR}/ RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer && \ composer global require hirak/prestissimo && \ composer update --prefer-lowest --prefer-stable && \ - composer require microsoft/windowsazure:~0.5 && \ rm -rf ~/.composer/cache/* VOLUME ${SRC_DIR}/vendor From 5f4b978c3ccf28b9e88a27b819d95c0c4b5e2986 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Fri, 9 Jun 2017 11:37:27 +0200 Subject: [PATCH 297/426] Add a badge for AppVeyor --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 5798da93d..8d9011a1b 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,7 @@ Gaufrette Gaufrette provides a filesystem abstraction layer. [![Build Status](https://img.shields.io/travis/KnpLabs/Gaufrette/master.svg?style=flat-square)](http://travis-ci.org/KnpLabs/Gaufrette) +[![AppVeyor Status](https://img.shields.io/appveyor/ci/NiR-/Gaufrette/master.svg?style=flat-square)](https://ci.appveyor.com/project/NiR-/gaufrette) [![Quality Score](https://img.shields.io/scrutinizer/g/KnpLabs/Gaufrette.svg?style=flat-square)](https://scrutinizer-ci.com/g/KnpLabs/Gaufrette) [![Packagist Version](https://img.shields.io/packagist/v/KnpLabs/Gaufrette.svg?style=flat-square)](https://packagist.org/packages/KnpLabs/Gaufrette) [![Total Downloads](https://img.shields.io/packagist/dt/KnpLabs/Gaufrette.svg?style=flat-square)](https://packagist.org/packages/KnpLabs/Gaufrette) From b6797f74557ceb7c3c4ad4151be8066723d9bafc Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Fri, 9 Jun 2017 01:14:32 +0200 Subject: [PATCH 298/426] Move adapter instantiation logic to DoctrineDbalTest --- .travis.yml | 1 - .../Functional/Adapter/DoctrineDbalTest.php | 37 +++++++++++++++++++ .../Functional/adapters/DoctrineDbal.php.dist | 31 ---------------- 3 files changed, 37 insertions(+), 32 deletions(-) delete mode 100644 tests/Gaufrette/Functional/adapters/DoctrineDbal.php.dist diff --git a/.travis.yml b/.travis.yml index 7afb7b528..6f6ed43c8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -54,7 +54,6 @@ before_script: - sudo chown gaufrette:gaufrette /home/gaufrette /srv/ftp - sudo cp .travis/vsftpd.conf /etc/vsftpd.conf - sudo service vsftpd restart - - cp tests/Gaufrette/Functional/adapters/DoctrineDbal.php.dist tests/Gaufrette/Functional/adapters/DoctrineDbal.php -f - composer self-update && composer install --prefer-source --no-interaction - if [[ ${TRAVIS_PHP_VERSION:0:1} == "5" ]]; then yes | pecl install ssh2 && composer require herzult/php-ssh; fi diff --git a/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php b/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php index 2684fffd1..e9d589ad8 100644 --- a/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php +++ b/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php @@ -2,8 +2,45 @@ namespace Gaufrette\Functional\Adapter; +use Doctrine\DBAL\DriverManager; +use Gaufrette\Adapter\DoctrineDbal; +use Gaufrette\Filesystem; + class DoctrineDbalTest extends FunctionalTestCase { + /** @var \Doctrine\DBAL\Connection */ + private $connection; + + public function setUp() + { + $this->connection = DriverManager::getConnection([ + 'driver' => 'pdo_sqlite', + 'memory' => true, + ]); + + $schema = $this->connection->getSchemaManager()->createSchema(); + + $table = $schema->createTable('gaufrette'); + $table->addColumn('key', 'string', array('unique' => true)); + $table->addColumn('content', 'blob'); + $table->addColumn('mtime', 'integer'); + $table->addColumn('checksum', 'string', array('length' => 32)); + + // Generates the SQL from the defined schema and execute each line + array_map([$this->connection, 'exec'], $schema->toSql($this->connection->getDatabasePlatform())); + + $this->filesystem = new Filesystem(new DoctrineDbal($this->connection, 'gaufrette')); + } + + public function tearDown() + { + $schemaManager = $this->connection->getSchemaManager(); + + if (in_array('gaufrette', $schemaManager->listTableNames())) { + $schemaManager->dropTable('gaufrette'); + } + } + /** * @test */ diff --git a/tests/Gaufrette/Functional/adapters/DoctrineDbal.php.dist b/tests/Gaufrette/Functional/adapters/DoctrineDbal.php.dist deleted file mode 100644 index d5a0dfb6c..000000000 --- a/tests/Gaufrette/Functional/adapters/DoctrineDbal.php.dist +++ /dev/null @@ -1,31 +0,0 @@ - 'pdo_sqlite', - 'memory' => true, -)); - -$platform = $connection->getDatabasePlatform(); - -$schemaManager = $connection->getSchemaManager(); - -if (in_array('gaufrette', $schemaManager->listTableNames())) { - $schemaManager->dropTable('gaufrette'); -} - -$schema = $schemaManager->createSchema(); - -$table = $schema->createTable('gaufrette'); -$table->addColumn('key', 'string', array('unique' => true)); -$table->addColumn('content', 'blob'); -$table->addColumn('mtime', 'integer'); -$table->addColumn('checksum', 'string', array('length' => 32)); - -foreach ($schema->toSql($platform) as $query) { - $connection->exec($query); -} - -return new DoctrineDbal($connection, 'gaufrette'); From 91d4577695624d9aaa4dc19fdef7ae3a8922b7df Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Wed, 14 Jun 2017 00:50:08 +0200 Subject: [PATCH 299/426] Add a functional test case for OpenCloud dapter --- .env.dist | 4 ++ phpunit.xml.dist | 5 ++ .../Functional/Adapter/OpenCloudTest.php | 48 +++++++++++++++++++ 3 files changed, 57 insertions(+) create mode 100644 tests/Gaufrette/Functional/Adapter/OpenCloudTest.php diff --git a/.env.dist b/.env.dist index 9b5769157..0face3dc2 100644 --- a/.env.dist +++ b/.env.dist @@ -19,3 +19,7 @@ FTP_BASE_DIR=/gaufrette/ AZURE_ACCOUNT= AZURE_KEY= AZURE_CONTAINER= + +RACKSPACE_USER= +RACKSPACE_APIKEY= +RACKSPACE_CONTAINER= diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 3da084d85..60c0cfd0b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -38,6 +38,11 @@ + + + diff --git a/tests/Gaufrette/Functional/Adapter/OpenCloudTest.php b/tests/Gaufrette/Functional/Adapter/OpenCloudTest.php new file mode 100644 index 000000000..322ebed0b --- /dev/null +++ b/tests/Gaufrette/Functional/Adapter/OpenCloudTest.php @@ -0,0 +1,48 @@ +markTestSkipped('Either RACKSPACE_USER, RACKSPACE_APIKEY and/or RACKSPACE_CONTAINER env vars are missing.'); + } + + $connection = new Rackspace('https://identity.api.rackspacecloud.com/v2.0/', [ + 'username' => $username, + 'apiKey' => $apiKey, + ]); + + $this->container = uniqid($container); + $this->objectStore = $connection->objectStoreService('cloudFiles', 'IAD', 'publicURL'); + $this->objectStore->createContainer($this->container); + + $adapter = new OpenCloud($this->objectStore, $this->container); + $this->filesystem = new Filesystem($adapter); + } + + public function tearDown() + { + if ($this->filesystem === null) { + return; + } + + $this->objectStore->getContainer($this->container)->delete(true); + } +} From f7cc969dc8f5f685038ee66e9c34e17fe677070c Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Wed, 14 Jun 2017 00:51:28 +0200 Subject: [PATCH 300/426] Converts lastModified string returned by OpenCloud SDK into timestamp --- spec/Gaufrette/Adapter/OpenCloudSpec.php | 16 ++++++++++++++++ src/Gaufrette/Adapter/OpenCloud.php | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/spec/Gaufrette/Adapter/OpenCloudSpec.php b/spec/Gaufrette/Adapter/OpenCloudSpec.php index dbbb2c405..be8a7ad87 100644 --- a/spec/Gaufrette/Adapter/OpenCloudSpec.php +++ b/spec/Gaufrette/Adapter/OpenCloudSpec.php @@ -6,6 +6,7 @@ use OpenCloud\Common\Exceptions\CreateUpdateError; use OpenCloud\Common\Exceptions\DeleteError; use OpenCloud\ObjectStore\Exception\ObjectNotFoundException; +use OpenCloud\ObjectStore\Resource\DataObject; use PhpSpec\ObjectBehavior; use Prophecy\Argument; @@ -236,4 +237,19 @@ function it_throws_exeption_if_container_creation_fails($objectStore) $this->shouldThrow('\RuntimeException')->duringExists('test'); } + + function it_returns_false_if_the_object_does_not_exists_when_fetching_mtime($container) + { + $container->getObject('foo')->willThrow(ObjectNotFoundException::class); + + $this->mtime('foo')->shouldReturn(false); + } + + function it_fetches_file_mtime(DataObject $object, $container) + { + $container->getObject('foo')->willReturn($object); + $object->getLastModified()->willReturn('Tue, 13 Jun 2017 22:02:34 GMT'); + + $this->mtime('foo')->shouldReturn('1497391354'); + } } diff --git a/src/Gaufrette/Adapter/OpenCloud.php b/src/Gaufrette/Adapter/OpenCloud.php index da060ede4..7d77d468f 100644 --- a/src/Gaufrette/Adapter/OpenCloud.php +++ b/src/Gaufrette/Adapter/OpenCloud.php @@ -161,7 +161,7 @@ public function keys() public function mtime($key) { if ($object = $this->tryGetObject($key)) { - return $object->getLastModified(); + return (new \DateTime($object->getLastModified()))->format('U'); } return false; From a8938133297439623da1431ea4c2c56d0837b653 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Wed, 14 Jun 2017 00:52:45 +0200 Subject: [PATCH 301/426] Compute length of content written by OpenCloud SDK The OpenCloud SDK is broken: the object returned by `uploadObject()` method contains a bad ContentLength. Thus the best is to compute the content size using `Util\Size::fromContent()` method. --- src/Gaufrette/Adapter/OpenCloud.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/Gaufrette/Adapter/OpenCloud.php b/src/Gaufrette/Adapter/OpenCloud.php index 7d77d468f..3877177bc 100644 --- a/src/Gaufrette/Adapter/OpenCloud.php +++ b/src/Gaufrette/Adapter/OpenCloud.php @@ -3,6 +3,7 @@ namespace Gaufrette\Adapter; use Gaufrette\Adapter; +use Gaufrette\Util; use Guzzle\Http\Exception\BadResponseException; use OpenCloud\Common\Exceptions\DeleteError; use OpenCloud\ObjectStore\Resource\Container; @@ -106,12 +107,12 @@ public function read($key) public function write($key, $content) { try { - $object = $this->getContainer()->uploadObject($key, $content); + $this->getContainer()->uploadObject($key, $content); } catch (CreateUpdateError $updateError) { return false; } - return $object->getContentLength(); + return Util\Size::fromContent($content); } /** From 4c808c86bc88bb75f1199fa7f647aa7553eda2a2 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Wed, 14 Jun 2017 00:56:20 +0200 Subject: [PATCH 302/426] Clean travis.yml --- .travis.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6f6ed43c8..8590a33aa 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,13 +6,6 @@ cache: directories: - $HOME/.composer/cache/files -env: - global: - - AWS_KEY: - - AWS_SECRET: - - AZURE_ACCOUNT: - - AZURE_KEY: - php: - 5.6 - 7.0 From 99569878f00efcbbb6274ff6b56a9ccb3d623b8d Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Wed, 14 Jun 2017 00:57:54 +0200 Subject: [PATCH 303/426] Add env vars used by OpenCloud functional test to appveyor config --- appveyor.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index f5d892c5c..8fa626cbd 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -14,6 +14,11 @@ environment: AWS_SECRET: secure: MiAf2q7mnqSIZSoChd9v3pwUrXHeAEpZP8Zc0E0qjiOBXPvplzJVtrkWRwTNlQl3 AWS_BUCKET: gaufretteci + RACKSPACE_USER: + secure: aEIZdm+IXe9dGLOnSmgu8A== + RACKSPACE_APIKEY: + secure: CqBlB4EVIM2FMaYxBdplTaQzw8JgKElxYvKYpXUXPHpOdwJcnYy0gk2KdjF4fXDM + RACKSPACE_CONTAINER: gaufretteci init: - SET PATH=C:\Program Files\OpenSSL;c:\tools\php;%PATH% From 9b3c96578f570152f6d7dcd744b943654065375a Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Wed, 14 Jun 2017 11:54:23 +0200 Subject: [PATCH 304/426] Disable flaky AzureMultiContainerBlobStorageTest for now --- .../Functional/Adapter/AzureMultiContainerBlobStorageTest.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php index 28cb82a26..1b9933626 100644 --- a/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php +++ b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php @@ -19,6 +19,8 @@ class AzureMultiContainerBlobStorageTest extends FunctionalTestCase public function setUp() { + $this->markTestSkipped(__CLASS__ . ' is flaky.'); + $account = getenv('AZURE_ACCOUNT'); $key = getenv('AZURE_KEY'); if (empty($account) || empty($key)) { From e0e8fbcb0d3646dea93722886c7fabdd7f9c3613 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Wed, 24 May 2017 16:16:16 +0200 Subject: [PATCH 305/426] Complete CHANGELOG for v.0.4.0 --- CHANGELOG.md | 35 +++++++++++++++++++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 165b7777d..a4bd966bf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,36 @@ v0.4 ==== -* Unmaintained `mongo` extension replaced with newer `mongodb` extension -* Support for PHP 5.4 & PHP 5.5 has been dropped +* Following adapters have been deprecated: AclAwareAmazonS3, AmazonS3, Apc, Cache, LazyOpenCloud, Sftp, Dropbox, MogileFS, GoogleCloudStorage (see #482) +* Improvement of test coverage during CI builds: functional tests for AzureBlobStorage, AwsS3, DoctrineDbal, Ftp, GridFS, OpenCloud and PhpseclibSftp now run on Travis (see #457, #460, #483, #484, #500, #504, #505) +* Maintained adapters now have metapackage to enforce version of 3rd party libraries, and ease installation process (see #487) +* Add FilesystemInterface and make current Filesystem implement it (see #492) +* Drop support for PHP v5.4 and v5.5 (see #503) +* File: + * Add rename method to File (see #468) +* Local adapter: + * Suppress warning if directory has been created between check and create attempt (see #331) + * Replace file_exists with is_file, to check if given path exists (see #479) + * Allow Local adapter mkdir mode to cascade to it's Stream (see #488) + * Fix phpdocs (see #489) +* AzureBlobStorage: + * Add support for multi container mode (see #486) +* AwsS3 adapter: + * Add ContentType support to AwsS3 (see #451) + * Allow aws-sdk-php v2 and v3 to be used (see #457, #462, #475) + * Provide mime type (see #491) + * Deprecate AwsS3::getUrl() method, instead use ResolvableFilesystem from [`gaufrette/extras`](https://github.com/Gaufrette/extras) (see #496) +* GridFS adapter: + * Unmaintained mongo extension has been replaced with newer mongodb extension (see #460) +* GoogleCloudStorage adapter: + * Fixed missing leading "\" before Google_Http_Request (see #471) +* Ftp adapter: + * Always ensure target directory exists before renaming (see #476) + * Don't use FTP_USEPASSVADDRESS before php 5.6.18, and 7.0.0/7.0.1 (see #477, #480, #483) +* Docs: + * Add minimum IAM roles for AwsS3 adapter, and recommend to manually create bucket (see #467) + + +Contributors: @NiR-, @nicolasmure, @WARrior-Alex, @zyphlar, @AntoineLelaisant, @Shivoham, @richardfullmer, @kcassam. + +Also, we thank @edhgoose and @zyphlar who made patches for deprecated adapters, before those adapters were deprecated, but still did not see their respective work merged in this version. From 119b6b4bf8a3f0e1553e3a100ccdff761bf01c1e Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Sat, 17 Jun 2017 12:40:32 +0200 Subject: [PATCH 306/426] Bump branch-alias to 0.5.x-dev --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 7f9293031..14e0ea797 100644 --- a/composer.json +++ b/composer.json @@ -68,7 +68,7 @@ }, "extra": { "branch-alias": { - "dev-master": "0.4.x-dev" + "dev-master": "0.5.x-dev" } } } From c43d8fb1b7570277f405dc74a0b819bd341af91a Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Sat, 17 Jun 2017 12:54:44 +0200 Subject: [PATCH 307/426] Add a note about deprecated adapters in the docs --- doc/adapters/amazon-s3.md | 2 ++ doc/adapters/apc.md | 2 ++ doc/adapters/open-cloud.md | 2 ++ doc/adapters/sftp.md | 2 ++ 4 files changed, 8 insertions(+) diff --git a/doc/adapters/amazon-s3.md b/doc/adapters/amazon-s3.md index cddfd3b42..db2edc6f7 100644 --- a/doc/adapters/amazon-s3.md +++ b/doc/adapters/amazon-s3.md @@ -4,6 +4,8 @@ currentMenu: amazon-s3 # AmazonS3 & AclAwareAmazonS3 +**Warning: This adapter has been deprecated since v0.4.0 and will be removed in v1.0.0. You should rather use the AwsS3 adapter.** + When using the legacy Amazon S3 adapters (aws-sdk-php < 2), you will need to specify a CA certificate to be able to talk to Amazon servers in https. You can use the one which is shipped with the SDK by defining before creating the diff --git a/doc/adapters/apc.md b/doc/adapters/apc.md index badd6888e..0ea013441 100644 --- a/doc/adapters/apc.md +++ b/doc/adapters/apc.md @@ -4,6 +4,8 @@ currentMenu: apc # APC +**Warning: This adapter has been deprecated since v0.4.0 and will be removed in v1.0.0.** + `apc` extension should be enabled in order to use this adapter. ## Example diff --git a/doc/adapters/open-cloud.md b/doc/adapters/open-cloud.md index f2791252e..00ef34f74 100644 --- a/doc/adapters/open-cloud.md +++ b/doc/adapters/open-cloud.md @@ -4,6 +4,8 @@ currentMenu: open-cloud # OpenCloud & LazyOpenCloud +**Warning: LazyOpenCloud adapter has been deprecated since v0.4.0 and will be removed in v1.0.0.** + First, you will need to install the adapter: ```bash composer require gaufrette/opencloud-adapter diff --git a/doc/adapters/sftp.md b/doc/adapters/sftp.md index 910f396be..6d716cee8 100644 --- a/doc/adapters/sftp.md +++ b/doc/adapters/sftp.md @@ -4,6 +4,8 @@ currentMenu: sftp # SFTP +**Warning: This adapter has been deprecated since v0.4.0 and will be removed in v1.0.0.** + *N.B.* SFTP adapter is not recommended to use due to https://bugs.php.net/bug.php?id=64169. It is recommended to use [phpseclib SFTP adapter](phpseclib-sftp.html) instead. From 396c5a7d3823a9e9792d60fa9b53872aacbfe462 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Mon, 19 Jun 2017 16:21:40 +0100 Subject: [PATCH 308/426] Fixed typo --- doc/caching.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/caching.md b/doc/caching.md index 80cfdf5da..529830456 100644 --- a/doc/caching.md +++ b/doc/caching.md @@ -6,7 +6,7 @@ currentMenu: caching If you have to deal with a slow filesystem, it is out of question to use it directly. So, you need a cache! Happily, Gaufrette offers a cache system ready for use. -It consist of an adapter decorator itself composed of two adapters: +It consists of an adapter decorator itself composed of two adapters: * The *source* adapter that should be cached * The *cache* adapter that is used to cache From 0ea59377b21058e2a1ff5b9af447966361a063a2 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Mon, 26 Jun 2017 22:59:14 +0200 Subject: [PATCH 309/426] Add a little note about the new dependency for zip extension --- doc/adapters/zip.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/doc/adapters/zip.md b/doc/adapters/zip.md index af5e6c2a2..ab6263919 100644 --- a/doc/adapters/zip.md +++ b/doc/adapters/zip.md @@ -9,6 +9,12 @@ First, you will need to install the adapter: composer require gaufrette/zip-adapter ``` +You need zip extension too: +```bash +sudo apt-get install libzip-dev # On Debian, Ubuntu, ... +sudo pecl install zip +``` + ## Example ```php From 0c630c2434022c05f44d0713d086d2ad9054f9c2 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Tue, 27 Jun 2017 00:11:54 +0200 Subject: [PATCH 310/426] Fix S3Client instantiation (for aws sdk v2) in the doc --- doc/adapters/aws-s3.md | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/doc/adapters/aws-s3.md b/doc/adapters/aws-s3.md index 5a2248931..5af8faa35 100644 --- a/doc/adapters/aws-s3.md +++ b/doc/adapters/aws-s3.md @@ -36,10 +36,8 @@ $s3client = new S3Client([ ]); // For aws-sdk-php v2 $s3client = S3Client::factory([ - 'credentials' => [ - 'key' => 'your_key_here', - 'secret' => 'your_secret', - ], + 'key' => 'your_key_here', + 'secret' => 'your_secret', 'version' => '2006-03-01', 'region' => 'eu-west-1', ]); From ba99b5022af34cd6ddf7e630df97d0cb93ead1b3 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Tue, 27 Jun 2017 00:46:14 +0200 Subject: [PATCH 311/426] Add a note about metapackages --- README.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/README.md b/README.md index 8d9011a1b..d31c6aca4 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,12 @@ solution. Read the official [Gaufrette documentation](http://knplabs.github.io/Gaufrette/). +### Metapackages for adapters + +Every maintained adapter now have a dedicated metapackage. You can [find the list on packagist](https://packagist.org/packages/gaufrette/). +**We highly recommend you to use them as they contain their own requirements**: you don't need to worry about third-party dependencies +to install before using Gaufrette anymore. + ### Symfony integration Symfony integration is available through [KnpLabs/KnpGaufretteBundle](https://github.com/KnpLabs/KnpGaufretteBundle). From 7012b054b253f770a9c3228044bdaa57d72cd465 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Tue, 27 Jun 2017 00:47:16 +0200 Subject: [PATCH 312/426] Add the list of maintainer(s) per adapter --- README.md | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/README.md b/README.md index d31c6aca4..843f5fd4f 100644 --- a/README.md +++ b/README.md @@ -40,6 +40,28 @@ to install before using Gaufrette anymore. Symfony integration is available through [KnpLabs/KnpGaufretteBundle](https://github.com/KnpLabs/KnpGaufretteBundle). +### Maintainers + +Here is the list of dedicated maintainer(s) for every adapter not deprecated. If you don't receive any response to +your issue or pull request in a timely manner, ping us: + +| Adapter | Referent | +|--------------------|-----------------------------| +| AwsS3 | @NiR- | +| AzureBlobStorage | @NiR- | +| DoctrineDbal | @pedrotroller, @NicolasNSSM | +| Flysystem | @nicolasmure | +| Ftp | @fabschurt | +| GoogleCloudStorage | @AntoineLelaisant | +| GridFS | @NiR- | +| InMemory | | +| Local | | +| OpenCloud | @NiR- | +| PhpseclibSftp | @fabschurt | +| Zip | | + +For `InMemory`, `Local` and `Zip` adapters everyone in this list is considered as a maintainer. + ### Launch the Test Suite Requires: From f606bc4fca9a9fb77a3ed965d726af5314a5594e Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Mon, 26 Jun 2017 21:03:18 +0200 Subject: [PATCH 313/426] Ignore errors during installation of ssh2 extension As mentioned in [this bug report](https://bugs.php.net/bug.php?id=74787), ssh2 v1.1 (and v1.1.1) does not provide the `tests/` folder, thus pecl refuses to install it. Closes #509. --- docker/Dockerfile-php71 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile-php71 b/docker/Dockerfile-php71 index 95eddd1ff..48c447bba 100644 --- a/docker/Dockerfile-php71 +++ b/docker/Dockerfile-php71 @@ -11,7 +11,8 @@ RUN apt-get update && \ unzip \ libzip-dev \ && rm -rf /var/lib/apt/lists/* && \ - pecl install mongodb ssh2-alpha zip && \ + pecl install mongodb zip && \ + pecl install --ignore-errors ssh2-alpha && \ echo 'extension=/usr/local/lib/php/extensions/no-debug-non-zts-20160303/mongodb.so' >> /usr/local/etc/php/conf.d/mongodb.ini && \ echo 'extension=/usr/local/lib/php/extensions/no-debug-non-zts-20160303/ssh2.so' >> /usr/local/etc/php/conf.d/ssh2.ini && \ echo 'extension=/usr/local/lib/php/extensions/no-debug-non-zts-20160303/zip.so' >> /usr/local/etc/php/conf.d/zip.ini From c85c99146674b72e35290a86d09a6e7606b1baf8 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Mon, 26 Jun 2017 22:54:48 +0200 Subject: [PATCH 314/426] Fix `tests` script Since the functional test for DoctrineDbal has been restructured, there's no more file to copy in order to test it (see b6797f74557). --- bin/tests | 4 ---- 1 file changed, 4 deletions(-) diff --git a/bin/tests b/bin/tests index 4180c0cb4..d647ea270 100755 --- a/bin/tests +++ b/bin/tests @@ -6,9 +6,5 @@ set -o xtrace DIR=$(dirname ${BASH_SOURCE[0]})/.. -cp \ - ${DIR}/tests/Gaufrette/Functional/adapters/DoctrineDbal.php.dist \ - ${DIR}/tests/Gaufrette/Functional/adapters/DoctrineDbal.php - ${DIR}/vendor/bin/phpspec run --format=pretty ${DIR}/vendor/bin/phpunit From 8448da0bdc2f1dc0a06cc2c78ba62fdacc1c6bbe Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Sun, 2 Jul 2017 23:45:46 +0200 Subject: [PATCH 315/426] Inherits FunctionalTestCase from AwsS3Test --- .../Functional/Adapter/AwsS3Test.php | 75 +++++++++---------- .../Functional/Adapter/FunctionalTestCase.php | 14 ++-- 2 files changed, 43 insertions(+), 46 deletions(-) diff --git a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php index 11921ecb4..fd4601543 100644 --- a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php +++ b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php @@ -6,10 +6,7 @@ use Gaufrette\Adapter\AwsS3; use Gaufrette\Filesystem; -/** - * @todo move to phpspec - */ -class AwsS3Test extends \PHPUnit_Framework_TestCase +class AwsS3Test extends FunctionalTestCase { /** @var int */ static private $SDK_VERSION; @@ -53,6 +50,8 @@ public function setUp() 'secret' => $secret, ]); } + + $this->createFilesystem(['create' => true]); } public function tearDown() @@ -62,18 +61,21 @@ public function tearDown() } $result = $this->client->listObjects(['Bucket' => $this->bucket]); - $staleObjects = $result->get('Contents'); - foreach ($staleObjects as $staleObject) { + if (!$result->hasKey('Contents')) { + return; + } + + foreach ($result->get('Contents') as $staleObject) { $this->client->deleteObject(['Bucket' => $this->bucket, 'Key' => $staleObject['Key']]); } $this->client->deleteBucket(['Bucket' => $this->bucket]); } - private function getFilesystem(array $adapterOptions = []) + private function createFilesystem(array $adapterOptions = []) { - return new Filesystem(new AwsS3($this->client, $this->bucket, $adapterOptions)); + $this->filesystem = new Filesystem(new AwsS3($this->client, $this->bucket, $adapterOptions)); } /** @@ -81,81 +83,76 @@ private function getFilesystem(array $adapterOptions = []) */ public function testThrowsExceptionIfBucketMissingAndNotCreating() { - $filesystem = $this->getFilesystem(); - $filesystem->read('foo'); + $this->createFilesystem(); + $this->filesystem->read('foo'); } public function testWritesObjects() { - $filesystem = $this->getFilesystem(['create' => true]); - $this->assertEquals(7, $filesystem->write('foo', 'testing')); + $this->assertEquals(7, $this->filesystem->write('foo', 'testing')); } public function testChecksForObjectExistence() { - $filesystem = $this->getFilesystem(['create' => true]); - $filesystem->write('foo', ''); - $this->assertTrue($filesystem->has('foo')); + $this->filesystem->write('foo', ''); + $this->assertTrue($this->filesystem->has('foo')); } public function testGetsObjectUrls() { - $filesystem = $this->getFilesystem(['create' => true]); - $this->assertNotEmpty($filesystem->getAdapter()->getUrl('foo')); + $this->assertNotEmpty($this->filesystem->getAdapter()->getUrl('foo')); } public function testChecksForObjectExistenceWithDirectory() { - $filesystem = $this->getFilesystem(['directory' => 'bar', 'create' => true]); - $filesystem->write('foo', ''); + $this->createFilesystem(['directory' => 'bar', 'create' => true]); + $this->filesystem->write('foo', ''); - $this->assertTrue($filesystem->has('foo')); + $this->assertTrue($this->filesystem->has('foo')); } public function testGetsObjectUrlsWithDirectory() { - $filesystem = $this->getFilesystem(['directory' => 'bar']); - $this->assertNotEmpty($filesystem->getAdapter()->getUrl('foo')); + $this->createFilesystem(['directory' => 'bar']); + $this->assertNotEmpty($this->filesystem->getAdapter()->getUrl('foo')); } public function testListKeysWithoutDirectory() { - $filesystem = $this->getFilesystem(['create' => true]); - $filesystem->write('test.txt', 'some content'); - $this->assertEquals(['test.txt'], $filesystem->listKeys()); + $this->assertEquals([], $this->filesystem->listKeys()); + $this->filesystem->write('test.txt', 'some content'); + $this->assertEquals(['test.txt'], $this->filesystem->listKeys()); } public function testListKeysWithDirectory() { - $filesystem = $this->getFilesystem(['create' => true, 'directory' => 'root/']); - $filesystem->write('test.txt', 'some content'); - $this->assertEquals(['test.txt'], $filesystem->listKeys()); - $this->assertTrue($filesystem->has('test.txt')); + $this->createFilesystem(['create' => true, 'directory' => 'root/']); + $this->filesystem->write('test.txt', 'some content'); + $this->assertEquals(['test.txt'], $this->filesystem->listKeys()); + $this->assertTrue($this->filesystem->has('test.txt')); } public function testKeysWithoutDirectory() { - $filesystem = $this->getFilesystem(['create' => true]); - $filesystem->write('test.txt', 'some content'); - $this->assertEquals(['test.txt'], $filesystem->keys()); + $this->filesystem->write('test.txt', 'some content'); + $this->assertEquals(['test.txt'], $this->filesystem->keys()); } public function testKeysWithDirectory() { - $filesystem = $this->getFilesystem(['create' => true, 'directory' => 'root/']); - $filesystem->write('test.txt', 'some content'); - $this->assertEquals(['test.txt'], $filesystem->keys()); + $this->createFilesystem(['create' => true, 'directory' => 'root/']); + $this->filesystem->write('test.txt', 'some content'); + $this->assertEquals(['test.txt'], $this->filesystem->keys()); } public function testUploadWithGivenContentType() { - $filesystem = $this->getFilesystem(['create' => true]); /** @var AwsS3 $adapter */ - $adapter = $filesystem->getAdapter(); + $adapter = $this->filesystem->getAdapter(); $adapter->setMetadata('foo', ['ContentType' => 'text/html']); - $filesystem->write('foo', ''); + $this->filesystem->write('foo', ''); - $this->assertEquals('text/html', $filesystem->mimeType('foo')); + $this->assertEquals('text/html', $this->filesystem->mimeType('foo')); } } diff --git a/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php b/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php index ba9b68c6a..663a83b6d 100644 --- a/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php +++ b/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php @@ -197,19 +197,19 @@ public function shouldWorkWithHiddenFiles() $this->filesystem->delete('.foo'); $this->assertFalse($this->filesystem->has('.foo')); } - + /** * @test * @group functional */ public function shouldKeepFileObjectInRegister() { - $FileObjectA = $this->filesystem->createFile('somefile'); + $FileObjectA = $this->filesystem->createFile('somefile'); $FileObjectB = $this->filesystem->createFile('somefile'); - + $this->assertTrue($FileObjectA === $FileObjectB); } - + /** * @test * @group functional @@ -218,12 +218,12 @@ public function shouldWrtieToSameFile() { $FileObjectA = $this->filesystem->createFile('somefile'); $FileObjectA->setContent('ABC'); - + $FileObjectB = $this->filesystem->createFile('somefile'); $FileObjectB->setContent('DEF'); - + $this->assertEquals('DEF', $FileObjectB->getContent()); - + $this->filesystem->delete('somefile'); } } From f7e9c91d3976339c4fed707e34d4559757213e4e Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Mon, 3 Jul 2017 00:06:52 +0200 Subject: [PATCH 316/426] AwsS3::listKeys() was generating an error if the bucket did not exist --- src/Gaufrette/Adapter/AwsS3.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index fec65a871..b39feb471 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -228,6 +228,8 @@ public function keys() */ public function listKeys($prefix = '') { + $this->ensureBucketExists(); + $options = ['Bucket' => $this->bucket]; if ((string) $prefix != '') { $options['Prefix'] = $this->computePath($prefix); From be5adec544c817121bf0046532dc4f314a90b86e Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Sun, 2 Jul 2017 23:46:23 +0200 Subject: [PATCH 317/426] Fix build of php70 image --- docker/Dockerfile-php70 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docker/Dockerfile-php70 b/docker/Dockerfile-php70 index fc6f95701..c0cf1b1d7 100644 --- a/docker/Dockerfile-php70 +++ b/docker/Dockerfile-php70 @@ -11,7 +11,8 @@ RUN apt-get update && \ unzip \ libzip-dev \ && rm -rf /var/lib/apt/lists/* && \ - pecl install mongodb ssh2-alpha zip && \ + pecl install mongodb zip && \ + pecl install --ignore-errors ssh2-alpha && \ echo 'extension=/usr/local/lib/php/extensions/no-debug-non-zts-20151012/mongodb.so' >> /usr/local/etc/php/conf.d/mongodb.ini && \ echo 'extension=/usr/local/lib/php/extensions/no-debug-non-zts-20151012/ssh2.so' >> /usr/local/etc/php/conf.d/ssh2.ini && \ echo 'extension=/usr/local/lib/php/extensions/no-debug-non-zts-20151012/zip.so' >> /usr/local/etc/php/conf.d/zip.ini From 5d06478abb60a9bc55927624f3e11889a1893736 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Sun, 2 Jul 2017 23:57:00 +0200 Subject: [PATCH 318/426] Fix some phpdocs --- src/Gaufrette/Adapter/FileFactory.php | 1 + src/Gaufrette/Adapter/Local.php | 31 +++++++++++++-------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/Gaufrette/Adapter/FileFactory.php b/src/Gaufrette/Adapter/FileFactory.php index b11c2b59e..f71be89f0 100644 --- a/src/Gaufrette/Adapter/FileFactory.php +++ b/src/Gaufrette/Adapter/FileFactory.php @@ -2,6 +2,7 @@ namespace Gaufrette\Adapter; +use Gaufrette\File; use Gaufrette\Filesystem; /** diff --git a/src/Gaufrette/Adapter/Local.php b/src/Gaufrette/Adapter/Local.php index 0e09c7cc2..b25a725b7 100644 --- a/src/Gaufrette/Adapter/Local.php +++ b/src/Gaufrette/Adapter/Local.php @@ -47,7 +47,7 @@ public function __construct($directory, $create = false, $mode = 0777) /** * {@inheritdoc} * - * @throws \OutOfBoundsException If the computed path is out of the directory + * @throws \OutOfBoundsException If the computed path is out of the directory * @throws \InvalidArgumentException if the directory already exists * @throws \RuntimeException if the directory could not be created */ @@ -59,7 +59,7 @@ public function read($key) /** * {@inheritdoc} * - * @throws \OutOfBoundsException If the computed path is out of the directory + * @throws \OutOfBoundsException If the computed path is out of the directory * @throws \InvalidArgumentException if the directory already exists * @throws \RuntimeException if the directory could not be created */ @@ -74,7 +74,7 @@ public function write($key, $content) /** * {@inheritdoc} * - * @throws \OutOfBoundsException If the computed path is out of the directory + * @throws \OutOfBoundsException If the computed path is out of the directory * @throws \InvalidArgumentException if the directory already exists * @throws \RuntimeException if the directory could not be created */ @@ -97,7 +97,7 @@ public function exists($key) /** * {@inheritdoc} * - * @throws \OutOfBoundsException If the computed path is out of the directory + * @throws \OutOfBoundsException If the computed path is out of the directory * @throws \InvalidArgumentException if the directory already exists * @throws \RuntimeException if the directory could not be created */ @@ -129,7 +129,7 @@ public function keys() /** * {@inheritdoc} * - * @throws \OutOfBoundsException If the computed path is out of the directory + * @throws \OutOfBoundsException If the computed path is out of the directory * @throws \InvalidArgumentException if the directory already exists * @throws \RuntimeException if the directory could not be created */ @@ -141,7 +141,7 @@ public function mtime($key) /** * {@inheritdoc} * - * @throws \OutOfBoundsException If the computed path is out of the directory + * @throws \OutOfBoundsException If the computed path is out of the directory * @throws \InvalidArgumentException if the directory already exists * @throws \RuntimeException if the directory could not be created */ @@ -159,7 +159,7 @@ public function delete($key) * * @return bool * - * @throws \OutOfBoundsException If the computed path is out of the directory + * @throws \OutOfBoundsException If the computed path is out of the directory * @throws \InvalidArgumentException if the directory already exists * @throws \RuntimeException if the directory could not be created */ @@ -171,7 +171,7 @@ public function isDirectory($key) /** * {@inheritdoc} * - * @throws \OutOfBoundsException If the computed path is out of the directory + * @throws \OutOfBoundsException If the computed path is out of the directory * @throws \InvalidArgumentException if the directory already exists * @throws \RuntimeException if the directory could not be created */ @@ -183,7 +183,7 @@ public function createStream($key) /** * {@inheritdoc} * - * @throws \OutOfBoundsException If the computed path is out of the directory + * @throws \OutOfBoundsException If the computed path is out of the directory * @throws \InvalidArgumentException if the directory already exists * @throws \RuntimeException if the directory could not be created */ @@ -195,7 +195,7 @@ public function checksum($key) /** * {@inheritdoc} * - * @throws \OutOfBoundsException If the computed path is out of the directory + * @throws \OutOfBoundsException If the computed path is out of the directory * @throws \InvalidArgumentException if the directory already exists * @throws \RuntimeException if the directory could not be created */ @@ -207,7 +207,7 @@ public function size($key) /** * {@inheritdoc} * - * @throws \OutOfBoundsException If the computed path is out of the directory + * @throws \OutOfBoundsException If the computed path is out of the directory * @throws \InvalidArgumentException if the directory already exists * @throws \RuntimeException if the directory could not be created */ @@ -224,7 +224,7 @@ public function mimeType($key) * @param $path * @return string * - * @throws \OutOfBoundsException If the computed path is out of the directory + * @throws \OutOfBoundsException If the computed path is out of the directory * @throws \InvalidArgumentException if the directory already exists * @throws \RuntimeException if the directory could not be created */ @@ -242,10 +242,9 @@ public function computeKey($path) * * @return string A path * - * @throws \InvalidArgumentException if the directory already exists - * @throws \OutOfBoundsException If the computed path is out of the - * directory - * @throws \RuntimeException If directory does not exists and cannot be created + * @throws \InvalidArgumentException If the directory already exists + * @throws \OutOfBoundsException If the computed path is out of the directory + * @throws \RuntimeException If directory does not exists and cannot be created */ protected function computePath($key) { From ba8e1c443dbd5502fc6267d93988390d02d3d714 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Mon, 3 Jul 2017 01:33:58 +0200 Subject: [PATCH 319/426] Deduplicate tearDown logic from SafeLocalTest --- .../Functional/Adapter/SafeLocalTest.php | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/tests/Gaufrette/Functional/Adapter/SafeLocalTest.php b/tests/Gaufrette/Functional/Adapter/SafeLocalTest.php index cd233ab90..257cabce5 100644 --- a/tests/Gaufrette/Functional/Adapter/SafeLocalTest.php +++ b/tests/Gaufrette/Functional/Adapter/SafeLocalTest.php @@ -4,6 +4,7 @@ use Gaufrette\Filesystem; use Gaufrette\Adapter\SafeLocal; +use Gaufrette\Functional\LocalDirectoryDeletor; class SafeLocalTest extends FunctionalTestCase { @@ -20,23 +21,7 @@ public function tearDown() { $this->filesystem = null; - if (file_exists($this->getDirectory())) { - $iterator = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator( - $this->getDirectory(), - \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS - ), - \RecursiveIteratorIterator::CHILD_FIRST - ); - - foreach ($iterator as $item) { - if ($item->isDir()) { - rmdir(strval($item)); - } else { - unlink(strval($item)); - } - } - } + LocalDirectoryDeletor::deleteDirectory($this->getDirectory()); } private function getDirectory() From 2d1638a88593b8b916a5ae83c3d19121b82c88c9 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Fri, 9 Jun 2017 11:31:37 +0200 Subject: [PATCH 320/426] Run Azure tests on appveyor --- appveyor.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index 8fa626cbd..d3152f5ca 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -19,6 +19,11 @@ environment: RACKSPACE_APIKEY: secure: CqBlB4EVIM2FMaYxBdplTaQzw8JgKElxYvKYpXUXPHpOdwJcnYy0gk2KdjF4fXDM RACKSPACE_CONTAINER: gaufretteci + AZURE_ACCOUNT: + secure: s6JCZ8ztVGvVYprceONZOg== + AZURE_KEY: + secure: 6+9kd9n/BO65LI8ZCJJXOwo9v5QmMULKWExHgECe+rnflaw+ZiQ8jFfHwxxjOfUF1qulAyxHQmDj7M3Cfvbe/DAASjk52ngEMvHC6wv2/Eo0cBMQiUlS9vPeUlvkUy1L + AZURE_CONTAINER: gaufretteci init: - SET PATH=C:\Program Files\OpenSSL;c:\tools\php;%PATH% From 9713649528d48bb504e1080bbe445cdde46581d2 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Fri, 9 Jun 2017 11:45:02 +0200 Subject: [PATCH 321/426] Fix appveyor --- appveyor.yml | 44 ++++++++++++++++++++++++++++++-------------- 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index d3152f5ca..1e20cd9bb 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,6 +7,7 @@ branches: cache: - '%LOCALAPPDATA%\Composer\files' + - c:\php -> appveyor.yml environment: AWS_KEY: @@ -24,25 +25,40 @@ environment: AZURE_KEY: secure: 6+9kd9n/BO65LI8ZCJJXOwo9v5QmMULKWExHgECe+rnflaw+ZiQ8jFfHwxxjOfUF1qulAyxHQmDj7M3Cfvbe/DAASjk52ngEMvHC6wv2/Eo0cBMQiUlS9vPeUlvkUy1L AZURE_CONTAINER: gaufretteci + MONGO_URI: "mongodb://127.0.0.1:27017" + MONGO_DBNAME: "gridfs_test" + +services: + - mongodb init: - - SET PATH=C:\Program Files\OpenSSL;c:\tools\php;%PATH% + - SET PATH=C:\Program Files\OpenSSL;c:\php;%PATH% + - SET PHP=1 install: - - cinst -y OpenSSL.Light - - cinst --allow-empty-checksums -y php -version 5.6.17 - - cd c:\tools\php - - copy php.ini-production php.ini /Y - - echo extension_dir=ext >> php.ini - - echo date.timezone="UTC" >> php.ini - - echo extension=php_openssl.dll >> php.ini + - IF EXIST c:\php (SET PHP=0) ELSE (mkdir c:\php) + - cd c:\php + - IF %PHP%==1 cinst -y OpenSSL.Light + - IF %PHP%==1 appveyor DownloadFile http://windows.php.net/downloads/releases/archives/php-5.6.17-nts-Win32-VC11-x86.zip + - IF %PHP%==1 7z x php-5.6.17-nts-Win32-VC11-x86.zip -y >nul + - IF %PHP%==1 del /Q *.zip + - IF %PHP%==1 cd ext + - IF %PHP%==1 appveyor DownloadFile http://windows.php.net/downloads/pecl/releases/mongodb/1.2.9/php_mongodb-1.2.9-5.6-nts-vc11-x86.zip + - IF %PHP%==1 7z x php_mongodb-1.2.9-5.6-nts-vc11-x86.zip php_mongodb.dll -y >nul + - IF %PHP%==1 del /Q *.zip + - IF %PHP%==1 cd .. + - IF %PHP%==1 copy php.ini-production php.ini /Y + - IF %PHP%==1 echo extension_dir=ext >> php.ini + - IF %PHP%==1 echo date.timezone="UTC" >> php.ini + - IF %PHP%==1 echo extension=php_openssl.dll >> php.ini + - IF %PHP%==1 echo extension=php_mongodb.dll >> php.ini + - IF %PHP%==1 echo extension=php_pdo_sqlite.dll >> php.ini + - IF %PHP%==1 echo extension=php_fileinfo.dll >> php.ini - cd C:\projects\gaufrette - - php -r "readfile('http://getcomposer.org/installer');" | php - - php composer.phar install --prefer-source --no-interaction --ignore-platform-reqs - - php -m - - php -i + - appveyor DownloadFile https://getcomposer.org/composer.phar + - php composer.phar install --prefer-dist --no-interaction --no-progress --ansi test_script: - cd C:\projects\gaufrette - - php vendor\bin\phpspec.bat run -fpretty --verbose - - php vendor\bin\phpunit.bat + - vendor\bin\phpspec run -fpretty --verbose + - vendor\bin\phpunit -c phpunit.xml.dist From 8e60501de644e7aa65a657dca3ba56ae16cdae78 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Wed, 14 Jun 2017 01:03:43 +0200 Subject: [PATCH 322/426] Skip test related to symlink on windows --- tests/Gaufrette/Functional/Adapter/LocalTest.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/Gaufrette/Functional/Adapter/LocalTest.php b/tests/Gaufrette/Functional/Adapter/LocalTest.php index 0730550c9..c1947f96b 100644 --- a/tests/Gaufrette/Functional/Adapter/LocalTest.php +++ b/tests/Gaufrette/Functional/Adapter/LocalTest.php @@ -34,6 +34,10 @@ public function tearDown() */ public function shouldWorkWithSyslink() { + if (strtolower(substr(PHP_OS, 0, 3)) === 'win') { + $this->markTestSkipped('Symlinks are not supported on Windows.'); + } + $dirname = sprintf( '%s/adapters/aaa', dirname(__DIR__) From 093f8a79733ac2a2706d0d8ed23558e4ebb3535a Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Wed, 14 Jun 2017 08:35:18 +0200 Subject: [PATCH 323/426] Concrete TestCases are responsible for cleaning up storage during tearDown --- .../Functional/Adapter/FunctionalTestCase.php | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php b/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php index 663a83b6d..48f60dd06 100644 --- a/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php +++ b/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php @@ -67,8 +67,6 @@ public function shouldWriteAndRead() $this->assertEquals('Some content', $this->filesystem->read('foo')); $this->assertEquals('Some content1', $this->filesystem->read('test/subdir/foo')); - $this->filesystem->delete('foo'); - $this->filesystem->delete('test/subdir/foo'); } /** @@ -81,7 +79,6 @@ public function shouldUpdateFileContent() $this->filesystem->write('foo', 'Some content updated', true); $this->assertEquals('Some content updated', $this->filesystem->read('foo')); - $this->filesystem->delete('foo'); } /** @@ -97,8 +94,6 @@ public function shouldCheckIfFileExists() $this->assertTrue($this->filesystem->has('foo')); $this->assertFalse($this->filesystem->has('test/somefile')); $this->assertFalse($this->filesystem->has('test/somefile')); - - $this->filesystem->delete('foo'); } /** @@ -110,8 +105,6 @@ public function shouldGetMtime() $this->filesystem->write('foo', 'Some content'); $this->assertGreaterThan(0, $this->filesystem->mtime('foo')); - - $this->filesystem->delete('foo'); } /** @@ -143,7 +136,6 @@ public function shouldRenameFile() $this->assertFalse($this->filesystem->has('somedir/sub/foo')); $this->assertEquals('Some content', $this->filesystem->read('somedir/sub/boo')); - $this->filesystem->delete('somedir/sub/boo'); } /** @@ -179,10 +171,6 @@ public function shouldFetchKeys() foreach (array('foo', 'bar', 'baz') as $key) { $this->assertContains($key, $actualKeys); } - - $this->filesystem->delete('foo'); - $this->filesystem->delete('bar'); - $this->filesystem->delete('baz'); } /** @@ -223,7 +211,5 @@ public function shouldWrtieToSameFile() $FileObjectB->setContent('DEF'); $this->assertEquals('DEF', $FileObjectB->getContent()); - - $this->filesystem->delete('somefile'); } } From 053f38ee6ea0c516e74c73af6a93e96943f45d98 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Wed, 14 Jun 2017 08:51:22 +0200 Subject: [PATCH 324/426] Skip ZipTest and warn about instability of Zip adapter on Windows --- doc/adapters/zip.md | 2 ++ tests/Gaufrette/Functional/Adapter/ZipTest.php | 2 ++ 2 files changed, 4 insertions(+) diff --git a/doc/adapters/zip.md b/doc/adapters/zip.md index ab6263919..ebe02d397 100644 --- a/doc/adapters/zip.md +++ b/doc/adapters/zip.md @@ -15,6 +15,8 @@ sudo apt-get install libzip-dev # On Debian, Ubuntu, ... sudo pecl install zip ``` +**Warning: this adapter is buggy under Windows.** + ## Example ```php diff --git a/tests/Gaufrette/Functional/Adapter/ZipTest.php b/tests/Gaufrette/Functional/Adapter/ZipTest.php index 5cbff7103..cc59d4e72 100644 --- a/tests/Gaufrette/Functional/Adapter/ZipTest.php +++ b/tests/Gaufrette/Functional/Adapter/ZipTest.php @@ -11,6 +11,8 @@ public function setUp() { if (!extension_loaded('zip')) { return $this->markTestSkipped('The zip extension is not available.'); + } elseif (strtolower(substr(PHP_OS, 0, 3)) === 'win') { + $this->markTestSkipped('Zip adapter is not supported on Windows.'); } @touch(__DIR__ . '/test.zip'); From 1eb0ef803149396ed1c69c65ccb3883ede244f03 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Wed, 14 Jun 2017 09:49:31 +0200 Subject: [PATCH 325/426] Fix stream-related bugs under windows --- tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php | 4 ++++ tests/Gaufrette/Functional/FileStream/LocalTest.php | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php b/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php index 1eba3c991..7575da3b8 100644 --- a/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php +++ b/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php @@ -168,6 +168,10 @@ public function shouldHandlesSubDir() */ public function shouldUnlinkFile() { + if (strtolower(substr(PHP_OS, 0, 3)) === 'win') { + $this->markTestSkipped('Flaky test on windows.'); + } + $this->filesystem->write('test.txt', 'some content'); unlink('gaufrette://filestream/test.txt'); diff --git a/tests/Gaufrette/Functional/FileStream/LocalTest.php b/tests/Gaufrette/Functional/FileStream/LocalTest.php index b6b6b4de3..2635d3317 100644 --- a/tests/Gaufrette/Functional/FileStream/LocalTest.php +++ b/tests/Gaufrette/Functional/FileStream/LocalTest.php @@ -22,6 +22,10 @@ public function setUp() public function testDirectoryChmod() { + if (strtolower(substr(PHP_OS, 0, 3)) === 'win') { + $this->markTestSkipped('Chmod and umask are not available on Windows.'); + } + $r = fopen('gaufrette://filestream/foo/bar', 'a+'); fclose($r); From 951940ea8673cf16cd7fcac8795f2834720cf1b7 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Tue, 27 Jun 2017 23:50:28 +0200 Subject: [PATCH 326/426] Execute phpunit tests on appveyor with verbose mode --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 1e20cd9bb..0f5052f28 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -61,4 +61,4 @@ install: test_script: - cd C:\projects\gaufrette - vendor\bin\phpspec run -fpretty --verbose - - vendor\bin\phpunit -c phpunit.xml.dist + - vendor\bin\phpunit --verbose -c phpunit.xml.dist From c8495059f786cbbbf91ef9e540c587e40298c84e Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Wed, 28 Jun 2017 00:09:11 +0200 Subject: [PATCH 327/426] Enable php_curl extension on appveyor --- appveyor.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/appveyor.yml b/appveyor.yml index 0f5052f28..6c7c41d56 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -42,6 +42,7 @@ install: - IF %PHP%==1 appveyor DownloadFile http://windows.php.net/downloads/releases/archives/php-5.6.17-nts-Win32-VC11-x86.zip - IF %PHP%==1 7z x php-5.6.17-nts-Win32-VC11-x86.zip -y >nul - IF %PHP%==1 del /Q *.zip + - IF %PHP%==1 appveyor DownloadFile http://curl.haxx.se/ca/cacert.pem - IF %PHP%==1 cd ext - IF %PHP%==1 appveyor DownloadFile http://windows.php.net/downloads/pecl/releases/mongodb/1.2.9/php_mongodb-1.2.9-5.6-nts-vc11-x86.zip - IF %PHP%==1 7z x php_mongodb-1.2.9-5.6-nts-vc11-x86.zip php_mongodb.dll -y >nul @@ -54,6 +55,8 @@ install: - IF %PHP%==1 echo extension=php_mongodb.dll >> php.ini - IF %PHP%==1 echo extension=php_pdo_sqlite.dll >> php.ini - IF %PHP%==1 echo extension=php_fileinfo.dll >> php.ini + - IF %PHP%==1 echo extension=php_curl.dll >> php.ini + - IF %PHP%==1 echo openssl.cafile=c:\php\cacert.pem >> php.ini - cd C:\projects\gaufrette - appveyor DownloadFile https://getcomposer.org/composer.phar - php composer.phar install --prefer-dist --no-interaction --no-progress --ansi From 9a9b05a9583c6a5b3fae15aabbee75b333c46421 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Fri, 7 Jul 2017 00:25:59 +0200 Subject: [PATCH 328/426] Fix incomplete tear down phase for AwsS3Test Since some days functionnal tests for AwsS3 are broken from time to time. That's because the adapter creates buckets never deleted during tear down phase and because AWS allows at most 100 buckets at the same time. --- tests/Gaufrette/Functional/Adapter/AwsS3Test.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php index fd4601543..e641ce520 100644 --- a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php +++ b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php @@ -63,6 +63,7 @@ public function tearDown() $result = $this->client->listObjects(['Bucket' => $this->bucket]); if (!$result->hasKey('Contents')) { + $this->client->deleteBucket(['Bucket' => $this->bucket]); return; } From 6fd9bddbb13fc666d68c8c11811d5385a57466ca Mon Sep 17 00:00:00 2001 From: Gerke Stam Date: Tue, 15 Aug 2017 11:20:46 +0200 Subject: [PATCH 329/426] Added support for calculated size for Azure Blob Storage --- src/Gaufrette/Adapter/AzureBlobStorage.php | 24 ++++++++++++++++++- .../AzureMultiContainerBlobStorageTest.php | 13 ++++++++++ 2 files changed, 36 insertions(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/AzureBlobStorage.php b/src/Gaufrette/Adapter/AzureBlobStorage.php index 9a43291aa..a1935a30a 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage.php @@ -20,7 +20,9 @@ * @author Paweł Czyżewski */ class AzureBlobStorage implements Adapter, - MetadataSupporter + MetadataSupporter, + SizeCalculator + { /** * Error constants. @@ -306,6 +308,26 @@ public function mtime($key) } } + /** + * {@inheritdoc} + */ + public function size($key) + { + $this->init(); + list($containerName, $key) = $this->tokenizeKey($key); + + try { + $properties = $this->blobProxy->getBlobProperties($containerName, $key); + + return $properties->getProperties()->getContentLength(); + } catch (ServiceException $e) { + $this->failIfContainerNotFound($e, sprintf('read content length for key "%s"', $key), $containerName); + + return false; + } + + } + /** * {@inheritdoc} * @throws \RuntimeException diff --git a/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php index 1b9933626..26d9ae41f 100644 --- a/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php +++ b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php @@ -95,6 +95,19 @@ public function shouldGetMtime() $this->assertGreaterThan(0, $this->filesystem->mtime($path)); } + /** + * @test + * @group functional + */ + public function shouldGetSize() + { + $path = $this->createUniqueContainerName('container') . '/foo'; + + $this->filesystem->write($path, 'Some content'); + + $this->assertGreaterThan(11, $this->filesystem->size($path)); + } + /** * @test * @group functional From 385e7f75b46b66c2b6b620428b94a7579dbcb833 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Parmentier?= Date: Wed, 13 Sep 2017 11:59:07 +0200 Subject: [PATCH 330/426] Fix FTP tests --- .travis.yml | 23 +++++++++---------- .travis/vsftpd.conf | 12 ---------- src/Gaufrette/Adapter/Ftp.php | 2 +- .../Gaufrette/Functional/Adapter/FtpTest.php | 5 +++- .../Functional/Adapter/PhpseclibSftpTest.php | 3 +-- 5 files changed, 17 insertions(+), 28 deletions(-) delete mode 100644 .travis/vsftpd.conf diff --git a/.travis.yml b/.travis.yml index 8590a33aa..9cac8398b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: php -sudo: true +sudo: required cache: directories: @@ -14,15 +14,18 @@ php: services: - mongodb + - docker env: - MONGO_URI="mongodb://127.0.0.1:27017" MONGO_DBNAME="gridfs_test" SFTP_HOST=localhost + SFTP_PORT=2222 SFTP_USER=gaufrette SFTP_PASSWORD=gaufrette - SFTP_BASE_DIR=/home/gaufrette + SFTP_BASE_DIR=gaufrette FTP_HOST=localhost + FTP_PORT=21 FTP_USER=gaufrette FTP_PASSWORD=gaufrette FTP_BASE_DIR=/gaufrette/ @@ -39,16 +42,12 @@ before_script: else composer require mongofill/mongofill --ignore-platform-reqs; fi - - sudo apt-get update - - sudo apt-get install -y libssh2-1-dev vsftpd - - sudo useradd -d /home/./gaufrette gaufrette # Removing "./" from this path would break any further Travis builds - - echo "gaufrette:gaufrette" | sudo chpasswd - - sudo mkdir /home/gaufrette /srv/empty - - sudo chown gaufrette:gaufrette /home/gaufrette /srv/ftp - - sudo cp .travis/vsftpd.conf /etc/vsftpd.conf - - sudo service vsftpd restart - - composer self-update && composer install --prefer-source --no-interaction - - if [[ ${TRAVIS_PHP_VERSION:0:1} == "5" ]]; then yes | pecl install ssh2 && composer require herzult/php-ssh; fi + - docker run -p 22:${SFTP_PORT} -d atmoz/sftp:alpine ${SFTP_USER}:${SFTP_PASSWORD}:::${SFTP_BASE_DIR} + - docker run -d --name ftpd_server -p 21:${FTP_PORT} -p 30000-30009:30000-30009 -e "PUBLICHOST=${FTP_HOST}" stilliard/pure-ftpd + - docker exec -it ftpd_server sh -c "(echo ${FTP_PASSWORD}; echo ${FTP_PASSWORD}) | pure-pw useradd ${FTP_USER} -f /etc/pure-ftpd/passwd/pureftpd.passwd -m -u ftpuser -d /home/ftpusers/${FTP_USER}" + - if [[ ${TRAVIS_PHP_VERSION} = 5.* ]]; then sudo apt-get -qq update && sudo apt-get install -y libssh2-1-dev; fi + - if [[ ${TRAVIS_PHP_VERSION} = 5.* ]]; then pecl install ssh2 && composer require herzult/php-ssh --no-update --no-scripts; fi + - composer update --prefer-dist --no-progress --no-suggest --ansi script: - php vendor/bin/phpspec run -fpretty --verbose diff --git a/.travis/vsftpd.conf b/.travis/vsftpd.conf deleted file mode 100644 index 002036831..000000000 --- a/.travis/vsftpd.conf +++ /dev/null @@ -1,12 +0,0 @@ -listen=YES -local_enable=YES -write_enable=YES -connect_from_port_20=YES -chroot_local_user=YES -secure_chroot_dir=/srv/empty -log_ftp_protocol=YES -xferlog_enable=YES -vsftpd_log_file=/var/log/vsftpd.log -passwd_chroot_enable=YES -local_root=/srv/ftp -background=YES diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index 09b49a482..05a228aeb 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -576,7 +576,7 @@ private function connect() /** * Closes the adapter's ftp connection. */ - private function close() + public function close() { if ($this->isConnected()) { ftp_close($this->connection); diff --git a/tests/Gaufrette/Functional/Adapter/FtpTest.php b/tests/Gaufrette/Functional/Adapter/FtpTest.php index 8da9d7bfd..8b623753e 100644 --- a/tests/Gaufrette/Functional/Adapter/FtpTest.php +++ b/tests/Gaufrette/Functional/Adapter/FtpTest.php @@ -10,6 +10,7 @@ class FtpTest extends FunctionalTestCase public function setUp() { $host = getenv('FTP_HOST'); + $port = getenv('FTP_PORT'); $user = getenv('FTP_USER'); $password = getenv('FTP_PASSWORD'); $baseDir = getenv('FTP_BASE_DIR'); @@ -18,7 +19,7 @@ public function setUp() $this->markTestSkipped('Either FTP_HOST, FTP_USER, FTP_PASSWORD and/or FTP_BASE_DIR env variables are not defined.'); } - $adapter = new Ftp($baseDir, $host, ['username' => $user, 'password' => $password, 'passive' => true]); + $adapter = new Ftp($baseDir, $host, ['port' => $port, 'username' => $user, 'password' => $password, 'passive' => true, 'create' => true]); $this->filesystem = new Filesystem($adapter); } @@ -41,5 +42,7 @@ public function tearDown() foreach ($keys as $key) { $adapter->delete($key); } + + $adapter->close(); } } diff --git a/tests/Gaufrette/Functional/Adapter/PhpseclibSftpTest.php b/tests/Gaufrette/Functional/Adapter/PhpseclibSftpTest.php index 35b427e97..3a996da6d 100644 --- a/tests/Gaufrette/Functional/Adapter/PhpseclibSftpTest.php +++ b/tests/Gaufrette/Functional/Adapter/PhpseclibSftpTest.php @@ -30,9 +30,8 @@ public function setUp() $this->sftp = new SFTP($host, $port); $this->sftp->login($user, $password); - $this->sftp->mkdir($this->baseDir); - $this->filesystem = new Filesystem(new PhpseclibSftp($this->sftp, $this->baseDir)); + $this->filesystem = new Filesystem(new PhpseclibSftp($this->sftp, $this->baseDir, true)); } public function tearDown() From a89094ac169800c1c223ab487401549e26f806c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Parmentier?= Date: Wed, 13 Sep 2017 12:00:18 +0200 Subject: [PATCH 331/426] Fix PhpseclibSftp fetchKeys's function when the directory doesn't exist --- src/Gaufrette/Adapter/PhpseclibSftp.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/PhpseclibSftp.php b/src/Gaufrette/Adapter/PhpseclibSftp.php index ef822e32f..19e82477d 100644 --- a/src/Gaufrette/Adapter/PhpseclibSftp.php +++ b/src/Gaufrette/Adapter/PhpseclibSftp.php @@ -205,8 +205,13 @@ protected function computePath($key) protected function fetchKeys($directory = '', $onlyKeys = true) { $keys = array('keys' => array(), 'dirs' => array()); + $computedPath = $this->computePath($directory); - $list = $this->sftp->rawlist($this->computePath($directory)); + if (!$this->sftp->file_exists($computedPath)) { + return $keys; + } + + $list = $this->sftp->rawlist($computedPath); foreach ((array) $list as $filename => $stat) { if ('.' === $filename || '..' === $filename) { continue; From 87fcfcf604b499d73e2da89dcd38534ec959c0e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Parmentier?= Date: Wed, 13 Sep 2017 12:03:45 +0200 Subject: [PATCH 332/426] Fix port mapping orders --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9cac8398b..c54679f93 100644 --- a/.travis.yml +++ b/.travis.yml @@ -42,8 +42,8 @@ before_script: else composer require mongofill/mongofill --ignore-platform-reqs; fi - - docker run -p 22:${SFTP_PORT} -d atmoz/sftp:alpine ${SFTP_USER}:${SFTP_PASSWORD}:::${SFTP_BASE_DIR} - - docker run -d --name ftpd_server -p 21:${FTP_PORT} -p 30000-30009:30000-30009 -e "PUBLICHOST=${FTP_HOST}" stilliard/pure-ftpd + - docker run -p ${SFTP_PORT}:22 -d atmoz/sftp:alpine ${SFTP_USER}:${SFTP_PASSWORD}:::${SFTP_BASE_DIR} + - docker run -d --name ftpd_server -p ${FTP_PORT}:21 -p 30000-30009:30000-30009 -e "PUBLICHOST=${FTP_HOST}" stilliard/pure-ftpd - docker exec -it ftpd_server sh -c "(echo ${FTP_PASSWORD}; echo ${FTP_PASSWORD}) | pure-pw useradd ${FTP_USER} -f /etc/pure-ftpd/passwd/pureftpd.passwd -m -u ftpuser -d /home/ftpusers/${FTP_USER}" - if [[ ${TRAVIS_PHP_VERSION} = 5.* ]]; then sudo apt-get -qq update && sudo apt-get install -y libssh2-1-dev; fi - if [[ ${TRAVIS_PHP_VERSION} = 5.* ]]; then pecl install ssh2 && composer require herzult/php-ssh --no-update --no-scripts; fi From 0311450cd50fe49872e32ee5e72121c5fc07a1a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Parmentier?= Date: Wed, 13 Sep 2017 12:14:52 +0200 Subject: [PATCH 333/426] Fix travis on PHP 5.6 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index c54679f93..6ec7c8b69 100644 --- a/.travis.yml +++ b/.travis.yml @@ -46,7 +46,7 @@ before_script: - docker run -d --name ftpd_server -p ${FTP_PORT}:21 -p 30000-30009:30000-30009 -e "PUBLICHOST=${FTP_HOST}" stilliard/pure-ftpd - docker exec -it ftpd_server sh -c "(echo ${FTP_PASSWORD}; echo ${FTP_PASSWORD}) | pure-pw useradd ${FTP_USER} -f /etc/pure-ftpd/passwd/pureftpd.passwd -m -u ftpuser -d /home/ftpusers/${FTP_USER}" - if [[ ${TRAVIS_PHP_VERSION} = 5.* ]]; then sudo apt-get -qq update && sudo apt-get install -y libssh2-1-dev; fi - - if [[ ${TRAVIS_PHP_VERSION} = 5.* ]]; then pecl install ssh2 && composer require herzult/php-ssh --no-update --no-scripts; fi + - if [[ ${TRAVIS_PHP_VERSION} = 5.* ]]; then yes | pecl install ssh2 && composer require herzult/php-ssh --no-update --no-scripts; fi - composer update --prefer-dist --no-progress --no-suggest --ansi script: From d531e2c26599da94eb1d6a8e1c57300f2194809a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Parmentier?= Date: Wed, 13 Sep 2017 12:15:01 +0200 Subject: [PATCH 334/426] Fix spec test suite --- spec/Gaufrette/Adapter/PhpseclibSftpSpec.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php b/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php index e75b6feb6..d4a624c24 100644 --- a/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php +++ b/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php @@ -43,6 +43,9 @@ function it_supports_native_list_keys() */ function it_fetches_keys($sftp) { + $sftp + ->file_exists('/home/l3l0/') + ->willReturn(true); $sftp ->rawlist('/home/l3l0/') ->willReturn(array( @@ -50,6 +53,9 @@ function it_fetches_keys($sftp) 'filename1' => array('type' => NET_SFTP_TYPE_REGULAR), 'aaa' => array('type' => NET_SFTP_TYPE_DIRECTORY) )); + $sftp + ->file_exists('/home/l3l0/aaa') + ->willReturn(true); $sftp ->rawlist('/home/l3l0/aaa') ->willReturn(array( From 2599af43c892c34f5d1c136cafd199327e7b32b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Parmentier?= Date: Wed, 20 Sep 2017 11:18:13 +0200 Subject: [PATCH 335/426] Bump PHPUnit to ^5.6.8 --- composer.json | 2 +- .../Functional/Adapter/FunctionalTestCase.php | 11 ++++++----- tests/Gaufrette/Functional/FileStream/LocalTest.php | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index 14e0ea797..7e4e1fffd 100644 --- a/composer.json +++ b/composer.json @@ -37,7 +37,7 @@ "phpseclib/phpseclib": "^2.0", "doctrine/dbal": ">=2.3", "dropbox-php/dropbox-php": "*", - "phpunit/phpunit": "3.7.*", + "phpunit/phpunit": "^5.6.8", "mikey179/vfsStream": "~1.2.0", "league/flysystem": "~1.0", "mongodb/mongodb": "~1.1.4", diff --git a/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php b/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php index 48f60dd06..f162353ab 100644 --- a/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php +++ b/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php @@ -3,8 +3,9 @@ namespace Gaufrette\Functional\Adapter; use Gaufrette\Filesystem; +use PHPUnit\Framework\TestCase; -abstract class FunctionalTestCase extends \PHPUnit_Framework_TestCase +abstract class FunctionalTestCase extends TestCase { /** * @var Filesystem @@ -34,7 +35,7 @@ public function setUp() ); if (!file_exists($filename)) { - return $this->markTestSkipped(<<markTestSkipped(<<filesystem->keys(); - $this->assertEquals(3, count($actualKeys)); + $this->assertCount(3, $actualKeys); foreach (array('foo', 'bar', 'baz') as $key) { $this->assertContains($key, $actualKeys); } @@ -195,14 +196,14 @@ public function shouldKeepFileObjectInRegister() $FileObjectA = $this->filesystem->createFile('somefile'); $FileObjectB = $this->filesystem->createFile('somefile'); - $this->assertTrue($FileObjectA === $FileObjectB); + $this->assertSame($FileObjectA, $FileObjectB); } /** * @test * @group functional */ - public function shouldWrtieToSameFile() + public function shouldWriteToSameFile() { $FileObjectA = $this->filesystem->createFile('somefile'); $FileObjectA->setContent('ABC'); diff --git a/tests/Gaufrette/Functional/FileStream/LocalTest.php b/tests/Gaufrette/Functional/FileStream/LocalTest.php index 2635d3317..f52641f88 100644 --- a/tests/Gaufrette/Functional/FileStream/LocalTest.php +++ b/tests/Gaufrette/Functional/FileStream/LocalTest.php @@ -43,7 +43,7 @@ public function tearDown() */ public function shouldSupportsDirectory() { - $this->assertTrue(file_exists('gaufrette://filestream/subdir')); + $this->assertFileExists('gaufrette://filestream/subdir'); $this->assertTrue(is_dir('gaufrette://filestream/subdir')); } } From 4aa8d36e4693e14fb1766dc9d2f62a91ac0d633c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Parmentier?= Date: Wed, 20 Sep 2017 11:28:26 +0200 Subject: [PATCH 336/426] Enable mbstring extension on AppVeyor --- appveyor.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 6c7c41d56..c2a06076a 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -51,6 +51,7 @@ install: - IF %PHP%==1 copy php.ini-production php.ini /Y - IF %PHP%==1 echo extension_dir=ext >> php.ini - IF %PHP%==1 echo date.timezone="UTC" >> php.ini + - IF %PHP%==1 echo extension=php_mbstring.dll >> php.ini - IF %PHP%==1 echo extension=php_openssl.dll >> php.ini - IF %PHP%==1 echo extension=php_mongodb.dll >> php.ini - IF %PHP%==1 echo extension=php_pdo_sqlite.dll >> php.ini @@ -59,7 +60,7 @@ install: - IF %PHP%==1 echo openssl.cafile=c:\php\cacert.pem >> php.ini - cd C:\projects\gaufrette - appveyor DownloadFile https://getcomposer.org/composer.phar - - php composer.phar install --prefer-dist --no-interaction --no-progress --ansi + - php composer.phar update --prefer-dist --no-interaction --no-progress --no-suggest --ansi test_script: - cd C:\projects\gaufrette From e1d5911921a6eccbe3539c4526717e3bd8815c88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Parmentier?= Date: Wed, 20 Sep 2017 12:04:00 +0200 Subject: [PATCH 337/426] Use composer's autoload-dev --- composer.json | 3 +++ phpunit.xml.dist | 2 +- tests/bootstrap.php.dist | 7 ------- 3 files changed, 4 insertions(+), 8 deletions(-) delete mode 100644 tests/bootstrap.php.dist diff --git a/composer.json b/composer.json index 7e4e1fffd..6e950dad2 100644 --- a/composer.json +++ b/composer.json @@ -66,6 +66,9 @@ "autoload": { "psr-0": { "Gaufrette": "src/" } }, + "autoload-dev": { + "psr-0": { "Gaufrette\\Functional": "tests/" } + }, "extra": { "branch-alias": { "dev-master": "0.5.x-dev" diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 60c0cfd0b..9bf58266e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -9,7 +9,7 @@ processIsolation="false" stopOnFailure="false" syntaxCheck="false" - bootstrap="tests/bootstrap.php.dist" + bootstrap="vendor/autoload.php" > diff --git a/tests/bootstrap.php.dist b/tests/bootstrap.php.dist deleted file mode 100644 index 04116a3d0..000000000 --- a/tests/bootstrap.php.dist +++ /dev/null @@ -1,7 +0,0 @@ -add('Gaufrette', array($baseDir.'/src/', $baseDir.'/tests/')); -$loader->register(); From 0e9053189b7313a5b8f6ebf6f4bfd2a51ede989b Mon Sep 17 00:00:00 2001 From: Gerke Stam Date: Wed, 27 Sep 2017 15:30:09 +0200 Subject: [PATCH 338/426] Updated based on feedback PR --- .../Functional/Adapter/AzureMultiContainerBlobStorageTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php index 26d9ae41f..10d349dda 100644 --- a/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php +++ b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php @@ -103,9 +103,9 @@ public function shouldGetSize() { $path = $this->createUniqueContainerName('container') . '/foo'; - $this->filesystem->write($path, 'Some content'); + $contentSize = $this->filesystem->write($path, 'Some content'); - $this->assertGreaterThan(11, $this->filesystem->size($path)); + $this->assertEquals($contentSize, $this->filesystem->size($path)); } /** From 382bcf7f2f771e50b54f8ed7e7629b3468f830bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Parmentier?= Date: Mon, 2 Oct 2017 15:03:24 +0200 Subject: [PATCH 339/426] Drop HHVM support + sync docker conf with Travis --- .dockerignore | 1 - .env.dist | 7 ++-- .travis.yml | 27 ++++++--------- Dockerfile-php56 | 56 +++++++++++++++++++++++++++++++ Dockerfile-php70 | 68 ++++++++++++++++++++++++++++++++++++++ Dockerfile-php71 | 56 +++++++++++++++++++++++++++++++ bin/tests | 8 ++--- bin/tests-all | 3 +- composer.json | 9 +---- docker-compose.yml | 59 ++++++++++++++------------------- docker/Dockerfile-ftp | 18 ++-------- docker/Dockerfile-hhvm | 16 --------- docker/Dockerfile-php56 | 23 ------------- docker/Dockerfile-php70 | 28 ---------------- docker/Dockerfile-php71 | 29 ---------------- docker/Dockerfile-sftp | 13 -------- docker/install-composer.sh | 21 ++++++++++++ docker/php.ini | 2 ++ docker/vsftpd.conf | 11 ------ 19 files changed, 249 insertions(+), 206 deletions(-) create mode 100644 Dockerfile-php56 create mode 100644 Dockerfile-php70 create mode 100644 Dockerfile-php71 delete mode 100644 docker/Dockerfile-hhvm delete mode 100644 docker/Dockerfile-php56 delete mode 100644 docker/Dockerfile-php70 delete mode 100644 docker/Dockerfile-php71 delete mode 100644 docker/Dockerfile-sftp create mode 100755 docker/install-composer.sh create mode 100644 docker/php.ini delete mode 100644 docker/vsftpd.conf diff --git a/.dockerignore b/.dockerignore index b9f6106f2..c4021d6cd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,6 @@ .idea/ bin/tests-all doc/ -docker/ vendor/ .dockerignore .editorconfig diff --git a/.env.dist b/.env.dist index 0face3dc2..b3553749d 100644 --- a/.env.dist +++ b/.env.dist @@ -9,12 +9,13 @@ SFTP_HOST=sftp SFTP_PORT=22 SFTP_USER=gaufrette SFTP_PASSWORD=gaufrette -SFTP_BASE_DIR=/home/gaufrette +SFTP_BASE_DIR=gaufrette -FTP_HOST=vsftpd +FTP_HOST=ftp +FTP_PORT=21 FTP_USER=gaufrette FTP_PASSWORD=gaufrette -FTP_BASE_DIR=/gaufrette/ +FTP_BASE_DIR=/gaufrette AZURE_ACCOUNT= AZURE_KEY= diff --git a/.travis.yml b/.travis.yml index 6ec7c8b69..1ed52d991 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,12 +6,6 @@ cache: directories: - $HOME/.composer/cache/files -php: - - 5.6 - - 7.0 - - 7.1 - - hhvm - services: - mongodb - docker @@ -31,22 +25,21 @@ env: FTP_BASE_DIR=/gaufrette/ matrix: - allow_failures: - - php: hhvm - fast_finish: true - -before_script: - - if [[ ${TRAVIS_PHP_VERSION:0:1} != "hhvm" ]]; then - echo "extension = mongodb.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; - phpenv config-rm xdebug.ini; - else - composer require mongofill/mongofill --ignore-platform-reqs; - fi + include: + - php: '5.6' + - php: '7.0' + - php: '7.1' + +before_install: + - echo "extension = mongodb.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; + - phpenv config-rm xdebug.ini || echo "xdebug not available" - docker run -p ${SFTP_PORT}:22 -d atmoz/sftp:alpine ${SFTP_USER}:${SFTP_PASSWORD}:::${SFTP_BASE_DIR} - docker run -d --name ftpd_server -p ${FTP_PORT}:21 -p 30000-30009:30000-30009 -e "PUBLICHOST=${FTP_HOST}" stilliard/pure-ftpd - docker exec -it ftpd_server sh -c "(echo ${FTP_PASSWORD}; echo ${FTP_PASSWORD}) | pure-pw useradd ${FTP_USER} -f /etc/pure-ftpd/passwd/pureftpd.passwd -m -u ftpuser -d /home/ftpusers/${FTP_USER}" - if [[ ${TRAVIS_PHP_VERSION} = 5.* ]]; then sudo apt-get -qq update && sudo apt-get install -y libssh2-1-dev; fi - if [[ ${TRAVIS_PHP_VERSION} = 5.* ]]; then yes | pecl install ssh2 && composer require herzult/php-ssh --no-update --no-scripts; fi + +install: - composer update --prefer-dist --no-progress --no-suggest --ansi script: diff --git a/Dockerfile-php56 b/Dockerfile-php56 new file mode 100644 index 000000000..97b543999 --- /dev/null +++ b/Dockerfile-php56 @@ -0,0 +1,56 @@ +FROM php:5.6-alpine + +ENV SRC_DIR /usr/src/gaufrette + +RUN apk add --no-cache --virtual .persistent-deps \ + git \ + zlib + +# PHP extensions +ENV MONGODB_VERSION="1.2.11" \ + SSH2_VERSION="0.13" + +RUN set -xe \ + && apk add --no-cache --virtual .build-deps \ + $PHPIZE_DEPS \ + libssh2-dev \ + zlib-dev \ + && docker-php-ext-install \ + zip \ + && pecl install \ + mongodb-${MONGODB_VERSION} \ + ssh2-${SSH2_VERSION} \ + && docker-php-ext-enable --ini-name 05-opcache.ini opcache \ + && docker-php-ext-enable \ + mongodb \ + ssh2 \ + && apk del .build-deps + +COPY docker/php.ini /usr/local/etc/php/php.ini + +COPY docker/install-composer.sh /usr/local/bin/install-composer +RUN chmod +x /usr/local/bin/install-composer + +RUN set -xe \ + && install-composer \ + && mv composer.phar /usr/local/bin/composer + +ENV COMPOSER_ALLOW_SUPERUSER 1 + +RUN composer global require "hirak/prestissimo" --prefer-dist --no-progress --no-suggest --optimize-autoloader --apcu-autoloader \ + && composer clear-cache + +WORKDIR ${SRC_DIR} + +COPY composer.json ./ +RUN composer update --prefer-dist --no-autoloader --no-scripts --no-progress --no-suggest \ + && composer clear-cache + +COPY spec spec/ +COPY src src/ +COPY tests tests/ +COPY bin/tests bin/tests + +RUN composer dump-autoload + +CMD ["bin/tests"] diff --git a/Dockerfile-php70 b/Dockerfile-php70 new file mode 100644 index 000000000..0b8b24a7c --- /dev/null +++ b/Dockerfile-php70 @@ -0,0 +1,68 @@ +FROM php:7.0-alpine + +ENV SRC_DIR /usr/src/gaufrette + +RUN apk add --no-cache --virtual .persistent-deps \ + git \ + zlib + +# PHP extensions +ENV MONGODB_VERSION="1.2.11" \ + SSH2_VERSION="1.1.2" + +RUN set -xe \ + && apk add --no-cache --virtual .build-deps \ + $PHPIZE_DEPS \ + libssh2-dev \ + zlib-dev \ + && docker-php-ext-install \ + zip \ + && pecl install \ + mongodb-${MONGODB_VERSION} \ + ssh2-${SSH2_VERSION} \ + && docker-php-ext-enable --ini-name 05-opcache.ini opcache \ + && docker-php-ext-enable \ + mongodb \ + ssh2 \ + && apk del .build-deps + +COPY php.ini /usr/local/etc/php/php.ini + +COPY install-composer.sh /usr/local/bin/docker-gaufrette-install-composer +RUN chmod +x /usr/local/bin/docker-gaufrette-install-composer + +RUN set -xe \ + && apk add --no-cache --virtual .fetch-deps \ + openssl \ + && docker-gaufrette-install-composer \ + && mv composer.phar /usr/local/bin/composer \ + && apk del .fetch-deps + +COPY docker/php.ini /usr/local/etc/php/php.ini + +COPY docker/install-composer.sh /usr/local/bin/install-composer +RUN chmod +x /usr/local/bin/install-composer + +RUN set -xe \ + && install-composer \ + && mv composer.phar /usr/local/bin/composer + +ENV COMPOSER_ALLOW_SUPERUSER 1 + +RUN composer global require "hirak/prestissimo" --prefer-dist --no-progress --no-suggest --optimize-autoloader --apcu-autoloader \ + && composer clear-cache + +WORKDIR ${SRC_DIR} + +COPY composer.json ./ +RUN composer update --prefer-dist --no-autoloader --no-scripts --no-progress --no-suggest \ + && composer clear-cache + +COPY spec spec/ +COPY src src/ +COPY tests tests/ +COPY bin/tests bin/tests + +RUN composer dump-autoload + +CMD ["bin/tests"] diff --git a/Dockerfile-php71 b/Dockerfile-php71 new file mode 100644 index 000000000..ac5026ceb --- /dev/null +++ b/Dockerfile-php71 @@ -0,0 +1,56 @@ +FROM php:7.1-alpine + +ENV SRC_DIR /usr/src/gaufrette + +RUN apk add --no-cache --virtual .persistent-deps \ + git \ + zlib + +# PHP extensions +ENV MONGODB_VERSION="1.2.11" \ + SSH2_VERSION="1.1.2" + +RUN set -xe \ + && apk add --no-cache --virtual .build-deps \ + $PHPIZE_DEPS \ + libssh2-dev \ + zlib-dev \ + && docker-php-ext-install \ + zip \ + && pecl install \ + mongodb-${MONGODB_VERSION} \ + ssh2-${SSH2_VERSION} \ + && docker-php-ext-enable --ini-name 05-opcache.ini opcache \ + && docker-php-ext-enable \ + mongodb \ + ssh2 \ + && apk del .build-deps + +COPY docker/php.ini /usr/local/etc/php/php.ini + +COPY docker/install-composer.sh /usr/local/bin/install-composer +RUN chmod +x /usr/local/bin/install-composer + +RUN set -xe \ + && install-composer \ + && mv composer.phar /usr/local/bin/composer + +ENV COMPOSER_ALLOW_SUPERUSER 1 + +RUN composer global require "hirak/prestissimo" --prefer-dist --no-progress --no-suggest --optimize-autoloader --apcu-autoloader \ + && composer clear-cache + +WORKDIR ${SRC_DIR} + +COPY composer.json ./ +RUN composer update --prefer-dist --no-autoloader --no-scripts --no-progress --no-suggest \ + && composer clear-cache + +COPY spec spec/ +COPY src src/ +COPY tests tests/ +COPY bin/tests bin/tests + +RUN composer dump-autoload + +CMD ["bin/tests"] diff --git a/bin/tests b/bin/tests index d647ea270..d15f6a66a 100755 --- a/bin/tests +++ b/bin/tests @@ -1,10 +1,10 @@ -#!/bin/bash +#!/bin/sh set -o nounset set -o errexit set -o xtrace -DIR=$(dirname ${BASH_SOURCE[0]})/.. +DIR=$(dirname $0)/.. -${DIR}/vendor/bin/phpspec run --format=pretty -${DIR}/vendor/bin/phpunit +${DIR}/vendor/bin/phpunit tests +${DIR}/vendor/bin/phpspec run --format=pretty --no-code-generation diff --git a/bin/tests-all b/bin/tests-all index a507fe4b3..e88aefe9e 100755 --- a/bin/tests-all +++ b/bin/tests-all @@ -1,8 +1,7 @@ -#!/bin/bash +#!/bin/sh set -o xtrace docker-compose run --rm php56 docker-compose run --rm php70 docker-compose run --rm php71 -docker-compose run --rm hhvm diff --git a/composer.json b/composer.json index 6e950dad2..5f9282606 100644 --- a/composer.json +++ b/composer.json @@ -3,7 +3,6 @@ "type": "library", "description": "PHP5 library that provides a filesystem abstraction layer", "keywords": ["file", "filesystem", "media", "abstraction"], - "minimum-stability": "dev", "homepage": "http://knplabs.com", "license": "MIT", "authors": [ @@ -16,12 +15,6 @@ "homepage": "http://github.com/knplabs/Gaufrette/contributors" } ], - "repositories": [ - { - "type": "pear", - "url": "https://pear.php.net" - } - ], "require": { "php": ">=5.6" }, @@ -40,7 +33,7 @@ "phpunit/phpunit": "^5.6.8", "mikey179/vfsStream": "~1.2.0", "league/flysystem": "~1.0", - "mongodb/mongodb": "~1.1.4", + "mongodb/mongodb": "^1.1", "microsoft/windowsazure": "~0.4", "microsoft/azure-storage": "~0.15.0", "akeneo/phpspec-skip-example-extension": "~1.2" diff --git a/docker-compose.yml b/docker-compose.yml index 5da26da61..8dbdc7040 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,72 +1,61 @@ - -version: '2' +version: '3' services: php56: build: context: . - dockerfile: docker/Dockerfile-php56 + dockerfile: ./Dockerfile-php56 volumes: - - './spec/:/usr/src/gaufrette/spec/' - - './src/:/usr/src/gaufrette/src/' - - './tests/:/usr/src/gaufrette/tests/' + - './spec:/usr/src/gaufrette/spec:rw' + - './src:/usr/src/gaufrette/src:ro' + - './tests:/usr/src/gaufrette/tests:rw' + - './bin/tests:/usr/src/gaufrette/bin/tests:ro' depends_on: - mongodb - sftp - - vsftpd + - ftp env_file: .env php70: build: context: . - dockerfile: docker/Dockerfile-php70 + dockerfile: ./Dockerfile-php70 volumes: - - './spec/:/usr/src/gaufrette/spec/' - - './src/:/usr/src/gaufrette/src/' - - './tests/:/usr/src/gaufrette/tests/' + - './spec:/usr/src/gaufrette/spec:rw' + - './src:/usr/src/gaufrette/src:ro' + - './tests:/usr/src/gaufrette/tests:rw' + - './bin/tests:/usr/src/gaufrette/bin/tests:ro' depends_on: - mongodb - sftp - - vsftpd + - ftp env_file: .env php71: build: context: . - dockerfile: docker/Dockerfile-php71 + dockerfile: ./Dockerfile-php71 volumes: - - './spec/:/usr/src/gaufrette/spec/' - - './src/:/usr/src/gaufrette/src/' - - './tests/:/usr/src/gaufrette/tests/' + - './spec:/usr/src/gaufrette/spec:rw' + - './src:/usr/src/gaufrette/src:ro' + - './tests:/usr/src/gaufrette/tests:rw' + - './bin/tests:/usr/src/gaufrette/bin/tests:ro' env_file: .env depends_on: - mongodb - sftp - - vsftpd - - hhvm: - build: - context: . - dockerfile: docker/Dockerfile-hhvm - volumes: - - './spec/:/usr/src/gaufrette/spec/' - - './src/:/usr/src/gaufrette/src/' - - './tests/:/usr/src/gaufrette/tests/' - env_file: .env - depends_on: - - mongodb - - sftp - - vsftpd + - ftp mongodb: image: mongo sftp: - build: - context: docker - dockerfile: Dockerfile-sftp + image: atmoz/sftp:alpine + command: gaufrette:gaufrette:::gaufrette - vsftpd: + ftp: build: context: docker dockerfile: Dockerfile-ftp + environment: + PUBLICHOST: 'ftp' diff --git a/docker/Dockerfile-ftp b/docker/Dockerfile-ftp index 96a57e877..8cf44f07a 100644 --- a/docker/Dockerfile-ftp +++ b/docker/Dockerfile-ftp @@ -1,17 +1,3 @@ -FROM ubuntu:precise +FROM stilliard/pure-ftpd -RUN apt-get update && \ - apt-get install -y --no-install-recommends vsftpd && \ - rm -rf /var/lib/apt/lists/* - -COPY vsftpd.conf /etc/vsftpd.conf - -# Removing "./" from homedir would break vsftpd config -RUN chown root:root /etc/vsftpd.conf && \ - useradd -d /home/./gaufrette gaufrette && \ - echo "gaufrette:gaufrette" | chpasswd && \ - mkdir /home/gaufrette /srv/empty && \ - chown gaufrette:gaufrette /home/gaufrette /srv/ftp - -EXPOSE 20-21 -CMD ["vsftpd"] +RUN (echo gaufrette; echo gaufrette) | pure-pw useradd gaufrette -f /etc/pure-ftpd/passwd/pureftpd.passwd -m -u ftpuser -d /home/ftpusers/gaufrette diff --git a/docker/Dockerfile-hhvm b/docker/Dockerfile-hhvm deleted file mode 100644 index 6c9c08a89..000000000 --- a/docker/Dockerfile-hhvm +++ /dev/null @@ -1,16 +0,0 @@ -FROM webdevops/hhvm:ubuntu-16.04 - -ENV SRC_DIR /usr/src/gaufrette -WORKDIR ${SRC_DIR} - -COPY composer.json ${SRC_DIR}/ -RUN composer global require hirak/prestissimo && \ - composer remove --dev --no-update herzult/php-ssh && \ - composer update --prefer-lowest --prefer-stable && \ - composer require microsoft/windowsazure:~0.5 && \ - rm -rf ~/.composer/cache/* - -VOLUME ${SRC_DIR}/vendor - -COPY . ${SRC_DIR}/ -CMD "${SRC_DIR}/bin/tests" diff --git a/docker/Dockerfile-php56 b/docker/Dockerfile-php56 deleted file mode 100644 index 7c6273c06..000000000 --- a/docker/Dockerfile-php56 +++ /dev/null @@ -1,23 +0,0 @@ -FROM jolicode/php56:latest - -ENV SRC_DIR /usr/src/gaufrette -WORKDIR ${SRC_DIR}/ - -RUN sudo apt-get update && \ - sudo apt-get install -y libssh2-1-dev && \ - sudo rm -rf /var/lib/apt/lists/* && \ - pecl install mongodb ssh2 - -COPY composer.json ${SRC_DIR}/ -RUN sudo chown -R travis ${SRC_DIR} && \ - composer global require hirak/prestissimo && \ - composer update --prefer-lowest --prefer-stable && \ - composer require microsoft/windowsazure:~0.4.3 && \ - rm -rf ~/.composer/cache/* - -VOLUME ${SRC_DIR}/vendor - -COPY . ${SRC_DIR}/ -RUN sudo chown -R travis ${SRC_DIR} - -CMD "${SRC_DIR}/bin/tests" diff --git a/docker/Dockerfile-php70 b/docker/Dockerfile-php70 deleted file mode 100644 index c0cf1b1d7..000000000 --- a/docker/Dockerfile-php70 +++ /dev/null @@ -1,28 +0,0 @@ -FROM php:7.0-cli - -ENV SRC_DIR /usr/src/gaufrette -WORKDIR ${SRC_DIR} - -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - git \ - libssh2-1-dev \ - libssl-dev \ - unzip \ - libzip-dev \ - && rm -rf /var/lib/apt/lists/* && \ - pecl install mongodb zip && \ - pecl install --ignore-errors ssh2-alpha && \ - echo 'extension=/usr/local/lib/php/extensions/no-debug-non-zts-20151012/mongodb.so' >> /usr/local/etc/php/conf.d/mongodb.ini && \ - echo 'extension=/usr/local/lib/php/extensions/no-debug-non-zts-20151012/ssh2.so' >> /usr/local/etc/php/conf.d/ssh2.ini && \ - echo 'extension=/usr/local/lib/php/extensions/no-debug-non-zts-20151012/zip.so' >> /usr/local/etc/php/conf.d/zip.ini - -COPY composer.json ${SRC_DIR}/ -RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer && \ - composer install && \ - rm -rf ~/.composer/cache/* - -VOLUME ${SRC_DIR}/vendor - -COPY . ${SRC_DIR}/ -CMD "${SRC_DIR}/bin/tests" diff --git a/docker/Dockerfile-php71 b/docker/Dockerfile-php71 deleted file mode 100644 index 48c447bba..000000000 --- a/docker/Dockerfile-php71 +++ /dev/null @@ -1,29 +0,0 @@ -FROM php:7.1-cli - -ENV SRC_DIR /usr/src/gaufrette -WORKDIR ${SRC_DIR} - -RUN apt-get update && \ - apt-get install -y --no-install-recommends \ - git \ - libssh2-1-dev \ - libssl-dev \ - unzip \ - libzip-dev \ - && rm -rf /var/lib/apt/lists/* && \ - pecl install mongodb zip && \ - pecl install --ignore-errors ssh2-alpha && \ - echo 'extension=/usr/local/lib/php/extensions/no-debug-non-zts-20160303/mongodb.so' >> /usr/local/etc/php/conf.d/mongodb.ini && \ - echo 'extension=/usr/local/lib/php/extensions/no-debug-non-zts-20160303/ssh2.so' >> /usr/local/etc/php/conf.d/ssh2.ini && \ - echo 'extension=/usr/local/lib/php/extensions/no-debug-non-zts-20160303/zip.so' >> /usr/local/etc/php/conf.d/zip.ini - -COPY composer.json ${SRC_DIR}/ -RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer && \ - composer global require hirak/prestissimo && \ - composer update --prefer-lowest --prefer-stable && \ - rm -rf ~/.composer/cache/* - -VOLUME ${SRC_DIR}/vendor - -COPY . ${SRC_DIR}/ -CMD "${SRC_DIR}/bin/tests" diff --git a/docker/Dockerfile-sftp b/docker/Dockerfile-sftp deleted file mode 100644 index 005e5b0b4..000000000 --- a/docker/Dockerfile-sftp +++ /dev/null @@ -1,13 +0,0 @@ -FROM ubuntu:precise - -RUN apt-get update && \ - apt-get install -y --no-install-recommends openssh-server && \ - rm -rf /var/lib/apt/lists/* - -RUN mkdir /var/run/sshd && \ - useradd -d /home/gaufrette gaufrette && \ - mkdir /home/gaufrette && \ - chown gaufrette:gaufrette /home/gaufrette && \ - echo "gaufrette:gaufrette" | chpasswd - -CMD ["/usr/sbin/sshd", "-D"] diff --git a/docker/install-composer.sh b/docker/install-composer.sh new file mode 100755 index 000000000..f4ad41bfc --- /dev/null +++ b/docker/install-composer.sh @@ -0,0 +1,21 @@ +#!/bin/sh + +# Copyright (c) Nils Adermann, Jordi Boggiano +# Origin: https://github.com/composer/composer/blob/master/doc/faqs/how-to-install-composer-programmatically.md +# Licence: MIT + +EXPECTED_SIGNATURE=$(wget -q -O - https://composer.github.io/installer.sig) +php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" +ACTUAL_SIGNATURE=$(php -r "echo hash_file('SHA384', 'composer-setup.php');") + +if [ "$EXPECTED_SIGNATURE" != "$ACTUAL_SIGNATURE" ] +then + >&2 echo 'ERROR: Invalid installer signature' + rm composer-setup.php + exit 1 +fi + +php composer-setup.php --quiet +RESULT=$? +rm composer-setup.php +exit $RESULT diff --git a/docker/php.ini b/docker/php.ini new file mode 100644 index 000000000..fe64554e1 --- /dev/null +++ b/docker/php.ini @@ -0,0 +1,2 @@ +apc.enable_cli = 1 +date.timezone = UTC diff --git a/docker/vsftpd.conf b/docker/vsftpd.conf deleted file mode 100644 index c8d224c8d..000000000 --- a/docker/vsftpd.conf +++ /dev/null @@ -1,11 +0,0 @@ -listen=YES -local_enable=YES -write_enable=YES -connect_from_port_20=YES -chroot_local_user=YES -secure_chroot_dir=/srv/empty -log_ftp_protocol=YES -xferlog_enable=YES -vsftpd_log_file=/var/log/vsftpd.log -passwd_chroot_enable=YES -local_root=/srv/ftp From dbfacd4409ebaa986806d9fc41b9b8c8e16f78f6 Mon Sep 17 00:00:00 2001 From: dawkamn Date: Fri, 15 Dec 2017 18:54:00 +0800 Subject: [PATCH 340/426] Fixed typo Fixed typo in the README and removed version constraint from the composer description because it no longer holds. --- README.md | 2 +- composer.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 843f5fd4f..71c7da83c 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ Imagine you have to manage a lot of medias in a PHP project. Lets see how to take this situation in your advantage using Gaufrette. The filesystem abstraction layer permits you to develop your application without -the need to know were all those medias will be stored and how. +the need to know where all those medias will be stored and how. Another advantage of this is the possibility to update the files location without any impact on the code apart from the definition of your filesystem. diff --git a/composer.json b/composer.json index 5f9282606..5ba7edea6 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "knplabs/gaufrette", "type": "library", - "description": "PHP5 library that provides a filesystem abstraction layer", + "description": "PHP library that provides a filesystem abstraction layer", "keywords": ["file", "filesystem", "media", "abstraction"], "homepage": "http://knplabs.com", "license": "MIT", From de57d3cdab5424d177c78da085c4585d1cef88d7 Mon Sep 17 00:00:00 2001 From: Nicolas MURE Date: Thu, 4 Jan 2018 09:43:23 +0100 Subject: [PATCH 341/426] fix travis build for php 5.6 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 1ed52d991..4602e2c43 100644 --- a/.travis.yml +++ b/.travis.yml @@ -37,7 +37,7 @@ before_install: - docker run -d --name ftpd_server -p ${FTP_PORT}:21 -p 30000-30009:30000-30009 -e "PUBLICHOST=${FTP_HOST}" stilliard/pure-ftpd - docker exec -it ftpd_server sh -c "(echo ${FTP_PASSWORD}; echo ${FTP_PASSWORD}) | pure-pw useradd ${FTP_USER} -f /etc/pure-ftpd/passwd/pureftpd.passwd -m -u ftpuser -d /home/ftpusers/${FTP_USER}" - if [[ ${TRAVIS_PHP_VERSION} = 5.* ]]; then sudo apt-get -qq update && sudo apt-get install -y libssh2-1-dev; fi - - if [[ ${TRAVIS_PHP_VERSION} = 5.* ]]; then yes | pecl install ssh2 && composer require herzult/php-ssh --no-update --no-scripts; fi + - if [[ ${TRAVIS_PHP_VERSION} = 5.* ]]; then yes | pecl install -f ssh2 && composer require herzult/php-ssh --no-update --no-scripts; fi install: - composer update --prefer-dist --no-progress --no-suggest --ansi From 31ab60446c1887a68f1a4c5a504bada854987eb0 Mon Sep 17 00:00:00 2001 From: Brian Sperduto Date: Sat, 28 Oct 2017 11:21:01 -0500 Subject: [PATCH 342/426] GridFS Metadata retrieve from bucket Fixes GridFS metadata feature where it would not retieve metadata from bucket unless the file was already written to --- src/Gaufrette/Adapter/GridFS.php | 15 ++++++++++++++- tests/Gaufrette/Functional/Adapter/GridFSTest.php | 15 +++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/GridFS.php b/src/Gaufrette/Adapter/GridFS.php index 29983a935..d302d8339 100644 --- a/src/Gaufrette/Adapter/GridFS.php +++ b/src/Gaufrette/Adapter/GridFS.php @@ -166,7 +166,20 @@ public function setMetadata($key, $metadata) */ public function getMetadata($key) { - return isset($this->metadata[$key]) ? $this->metadata[$key] : array(); + if(isset($this->metadata[$key])) + { + return $this->metadata[$key]; + } + else + { + $meta = $this->bucket->findOne(['filename' => $key], ['projection' => ['metadata' => 1, '_id' => 0]]); + if($meta === null) + { + return array(); + } + $this->metadata[$key] = iterator_to_array($meta['metadata']); + return $this->metadata[$key]; + } } /** diff --git a/tests/Gaufrette/Functional/Adapter/GridFSTest.php b/tests/Gaufrette/Functional/Adapter/GridFSTest.php index 531d15e5e..447e80afb 100644 --- a/tests/Gaufrette/Functional/Adapter/GridFSTest.php +++ b/tests/Gaufrette/Functional/Adapter/GridFSTest.php @@ -74,4 +74,19 @@ public function shouldListKeys() $keys['keys'], '', 0, 10, true); } + + /** + * @test + * Tests metadata written to GridFS can be retrieved after writing + */ + public function testMetadataRetrieveAfterWrite() + { + //Create local copy of fileadapter + $fileadpt = clone $this->filesystem->getAdapter(); + + $this->filesystem->getAdapter()->setMetadata('metadatatest', array('testing' => true)); + $this->filesystem->write('metadatatest', 'test'); + + $this->assertEquals($this->filesystem->getAdapter()->getMetadata('metadatatest'), $fileadpt->getMetadata('metadatatest')); + } } From bd5dc87d11d137c3b28f5cabaeb2ebae66dfb5c5 Mon Sep 17 00:00:00 2001 From: Brian Sperduto Date: Thu, 4 Jan 2018 06:37:54 -0600 Subject: [PATCH 343/426] Updated formatting --- src/Gaufrette/Adapter/GridFS.php | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/Gaufrette/Adapter/GridFS.php b/src/Gaufrette/Adapter/GridFS.php index d302d8339..990050c1c 100644 --- a/src/Gaufrette/Adapter/GridFS.php +++ b/src/Gaufrette/Adapter/GridFS.php @@ -166,19 +166,15 @@ public function setMetadata($key, $metadata) */ public function getMetadata($key) { - if(isset($this->metadata[$key])) - { - return $this->metadata[$key]; - } - else - { - $meta = $this->bucket->findOne(['filename' => $key], ['projection' => ['metadata' => 1, '_id' => 0]]); - if($meta === null) - { - return array(); - } - $this->metadata[$key] = iterator_to_array($meta['metadata']); - return $this->metadata[$key]; + if (isset($this->metadata[$key])) { + return $this->metadata[$key]; + } else { + $meta = $this->bucket->findOne(['filename' => $key], ['projection' => ['metadata' => 1,'_id' => 0]]); + if ($meta === null) { + return array(); + } + $this->metadata[$key] = iterator_to_array($meta['metadata']); + return $this->metadata[$key]; } } From 6364659f122bd7c595d5bd4404afc7118aa19106 Mon Sep 17 00:00:00 2001 From: Brian Sperduto Date: Thu, 4 Jan 2018 06:59:39 -0600 Subject: [PATCH 344/426] Fixed line encoding --- src/Gaufrette/Adapter/GridFS.php | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/Gaufrette/Adapter/GridFS.php b/src/Gaufrette/Adapter/GridFS.php index 990050c1c..fe2873183 100644 --- a/src/Gaufrette/Adapter/GridFS.php +++ b/src/Gaufrette/Adapter/GridFS.php @@ -166,15 +166,15 @@ public function setMetadata($key, $metadata) */ public function getMetadata($key) { - if (isset($this->metadata[$key])) { - return $this->metadata[$key]; - } else { - $meta = $this->bucket->findOne(['filename' => $key], ['projection' => ['metadata' => 1,'_id' => 0]]); - if ($meta === null) { - return array(); - } - $this->metadata[$key] = iterator_to_array($meta['metadata']); - return $this->metadata[$key]; + if (isset($this->metadata[$key])) { + return $this->metadata[$key]; + } else { + $meta = $this->bucket->findOne(['filename' => $key], ['projection' => ['metadata' => 1,'_id' => 0]]); + if ($meta === null) { + return array(); + } + $this->metadata[$key] = iterator_to_array($meta['metadata']); + return $this->metadata[$key]; } } From 57f6b15b4d0ccd8cf4d52dfb7e4c1451d3e03a42 Mon Sep 17 00:00:00 2001 From: andreasschacht Date: Fri, 15 Dec 2017 16:24:50 +0100 Subject: [PATCH 345/426] Quickfix for Adapter/AwsS3, check if count() call is allowed. (cherry picked from commit ad3aad3) --- src/Gaufrette/Adapter/AwsS3.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index b39feb471..72e3a5988 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -270,8 +270,13 @@ public function isDirectory($key) 'Prefix' => rtrim($this->computePath($key), '/').'/', 'MaxKeys' => 1, ]); + if (isset($result['Contents'])) { + if (is_array($result['Contents']) || $result['Contents'] instanceof \Countable) { + return count($result['Contents']) > 0; + } + } - return count($result['Contents']) > 0; + return false; } /** From ac60010ff30bbff2256f27b2ede341d0aa6459cd Mon Sep 17 00:00:00 2001 From: Gabriel Caruso Date: Thu, 4 Jan 2018 03:31:06 -0200 Subject: [PATCH 346/426] Refactoring tests Signed-off-by: Gabriel Caruso --- .../Adapter/AzureMultiContainerBlobStorageTest.php | 2 +- tests/Gaufrette/Functional/Adapter/LocalTest.php | 2 +- .../Gaufrette/Functional/FileStream/FunctionalTestCase.php | 6 +++--- tests/Gaufrette/Functional/FileStream/LocalTest.php | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php index 10d349dda..8fba0dad0 100644 --- a/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php +++ b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php @@ -206,7 +206,7 @@ public function shouldKeepFileObjectInRegister() $FileObjectA = $this->filesystem->createFile($path); $FileObjectB = $this->filesystem->createFile($path); - $this->assertTrue($FileObjectA === $FileObjectB); + $this->assertSame($FileObjectB, $FileObjectA); } /** diff --git a/tests/Gaufrette/Functional/Adapter/LocalTest.php b/tests/Gaufrette/Functional/Adapter/LocalTest.php index c1947f96b..7dbd9d27b 100644 --- a/tests/Gaufrette/Functional/Adapter/LocalTest.php +++ b/tests/Gaufrette/Functional/Adapter/LocalTest.php @@ -160,7 +160,7 @@ public function shouldBeAbleToClearCache() $this->filesystem->clearFileRegister(); $fsRegister = $fsReflection->getProperty('fileRegister'); $fsRegister->setAccessible(true); - $this->assertEquals(0, count($fsRegister->getValue($this->filesystem))); + $this->assertCount(0, $fsRegister->getValue($this->filesystem)); $this->filesystem->delete('test.txt'); $this->filesystem->delete('test2.txt'); diff --git a/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php b/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php index 7575da3b8..78b329a10 100644 --- a/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php +++ b/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php @@ -26,10 +26,10 @@ public function shouldCheckIsFile() public function shouldCheckFileExists() { $this->filesystem->write('test.txt', 'some content'); - $this->assertTrue(file_exists('gaufrette://filestream/test.txt')); + $this->assertFileExists('gaufrette://filestream/test.txt'); $this->filesystem->delete('test.txt'); - $this->assertFalse(file_exists('gaufrette://filestream/test.txt')); + $this->assertFileNotExists('gaufrette://filestream/test.txt'); } /** @@ -201,7 +201,7 @@ public function shouldCopyFile() public function shouldCreateNewFile($mode) { $fileHandler = fopen('gaufrette://filestream/test.txt', $mode); - $this->assertTrue(file_exists('gaufrette://filestream/test.txt')); + $this->assertFileExists('gaufrette://filestream/test.txt'); } public static function modesProvider() diff --git a/tests/Gaufrette/Functional/FileStream/LocalTest.php b/tests/Gaufrette/Functional/FileStream/LocalTest.php index f52641f88..4837bedc7 100644 --- a/tests/Gaufrette/Functional/FileStream/LocalTest.php +++ b/tests/Gaufrette/Functional/FileStream/LocalTest.php @@ -44,6 +44,6 @@ public function tearDown() public function shouldSupportsDirectory() { $this->assertFileExists('gaufrette://filestream/subdir'); - $this->assertTrue(is_dir('gaufrette://filestream/subdir')); + $this->assertDirectoryExists('gaufrette://filestream/subdir'); } } From 0c6f379075f3b8b2523d04bbfa78d267116371e7 Mon Sep 17 00:00:00 2001 From: Nicolas MURE Date: Fri, 5 Jan 2018 16:29:24 +0100 Subject: [PATCH 347/426] bump branch alias to 0.6.x-dev --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 5ba7edea6..958392204 100644 --- a/composer.json +++ b/composer.json @@ -64,7 +64,7 @@ }, "extra": { "branch-alias": { - "dev-master": "0.5.x-dev" + "dev-master": "0.6.x-dev" } } } From 791882820cc18917e20a0de6157dc572033d7eb2 Mon Sep 17 00:00:00 2001 From: Nicolas MURE Date: Fri, 5 Jan 2018 16:39:28 +0100 Subject: [PATCH 348/426] update changelog --- CHANGELOG.md | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4bd966bf..03838be7b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,29 @@ +v0.5 +==== + +## New features +- Added support for calculated size for Azure Blob Storage #523 +- GridFS Support for Metadata Retrieval after Write #535 + +## Changes +- Test case for AwsS3 now inherits common test case #514 +- Run azure tests on appveyor #512 +- Bump PHPUnit to ^5.6.8 #529 +- Use composer's autoload-dev #530 +- Drop HHVM support + sync docker conf with Travis #528 +- Refactoring tests to have more detailed failure messages #542 + +## Fixes +- Documentation #510 +- Typos #506, #538 +- Fix incomplete tear down phase for AwsS3Test #516 +- Fix FTP tests + bug in PhpseclibSftp::fetchKeys() #527 +- fix travis build for php 5.6 #543 +- Quickfix for Adapter/AwsS3, check if count() call is allowed #544 + +Thank you @andreasschacht, @bsperduto, @carusogabriel, @dawkaa, @gerkestam2, +@GrahamCampbell, @Lctrs, @nicolasmure, @NiR- for your contributions ! + v0.4 ==== From aaa66dcf298d313e7ae3f525714923fcfd787e94 Mon Sep 17 00:00:00 2001 From: Albin Kerouanton Date: Fri, 5 Jan 2018 18:33:40 +0100 Subject: [PATCH 349/426] Fix Dockerfile for php 7.0 --- Dockerfile-php70 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Dockerfile-php70 b/Dockerfile-php70 index 0b8b24a7c..2fe95a537 100644 --- a/Dockerfile-php70 +++ b/Dockerfile-php70 @@ -26,9 +26,9 @@ RUN set -xe \ ssh2 \ && apk del .build-deps -COPY php.ini /usr/local/etc/php/php.ini +COPY docker/php.ini /usr/local/etc/php/php.ini -COPY install-composer.sh /usr/local/bin/docker-gaufrette-install-composer +COPY docker/install-composer.sh /usr/local/bin/docker-gaufrette-install-composer RUN chmod +x /usr/local/bin/docker-gaufrette-install-composer RUN set -xe \ From 91337a0e2384c7e11aba00209ce76279d35a8a87 Mon Sep 17 00:00:00 2001 From: Nicolas MURE Date: Tue, 20 Mar 2018 17:12:48 +0100 Subject: [PATCH 350/426] fix appveyor build --- appveyor.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index c2a06076a..67a3f8f31 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -39,13 +39,13 @@ install: - IF EXIST c:\php (SET PHP=0) ELSE (mkdir c:\php) - cd c:\php - IF %PHP%==1 cinst -y OpenSSL.Light - - IF %PHP%==1 appveyor DownloadFile http://windows.php.net/downloads/releases/archives/php-5.6.17-nts-Win32-VC11-x86.zip - - IF %PHP%==1 7z x php-5.6.17-nts-Win32-VC11-x86.zip -y >nul + - IF %PHP%==1 curl -fsSL -o php-5.6.33-Win32-VC11-x86.zip https://windows.php.net/downloads/releases/archives/php-5.6.33-Win32-VC11-x86.zip + - IF %PHP%==1 7z x php-5.6.33-Win32-VC11-x86.zip -y >nul - IF %PHP%==1 del /Q *.zip - - IF %PHP%==1 appveyor DownloadFile http://curl.haxx.se/ca/cacert.pem + - IF %PHP%==1 curl -fsSL -o cacert.pem https://curl.haxx.se/ca/cacert.pem - IF %PHP%==1 cd ext - - IF %PHP%==1 appveyor DownloadFile http://windows.php.net/downloads/pecl/releases/mongodb/1.2.9/php_mongodb-1.2.9-5.6-nts-vc11-x86.zip - - IF %PHP%==1 7z x php_mongodb-1.2.9-5.6-nts-vc11-x86.zip php_mongodb.dll -y >nul + - IF %PHP%==1 curl -fsSL -o php_mongodb-1.2.9-5.6-ts-vc11-x86.zip https://windows.php.net/downloads/pecl/releases/mongodb/1.2.9/php_mongodb-1.2.9-5.6-ts-vc11-x86.zip + - IF %PHP%==1 7z x php_mongodb-1.2.9-5.6-ts-vc11-x86.zip php_mongodb.dll -y >nul - IF %PHP%==1 del /Q *.zip - IF %PHP%==1 cd .. - IF %PHP%==1 copy php.ini-production php.ini /Y @@ -59,7 +59,7 @@ install: - IF %PHP%==1 echo extension=php_curl.dll >> php.ini - IF %PHP%==1 echo openssl.cafile=c:\php\cacert.pem >> php.ini - cd C:\projects\gaufrette - - appveyor DownloadFile https://getcomposer.org/composer.phar + - curl -fsSL -o composer.phar https://getcomposer.org/composer.phar - php composer.phar update --prefer-dist --no-interaction --no-progress --no-suggest --ansi test_script: From 04e7b3f233bf5bba0a77975f43de84846c1280c4 Mon Sep 17 00:00:00 2001 From: z38 Date: Sun, 25 Mar 2018 11:43:25 +0200 Subject: [PATCH 351/426] Add support for major release of Azure Blob Storage SDK --- composer.json | 3 +-- doc/adapters/azure-blob-storage.md | 6 +----- src/Gaufrette/Adapter/AzureBlobStorage.php | 8 +++++++- .../Adapter/AzureBlobStorage/BlobProxyFactory.php | 8 +++++++- .../Adapter/AzureMultiContainerBlobStorageTest.php | 4 ++-- tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php | 2 +- 6 files changed, 19 insertions(+), 12 deletions(-) diff --git a/composer.json b/composer.json index 958392204..4f2bfe287 100644 --- a/composer.json +++ b/composer.json @@ -34,8 +34,7 @@ "mikey179/vfsStream": "~1.2.0", "league/flysystem": "~1.0", "mongodb/mongodb": "^1.1", - "microsoft/windowsazure": "~0.4", - "microsoft/azure-storage": "~0.15.0", + "microsoft/azure-storage-blob": "^1.0", "akeneo/phpspec-skip-example-extension": "~1.2" }, "suggest": { diff --git a/doc/adapters/azure-blob-storage.md b/doc/adapters/azure-blob-storage.md index cd53e0ee7..38a3df98e 100644 --- a/doc/adapters/azure-blob-storage.md +++ b/doc/adapters/azure-blob-storage.md @@ -4,15 +4,11 @@ currentMenu: azure-blob-storage # AzureBlobStorage -First, you will need to install the adapter: +Azure Blob Storage is the storage service provided by Microsoft Windows Azure cloud environment. First, you will need to install the adapter: ```bash composer require gaufrette/azure-blob-storage-adapter ``` -Azure Blob Storage is the storage service provided by Microsoft Windows Azure cloud environment. To use this adapter -you need to install the [Azure SDK for php](http://www.windowsazure.com/en-us/develop/php/common-tasks/download-php-sdk/) -into your project. - To instantiate the `AzureBlobStorage` adapter you need a `BlobProxyFactoryInterface` instance (you can use the default `BlobProxyFactory` class) and a connection string. The connection string should follow this prototype: diff --git a/src/Gaufrette/Adapter/AzureBlobStorage.php b/src/Gaufrette/Adapter/AzureBlobStorage.php index a1935a30a..e1ab1e57b 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage.php @@ -8,6 +8,7 @@ use MicrosoftAzure\Storage\Blob\Models\Blob; use MicrosoftAzure\Storage\Blob\Models\Container; use MicrosoftAzure\Storage\Blob\Models\CreateBlobOptions; +use MicrosoftAzure\Storage\Blob\Models\CreateBlockBlobOptions; use MicrosoftAzure\Storage\Blob\Models\CreateContainerOptions; use MicrosoftAzure\Storage\Blob\Models\DeleteContainerOptions; use MicrosoftAzure\Storage\Blob\Models\ListBlobsOptions; @@ -187,7 +188,12 @@ public function write($key, $content) $this->init(); list($containerName, $key) = $this->tokenizeKey($key); - $options = new CreateBlobOptions(); + if (class_exists(CreateBlockBlobOptions::class)) { + $options = new CreateBlockBlobOptions(); + } else { + // for microsoft/azure-storage < 1.0 + $options = new CreateBlobOptions(); + } if ($this->detectContentType) { $contentType = $this->guessContentType($content); diff --git a/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php b/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php index 0db48794c..8eea93c24 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php @@ -2,6 +2,7 @@ namespace Gaufrette\Adapter\AzureBlobStorage; +use MicrosoftAzure\Storage\Blob\BlobRestProxy; use MicrosoftAzure\Storage\Common\ServicesBuilder; /** @@ -29,6 +30,11 @@ public function __construct($connectionString) */ public function create() { - return ServicesBuilder::getInstance()->createBlobService($this->connectionString); + if (class_exists(ServicesBuilder::class)) { + // for microsoft/azure-storage < 1.0 + return ServicesBuilder::getInstance()->createBlobService($this->connectionString); + } else { + return BlobRestProxy::createBlobService($this->connectionString); + } } } diff --git a/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php index 8fba0dad0..00d16d80f 100644 --- a/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php +++ b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php @@ -213,7 +213,7 @@ public function shouldKeepFileObjectInRegister() * @test * @group functional */ - public function shouldWrtieToSameFile() + public function shouldWriteToSameFile() { $path = $this->createUniqueContainerName('container') . '/somefile'; @@ -223,7 +223,7 @@ public function shouldWrtieToSameFile() $FileObjectB = $this->filesystem->createFile($path); $FileObjectB->setContent('DEF'); - $this->assertEquals('DEF', $FileObjectB->getContent()); + $this->assertEquals('DEF', $FileObjectA->getContent()); } private function createUniqueContainerName($prefix) diff --git a/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php b/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php index f162353ab..1e53e18fb 100644 --- a/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php +++ b/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php @@ -211,6 +211,6 @@ public function shouldWriteToSameFile() $FileObjectB = $this->filesystem->createFile('somefile'); $FileObjectB->setContent('DEF'); - $this->assertEquals('DEF', $FileObjectB->getContent()); + $this->assertEquals('DEF', $FileObjectA->getContent()); } } From 1d16f627f6edb62d5c692fdfbeda0ef0067dbf0c Mon Sep 17 00:00:00 2001 From: Nicolas MURE Date: Mon, 26 Mar 2018 13:48:11 +0200 Subject: [PATCH 352/426] prepare v0.6 release --- CHANGELOG.md | 12 ++++++++++++ composer.json | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 03838be7b..2f003e38b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,15 @@ +v0.6 +==== + +## Changes +- Add support for major release of Azure Blob Storage SDK [#558](https://github.com/KnpLabs/Gaufrette/pull/558) + +## Fixes +- Fix Dockerfile for php 7.0 [aaa66dc](https://github.com/KnpLabs/Gaufrette/commit/aaa66dcf298d313e7ae3f525714923fcfd787e94) +- Fix appveyor build [#562](https://github.com/KnpLabs/Gaufrette/pull/562) + +Thank you @nicolasmure, @NiR- and @z38 for your contributions! + v0.5 ==== diff --git a/composer.json b/composer.json index 4f2bfe287..3d327d9ab 100644 --- a/composer.json +++ b/composer.json @@ -63,7 +63,7 @@ }, "extra": { "branch-alias": { - "dev-master": "0.6.x-dev" + "dev-master": "0.7.x-dev" } } } From 50d54855ce8c3613f7b4adb4f023bba78aeab8b7 Mon Sep 17 00:00:00 2001 From: Tristan Bessoussa Date: Wed, 18 Apr 2018 10:53:35 +0200 Subject: [PATCH 353/426] Add PutObjectAcl in the required permission Without this permission, the application can't create a media in the bucket. --- doc/adapters/aws-s3.md | 1 + 1 file changed, 1 insertion(+) diff --git a/doc/adapters/aws-s3.md b/doc/adapters/aws-s3.md index 5af8faa35..b57145ed3 100644 --- a/doc/adapters/aws-s3.md +++ b/doc/adapters/aws-s3.md @@ -74,6 +74,7 @@ for production environment. "Effect": "Allow", "Action": [ "s3:PutObject", + "s3:PutObjectAcl", "s3:GetObject", "s3:DeleteObject" ], From 172a352de9eb98e09444908b1fd19708b8e7f5fb Mon Sep 17 00:00:00 2001 From: Andrey Bolonin Date: Thu, 19 Apr 2018 14:55:59 +0300 Subject: [PATCH 354/426] Update .travis.yml --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 4602e2c43..59f918783 100644 --- a/.travis.yml +++ b/.travis.yml @@ -29,6 +29,7 @@ matrix: - php: '5.6' - php: '7.0' - php: '7.1' + - php: '7.2' before_install: - echo "extension = mongodb.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; From ac530108d3cb9f432ccbaf9d82fee4735d670075 Mon Sep 17 00:00:00 2001 From: Jakob Stoeck Date: Wed, 25 Apr 2018 20:57:22 +0200 Subject: [PATCH 355/426] docs fix `AwsS3PresignedUrlResolver` was once incorrectly written as `AwsS3PresignedResolver`, it also needs 4 parameters. Switched example to `AwsS3PublicUrlResolver`. --- doc/extras.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/extras.md b/doc/extras.md index 0adeca737..eb5b07926 100644 --- a/doc/extras.md +++ b/doc/extras.md @@ -24,13 +24,13 @@ Filesystem decorator providing `resolve()` method to resolve an object path into use Gaufrette\Filesystem; use Gaufrette\Adapter\AwsS3; use Gaufrette\Extras\Resolvable\ResolvableFilesystem; -use Gaufrette\Extras\Resolvable\Resolver\AwsS3PresignedResolver; +use Gaufrette\Extras\Resolvable\Resolver\AwsS3PublicUrlResolver; $client = // AwsS3 client instantiation $decorated = new Filesystem(new AwsS3($client, 'my_bucket', ['directory' => 'root/dir'])); $filesystem = new ResolvableFilesystem( $decorated, - new AwsS3PresignedUrlResolver($client, 'my_bucket', 'root/dir') + new AwsS3PublicUrlResolver($client, 'my_bucket', 'root/dir') ); // should return something like "https://eu-west-1.blabla.aws.com/my_bucket/root/dir/foo/bar.png?token From 5be9eae2092cfede628c15562681010fc03b32fd Mon Sep 17 00:00:00 2001 From: Teoh Han Hui Date: Mon, 28 May 2018 18:03:01 +0200 Subject: [PATCH 356/426] Ensure correct return type from Flysystem adapter "exists" method --- spec/Gaufrette/Adapter/FlysystemSpec.php | 7 +++++++ src/Gaufrette/Adapter/Flysystem.php | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/spec/Gaufrette/Adapter/FlysystemSpec.php b/spec/Gaufrette/Adapter/FlysystemSpec.php index cefc46ef4..2fbfbba43 100644 --- a/spec/Gaufrette/Adapter/FlysystemSpec.php +++ b/spec/Gaufrette/Adapter/FlysystemSpec.php @@ -45,6 +45,13 @@ function it_checks_if_file_exists(AdapterInterface $adapter) $this->exists('filename')->shouldReturn(true); } + function it_checks_if_file_exists_when_flysystem_returns_array(AdapterInterface $adapter) + { + $adapter->has('filename')->willReturn(['type' => 'file']); + + $this->exists('filename')->shouldReturn(true); + } + function it_fetches_keys(AdapterInterface $adapter) { $adapter->listContents()->willReturn([[ diff --git a/src/Gaufrette/Adapter/Flysystem.php b/src/Gaufrette/Adapter/Flysystem.php index 0e1c6dead..9e1842e7f 100644 --- a/src/Gaufrette/Adapter/Flysystem.php +++ b/src/Gaufrette/Adapter/Flysystem.php @@ -50,7 +50,7 @@ public function write($key, $content) */ public function exists($key) { - return $this->adapter->has($key); + return (bool) $this->adapter->has($key); } /** From 7eb06fb73db89095f4fcc6b37964599747a5411b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20MICHELET?= Date: Thu, 30 Aug 2018 14:31:23 +0200 Subject: [PATCH 357/426] FilesystemMap::set() should expects FilesystemInterface instead of Filesystem Fix: #575 --- src/Gaufrette/FilesystemMap.php | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Gaufrette/FilesystemMap.php b/src/Gaufrette/FilesystemMap.php index 384e69721..96deae392 100644 --- a/src/Gaufrette/FilesystemMap.php +++ b/src/Gaufrette/FilesystemMap.php @@ -26,12 +26,12 @@ public function all() * Register the given filesystem for the specified domain. * * @param string $domain - * @param Filesystem $filesystem + * @param FilesystemInterface $filesystem * - * @throws InvalidArgumentException when the specified domain contains + * @throws \InvalidArgumentException when the specified domain contains * forbidden characters */ - public function set($domain, Filesystem $filesystem) + public function set($domain, FilesystemInterface $filesystem) { if (!preg_match('/^[-_a-zA-Z0-9]+$/', $domain)) { throw new \InvalidArgumentException(sprintf( @@ -61,9 +61,9 @@ public function has($domain) * * @param string $domain * - * @return Filesystem + * @return FilesystemInterface * - * @throw InvalidArgumentException when there is no filesystem registered + * @throw \InvalidArgumentException when there is no filesystem registered * for the specified domain */ public function get($domain) From a244eddf45125c16d8446827295d4a0b0f92e2b0 Mon Sep 17 00:00:00 2001 From: Nicolas MURE Date: Fri, 7 Sep 2018 13:35:39 +0200 Subject: [PATCH 358/426] fix opencloud tests --- .../Functional/Adapter/OpenCloudTest.php | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/tests/Gaufrette/Functional/Adapter/OpenCloudTest.php b/tests/Gaufrette/Functional/Adapter/OpenCloudTest.php index 322ebed0b..ae459e4b6 100644 --- a/tests/Gaufrette/Functional/Adapter/OpenCloudTest.php +++ b/tests/Gaufrette/Functional/Adapter/OpenCloudTest.php @@ -24,10 +24,22 @@ public function setUp() $this->markTestSkipped('Either RACKSPACE_USER, RACKSPACE_APIKEY and/or RACKSPACE_CONTAINER env vars are missing.'); } - $connection = new Rackspace('https://identity.api.rackspacecloud.com/v2.0/', [ - 'username' => $username, - 'apiKey' => $apiKey, - ]); + $connection = new Rackspace( + 'https://identity.api.rackspacecloud.com/v2.0/', + [ + 'username' => $username, + 'apiKey' => $apiKey, + ], + [ + // Guzzle ships with outdated certs + // @see https://github.com/rackspace/php-opencloud/issues/727 + Rackspace::SSL_CERT_AUTHORITY => 'system', + Rackspace::CURL_OPTIONS => [ + CURLOPT_SSL_VERIFYPEER => true, + CURLOPT_SSL_VERIFYHOST => 2, + ], + ] + ); $this->container = uniqid($container); $this->objectStore = $connection->objectStoreService('cloudFiles', 'IAD', 'publicURL'); From 4d335f49a4e6a8cf73382cb06ea73e776cd13a6a Mon Sep 17 00:00:00 2001 From: Nicolas MURE Date: Thu, 6 Sep 2018 17:56:24 +0200 Subject: [PATCH 359/426] fix branch alias as v0.7.0 is released --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index 3d327d9ab..6bb5e7889 100644 --- a/composer.json +++ b/composer.json @@ -63,7 +63,7 @@ }, "extra": { "branch-alias": { - "dev-master": "0.7.x-dev" + "dev-master": "0.8.x-dev" } } } From 608d10779870ee4ca36a966b0dc4064a4bc8e52c Mon Sep 17 00:00:00 2001 From: Nicolas MURE Date: Fri, 23 Nov 2018 14:50:41 +0100 Subject: [PATCH 360/426] fix appveyor build --- appveyor.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/appveyor.yml b/appveyor.yml index 67a3f8f31..f643f27a5 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -38,7 +38,7 @@ init: install: - IF EXIST c:\php (SET PHP=0) ELSE (mkdir c:\php) - cd c:\php - - IF %PHP%==1 cinst -y OpenSSL.Light + - IF %PHP%==1 cinst -y OpenSSL.Light --version 1.1.1 - IF %PHP%==1 curl -fsSL -o php-5.6.33-Win32-VC11-x86.zip https://windows.php.net/downloads/releases/archives/php-5.6.33-Win32-VC11-x86.zip - IF %PHP%==1 7z x php-5.6.33-Win32-VC11-x86.zip -y >nul - IF %PHP%==1 del /Q *.zip From 38a3de4defe1e8b927f31be4fa0bbc098759f832 Mon Sep 17 00:00:00 2001 From: andreybolonin Date: Sat, 27 Oct 2018 10:53:51 +0300 Subject: [PATCH 361/426] add and fix php 7.3 --- .travis.yml | 1 + src/Gaufrette/Util/Path.php | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 59f918783..7cd44f6c5 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,6 +30,7 @@ matrix: - php: '7.0' - php: '7.1' - php: '7.2' + - php: '7.3' before_install: - echo "extension = mongodb.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; diff --git a/src/Gaufrette/Util/Path.php b/src/Gaufrette/Util/Path.php index d22e1ca5d..eb7e2a0e3 100644 --- a/src/Gaufrette/Util/Path.php +++ b/src/Gaufrette/Util/Path.php @@ -27,13 +27,13 @@ public static function normalize($path) foreach ($parts as $part) { switch ($part) { case '.': - continue; + continue 2; case '..': if (0 !== count($tokens)) { array_pop($tokens); - continue; + continue 2; } elseif (!empty($prefix)) { - continue; + continue 2; } default: $tokens[] = $part; From e6b85c879498a7a65371e44c197532dfe42300a5 Mon Sep 17 00:00:00 2001 From: Nicolas MURE Date: Fri, 12 Jan 2018 16:06:07 +0100 Subject: [PATCH 362/426] drop support for php 5.6 and 7.0 Also bump phpunit to v7 and phpspec to v5 --- .travis.yml | 4 - appveyor.yml | 9 +- composer.json | 8 +- phpspec.yml | 2 +- phpunit.xml.dist | 1 - .../Adapter/AclAwareAmazonS3Spec.php | 28 +++--- spec/Gaufrette/Adapter/AmazonS3Spec.php | 57 ++++++------ spec/Gaufrette/Adapter/AwsS3Spec.php | 4 +- spec/Gaufrette/Adapter/CacheSpec.php | 26 +++--- spec/Gaufrette/Adapter/DoctrineDbalSpec.php | 22 ++--- spec/Gaufrette/Adapter/DropboxSpec.php | 39 +++++---- spec/Gaufrette/Adapter/FtpSpec.php | 5 +- spec/Gaufrette/Adapter/LazyOpenCloudSpec.php | 11 ++- spec/Gaufrette/Adapter/OpenCloudSpec.php | 47 ++++++---- spec/Gaufrette/Adapter/PhpseclibSftpSpec.php | 31 +++---- spec/Gaufrette/FileSpec.php | 40 +++++---- spec/Gaufrette/FilesystemMapSpec.php | 9 +- spec/Gaufrette/FilesystemSpec.php | 86 ++++++++++--------- spec/Gaufrette/StreamWrapperSpec.php | 42 +++++---- .../FileStream/FunctionalTestCase.php | 3 +- 20 files changed, 255 insertions(+), 219 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7cd44f6c5..085c8fc42 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,8 +26,6 @@ env: matrix: include: - - php: '5.6' - - php: '7.0' - php: '7.1' - php: '7.2' - php: '7.3' @@ -38,8 +36,6 @@ before_install: - docker run -p ${SFTP_PORT}:22 -d atmoz/sftp:alpine ${SFTP_USER}:${SFTP_PASSWORD}:::${SFTP_BASE_DIR} - docker run -d --name ftpd_server -p ${FTP_PORT}:21 -p 30000-30009:30000-30009 -e "PUBLICHOST=${FTP_HOST}" stilliard/pure-ftpd - docker exec -it ftpd_server sh -c "(echo ${FTP_PASSWORD}; echo ${FTP_PASSWORD}) | pure-pw useradd ${FTP_USER} -f /etc/pure-ftpd/passwd/pureftpd.passwd -m -u ftpuser -d /home/ftpusers/${FTP_USER}" - - if [[ ${TRAVIS_PHP_VERSION} = 5.* ]]; then sudo apt-get -qq update && sudo apt-get install -y libssh2-1-dev; fi - - if [[ ${TRAVIS_PHP_VERSION} = 5.* ]]; then yes | pecl install -f ssh2 && composer require herzult/php-ssh --no-update --no-scripts; fi install: - composer update --prefer-dist --no-progress --no-suggest --ansi diff --git a/appveyor.yml b/appveyor.yml index f643f27a5..efc4bb3b0 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -39,13 +39,13 @@ install: - IF EXIST c:\php (SET PHP=0) ELSE (mkdir c:\php) - cd c:\php - IF %PHP%==1 cinst -y OpenSSL.Light --version 1.1.1 - - IF %PHP%==1 curl -fsSL -o php-5.6.33-Win32-VC11-x86.zip https://windows.php.net/downloads/releases/archives/php-5.6.33-Win32-VC11-x86.zip - - IF %PHP%==1 7z x php-5.6.33-Win32-VC11-x86.zip -y >nul + - IF %PHP%==1 curl -fsSL -o php-7.2.12-Win32-VC15-x86.zip https://windows.php.net/downloads/releases/archives/php-7.2.12-Win32-VC15-x86.zip + - IF %PHP%==1 7z x php-7.2.12-Win32-VC15-x86.zip -y >nul - IF %PHP%==1 del /Q *.zip - IF %PHP%==1 curl -fsSL -o cacert.pem https://curl.haxx.se/ca/cacert.pem - IF %PHP%==1 cd ext - - IF %PHP%==1 curl -fsSL -o php_mongodb-1.2.9-5.6-ts-vc11-x86.zip https://windows.php.net/downloads/pecl/releases/mongodb/1.2.9/php_mongodb-1.2.9-5.6-ts-vc11-x86.zip - - IF %PHP%==1 7z x php_mongodb-1.2.9-5.6-ts-vc11-x86.zip php_mongodb.dll -y >nul + - IF %PHP%==1 curl -fsSL -o php_mongodb-1.5.3-7.2-ts-vc15-x86.zip https://windows.php.net/downloads/pecl/releases/mongodb/1.5.3/php_mongodb-1.5.3-7.2-ts-vc15-x86.zip + - IF %PHP%==1 7z x php_mongodb-1.5.3-7.2-ts-vc15-x86.zip php_mongodb.dll -y >nul - IF %PHP%==1 del /Q *.zip - IF %PHP%==1 cd .. - IF %PHP%==1 copy php.ini-production php.ini /Y @@ -57,6 +57,7 @@ install: - IF %PHP%==1 echo extension=php_pdo_sqlite.dll >> php.ini - IF %PHP%==1 echo extension=php_fileinfo.dll >> php.ini - IF %PHP%==1 echo extension=php_curl.dll >> php.ini + - IF %PHP%==1 echo extension=php_ftp.dll >> php.ini - IF %PHP%==1 echo openssl.cafile=c:\php\cacert.pem >> php.ini - cd C:\projects\gaufrette - curl -fsSL -o composer.phar https://getcomposer.org/composer.phar diff --git a/composer.json b/composer.json index 6bb5e7889..cd20ebea1 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,7 @@ } ], "require": { - "php": ">=5.6" + "php": ">=7.1" }, "conflict": { "microsoft/windowsazure": "<0.4.3" @@ -26,16 +26,16 @@ "amazonwebservices/aws-sdk-for-php": "1.5.*", "rackspace/php-opencloud" : "^1.9.2", "google/apiclient": "~1.1.3", - "phpspec/phpspec": "~2.4", + "phpspec/phpspec": "~5.1", "phpseclib/phpseclib": "^2.0", "doctrine/dbal": ">=2.3", "dropbox-php/dropbox-php": "*", - "phpunit/phpunit": "^5.6.8", + "phpunit/phpunit": "~7.5", "mikey179/vfsStream": "~1.2.0", "league/flysystem": "~1.0", "mongodb/mongodb": "^1.1", "microsoft/azure-storage-blob": "^1.0", - "akeneo/phpspec-skip-example-extension": "~1.2" + "akeneo/phpspec-skip-example-extension": "^4.0" }, "suggest": { "knplabs/knp-gaufrette-bundle": "to use with Symfony2", diff --git a/phpspec.yml b/phpspec.yml index feb982816..38cc7d8e3 100644 --- a/phpspec.yml +++ b/phpspec.yml @@ -1,2 +1,2 @@ extensions: - Akeneo\SkipExampleExtension: Akeneo\SkipExampleExtension + Akeneo\SkipExampleExtension: ~ diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 9bf58266e..ad116a588 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,7 +8,6 @@ convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" - syntaxCheck="false" bootstrap="vendor/autoload.php" > diff --git a/spec/Gaufrette/Adapter/AclAwareAmazonS3Spec.php b/spec/Gaufrette/Adapter/AclAwareAmazonS3Spec.php index 8898c0d03..febb83553 100644 --- a/spec/Gaufrette/Adapter/AclAwareAmazonS3Spec.php +++ b/spec/Gaufrette/Adapter/AclAwareAmazonS3Spec.php @@ -2,6 +2,8 @@ namespace spec\Gaufrette\Adapter; +use AmazonS3; +use Gaufrette\Adapter; use PhpSpec\ObjectBehavior; class AclAwareAmazonS3Spec extends ObjectBehavior @@ -10,7 +12,7 @@ class AclAwareAmazonS3Spec extends ObjectBehavior * @param \Gaufrette\Adapter $adapter * @param \AmazonS3 $service */ - function let($adapter, $service) + function let(Adapter $adapter, AmazonS3 $service) { $this->beConstructedWith($adapter, $service, 'bucketName'); } @@ -28,7 +30,7 @@ function it_supports_metadata() /** * @param \Gaufrette\Adapter $adapter */ - function it_delegates_read($adapter) + function it_delegates_read(Adapter $adapter) { $adapter->read('filename')->willReturn('some content'); $adapter->read('filename2')->willReturn('other content'); @@ -41,7 +43,7 @@ function it_delegates_read($adapter) * @param \Gaufrette\Adapter $adapter * @param \AmazonS3 $service */ - function it_delegates_rename_and_update_acl($adapter, $service) + function it_delegates_rename_and_update_acl(Adapter $adapter, AmazonS3 $service) { $service ->set_object_acl('bucketName', 'filename2', \AmazonS3::ACL_PRIVATE) @@ -65,7 +67,7 @@ function it_delegates_rename_and_update_acl($adapter, $service) * @param \Gaufrette\Adapter $adapter * @param \AmazonS3 $service */ - function it_does_not_rename_when_cannot_update_acl($adapter, $service) + function it_does_not_rename_when_cannot_update_acl(Adapter $adapter, AmazonS3 $service) { $service ->set_object_acl('bucketName', 'filename2', \AmazonS3::ACL_PRIVATE) @@ -86,7 +88,7 @@ function it_does_not_rename_when_cannot_update_acl($adapter, $service) * @param \Gaufrette\Adapter $adapter * @param \AmazonS3 $service */ - function it_updates_acl_with_users_array_when_rename($adapter, $service) + function it_updates_acl_with_users_array_when_rename(Adapter $adapter, AmazonS3 $service) { $service ->set_object_acl('bucketName', 'filename2', array(array('id' => 'someId', 'permission' => \AmazonS3::GRANT_READ))) @@ -106,7 +108,7 @@ function it_updates_acl_with_users_array_when_rename($adapter, $service) * @param \Gaufrette\Adapter $adapter * @param \AmazonS3 $service */ - function it_delegates_write_and_update_acl($adapter, $service) + function it_delegates_write_and_update_acl(Adapter $adapter, AmazonS3 $service) { $service ->set_object_acl('bucketName', 'filename', \AmazonS3::ACL_PRIVATE) @@ -130,7 +132,7 @@ function it_delegates_write_and_update_acl($adapter, $service) * @param \Gaufrette\Adapter $adapter * @param \AmazonS3 $service */ - function it_does_not_write_when_cannot_update_acl($adapter, $service) + function it_does_not_write_when_cannot_update_acl(Adapter $adapter, AmazonS3 $service) { $service ->set_object_acl('bucketName', 'filename', \AmazonS3::ACL_PRIVATE) @@ -154,7 +156,7 @@ function it_does_not_write_when_cannot_update_acl($adapter, $service) * @param \Gaufrette\Adapter $adapter * @param \AmazonS3 $service */ - function it_updates_acl_with_users_array_when_write($adapter, $service) + function it_updates_acl_with_users_array_when_write(Adapter $adapter, AmazonS3 $service) { $service ->set_object_acl('bucketName', 'filename', array(array('id' => 'someId', 'permission' => \AmazonS3::GRANT_READ))) @@ -173,7 +175,7 @@ function it_updates_acl_with_users_array_when_write($adapter, $service) /** * @param \Gaufrette\Adapter $adapter */ - function it_delegates_exists($adapter) + function it_delegates_exists(Adapter $adapter) { $adapter->exists('filename')->willReturn(true); $adapter->exists('filename2')->willReturn(false); @@ -185,7 +187,7 @@ function it_delegates_exists($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_delegates_mtime($adapter) + function it_delegates_mtime(Adapter $adapter) { $adapter->mtime('filename')->willReturn(1234); $adapter->mtime('filename2')->willReturn(2345); @@ -197,7 +199,7 @@ function it_delegates_mtime($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_delegates_directory_check($adapter) + function it_delegates_directory_check(Adapter $adapter) { $adapter->isDirectory('filename')->willReturn(true); $adapter->isDirectory('filename2')->willReturn(false); @@ -209,7 +211,7 @@ function it_delegates_directory_check($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_delegates_keys($adapter) + function it_delegates_keys(Adapter $adapter) { $adapter->keys()->willReturn(array('filename', 'filename2')); @@ -220,7 +222,7 @@ function it_delegates_keys($adapter) * @param \spec\Gaufrette\Adapter\TestDelegateAdapter $extendedAdapter * @param \AmazonS3 $service */ - function it_delegates_metadata_handling($extendedAdapter, $service) + function it_delegates_metadata_handling(TestDelegateAdapter $extendedAdapter, AmazonS3 $service) { $this->beConstructedWith($extendedAdapter, $service, 'bucketName'); diff --git a/spec/Gaufrette/Adapter/AmazonS3Spec.php b/spec/Gaufrette/Adapter/AmazonS3Spec.php index d81a5c754..037775a68 100644 --- a/spec/Gaufrette/Adapter/AmazonS3Spec.php +++ b/spec/Gaufrette/Adapter/AmazonS3Spec.php @@ -2,6 +2,7 @@ namespace spec\Gaufrette\Adapter; +use AmazonS3; use PhpSpec\ObjectBehavior; use Prophecy\Argument; @@ -10,7 +11,7 @@ class AmazonS3Spec extends ObjectBehavior /** * @param \AmazonS3 $service */ - function let($service) + function let(AmazonS3 $service) { $this->beConstructedWith($service, 'bucketName'); } @@ -28,7 +29,7 @@ function it_supports_metadata() /** * @param \AmazonS3 $service */ - function it_reads_file($service) + function it_reads_file(AmazonS3 $service) { $options = array( 'range' => 12, @@ -59,7 +60,7 @@ function it_reads_file($service) /** * @param \AmazonS3 $service */ - function it_returns_false_when_cannot_read($service) + function it_returns_false_when_cannot_read(AmazonS3 $service) { $service ->if_bucket_exists('bucketName') @@ -82,7 +83,7 @@ function it_returns_false_when_cannot_read($service) /** * @param \AmazonS3 $service */ - function it_is_verbose_and_throws_exceptions_when_read($service) + function it_is_verbose_and_throws_exceptions_when_read(AmazonS3 $service) { $service ->if_bucket_exists('bucketName') @@ -104,7 +105,7 @@ function it_is_verbose_and_throws_exceptions_when_read($service) /** * @param \AmazonS3 $service */ - function it_rename_file($service) + function it_rename_file(AmazonS3 $service) { $service ->if_bucket_exists('bucketName') @@ -143,7 +144,7 @@ function it_rename_file($service) /** * @param \AmazonS3 $service */ - function it_is_verbose_and_throws_exceptions_when_rename($service) + function it_is_verbose_and_throws_exceptions_when_rename(AmazonS3 $service) { $service ->if_bucket_exists('bucketName') @@ -161,7 +162,7 @@ function it_is_verbose_and_throws_exceptions_when_rename($service) /** * @param \AmazonS3 $service */ - function it_returns_false_when_cannot_rename($service) + function it_returns_false_when_cannot_rename(AmazonS3 $service) { $service ->if_bucket_exists('bucketName') @@ -190,7 +191,7 @@ function it_returns_false_when_cannot_rename($service) /** * @param \AmazonS3 $service */ - function it_should_write_file($service) + function it_should_write_file(AmazonS3 $service) { $service ->if_bucket_exists('bucketName') @@ -217,7 +218,7 @@ function it_should_write_file($service) /** * @param \AmazonS3 $service */ - function it_returns_false_when_cannot_write($service) + function it_returns_false_when_cannot_write(AmazonS3 $service) { $service ->if_bucket_exists('bucketName') @@ -243,7 +244,7 @@ function it_returns_false_when_cannot_write($service) /** * @param \AmazonS3 $service */ - function it_is_verbose_and_throws_exceptions_when_write($service) + function it_is_verbose_and_throws_exceptions_when_write(AmazonS3 $service) { $service ->if_bucket_exists('bucketName') @@ -261,7 +262,7 @@ function it_is_verbose_and_throws_exceptions_when_write($service) /** * @param \AmazonS3 $service */ - function it_should_check_if_file_exists($service) + function it_should_check_if_file_exists(AmazonS3 $service) { $service ->if_bucket_exists('bucketName') @@ -279,7 +280,7 @@ function it_should_check_if_file_exists($service) /** * @param \AmazonS3 $service */ - function it_is_verbose_and_throws_exceptions_when_file_exists($service) + function it_is_verbose_and_throws_exceptions_when_file_exists(AmazonS3 $service) { $service ->if_bucket_exists('bucketName') @@ -297,7 +298,7 @@ function it_is_verbose_and_throws_exceptions_when_file_exists($service) /** * @param \AmazonS3 $service */ - function it_should_get_file_mtime($service) + function it_should_get_file_mtime(AmazonS3 $service) { $metadata = array('acl' => \AmazonS3::ACL_PUBLIC); $service @@ -323,7 +324,7 @@ function it_should_get_file_mtime($service) /** * @param \AmazonS3 $service */ - function it_returns_false_when_cannot_fetch_mtime($service) + function it_returns_false_when_cannot_fetch_mtime(AmazonS3 $service) { $service ->if_bucket_exists('bucketName') @@ -347,7 +348,7 @@ function it_returns_false_when_cannot_fetch_mtime($service) /** * @param \AmazonS3 $service */ - function it_is_verbose_and_throws_exceptions_when_fetch_mtime($service) + function it_is_verbose_and_throws_exceptions_when_fetch_mtime(AmazonS3 $service) { $service ->if_bucket_exists('bucketName') @@ -365,7 +366,7 @@ function it_is_verbose_and_throws_exceptions_when_fetch_mtime($service) /** * @param \AmazonS3 $service */ - function it_should_delete_file($service) + function it_should_delete_file(AmazonS3 $service) { $metadata = array('acl' => \AmazonS3::ACL_PRIVATE); @@ -390,7 +391,7 @@ function it_should_delete_file($service) /** * @param \AmazonS3 $service */ - function it_is_verbose_and_throws_exceptions_when_fetch_delete($service) + function it_is_verbose_and_throws_exceptions_when_fetch_delete(AmazonS3 $service) { $service ->if_bucket_exists('bucketName') @@ -411,7 +412,7 @@ function it_is_verbose_and_throws_exceptions_when_fetch_delete($service) /** * @param \AmazonS3 $service */ - function it_returns_false_when_cannot_delete($service) + function it_returns_false_when_cannot_delete(AmazonS3 $service) { $service ->if_bucket_exists('bucketName') @@ -433,7 +434,7 @@ function it_returns_false_when_cannot_delete($service) /** * @param \AmazonS3 $service */ - function it_should_get_keys($service) + function it_should_get_keys(AmazonS3 $service) { $service ->if_bucket_exists('bucketName') @@ -452,7 +453,7 @@ function it_should_get_keys($service) /** * @param \AmazonS3 $service */ - function it_is_verbose_and_throws_exceptions_when_fetch_keys($service) + function it_is_verbose_and_throws_exceptions_when_fetch_keys(AmazonS3 $service) { $service ->if_bucket_exists('bucketName') @@ -469,7 +470,7 @@ function it_is_verbose_and_throws_exceptions_when_fetch_keys($service) /** * @param \AmazonS3 $service */ - function it_should_handle_dirs($service) + function it_should_handle_dirs(AmazonS3 $service) { $service ->if_bucket_exists('bucketName') @@ -496,7 +497,7 @@ function it_should_handle_dirs($service) /** * @param \AmazonS3 $service */ - function it_should_fail_when_bucket_does_not_exist($service) + function it_should_fail_when_bucket_does_not_exist(AmazonS3 $service) { $service ->if_bucket_exists('bucketName') @@ -535,7 +536,7 @@ function it_should_fail_when_bucket_does_not_exist($service) /** * @param \AmazonS3 $service */ - function it_creates_bucket_if_create_mode_is_enabled($service) + function it_creates_bucket_if_create_mode_is_enabled(AmazonS3 $service) { $service->set_region(Argument::any())->shouldBeCalled(); $service @@ -560,7 +561,7 @@ function it_creates_bucket_if_create_mode_is_enabled($service) /** * @param \AmazonS3 $service */ - function it_fails_when_cannot_create_bucket($service) + function it_fails_when_cannot_create_bucket(AmazonS3 $service) { $service ->if_bucket_exists('bucketName') @@ -582,7 +583,7 @@ function it_fails_when_cannot_create_bucket($service) /** * @param \AmazonS3 $service */ - function it_allows_to_configure_reqion($service) + function it_allows_to_configure_reqion(AmazonS3 $service) { $service ->if_bucket_exists('bucketName') @@ -604,7 +605,7 @@ function it_allows_to_configure_reqion($service) /** * @param \AmazonS3 $service */ - function it_allows_to_configure_region_for_bucket($service) + function it_allows_to_configure_region_for_bucket(AmazonS3 $service) { $service->set_region(Argument::any())->shouldBeCalled(); $service @@ -628,7 +629,7 @@ function it_allows_to_configure_region_for_bucket($service) /** * @param \AmazonS3 $service */ - function it_allows_to_configure_acl($service) + function it_allows_to_configure_acl(AmazonS3 $service) { $this->setAcl('123abc'); $service @@ -656,7 +657,7 @@ function it_allows_to_configure_acl($service) /** * @param \AmazonS3 $service */ - function its_file_metadata_acl_are_more_important_than_global_acl_config($service) + function its_file_metadata_acl_are_more_important_than_global_acl_config(AmazonS3 $service) { $this->setAcl('123abc'); $service diff --git a/spec/Gaufrette/Adapter/AwsS3Spec.php b/spec/Gaufrette/Adapter/AwsS3Spec.php index 2f716dba0..7dd38a8f9 100644 --- a/spec/Gaufrette/Adapter/AwsS3Spec.php +++ b/spec/Gaufrette/Adapter/AwsS3Spec.php @@ -2,16 +2,16 @@ namespace spec\Gaufrette\Adapter; +use Aws\S3\S3Client; use Gaufrette\Adapter\MimeTypeProvider; use PhpSpec\ObjectBehavior; -use Prophecy\Argument; class AwsS3Spec extends ObjectBehavior { /** * @param \Aws\S3\S3Client $service */ - function let($service) + function let(S3Client $service) { $this->beConstructedWith($service, 'bucketName'); } diff --git a/spec/Gaufrette/Adapter/CacheSpec.php b/spec/Gaufrette/Adapter/CacheSpec.php index cda8c1a17..1dc36146e 100644 --- a/spec/Gaufrette/Adapter/CacheSpec.php +++ b/spec/Gaufrette/Adapter/CacheSpec.php @@ -2,6 +2,7 @@ namespace spec\Gaufrette\Adapter; +use Gaufrette\Adapter; use PhpSpec\ObjectBehavior; class CacheSpec extends ObjectBehavior @@ -10,7 +11,7 @@ class CacheSpec extends ObjectBehavior * @param \Gaufrette\Adapter $source * @param \Gaufrette\Adapter $cache */ - function let($source, $cache) + function let(Adapter $source, Adapter $cache) { $this->beConstructedWith($source, $cache); } @@ -29,7 +30,10 @@ function it_supports_metadata() * @param \spec\Gaufrette\Adapter\CacheTestExtendedAdapter $extendedSource * @param \spec\Gaufrette\Adapter\CacheTestExtendedAdapter $extendedCache */ - function it_handles_metadata_when_cached_adapters_supports_metadata($extendedSource, $extendedCache) + function it_handles_metadata_when_cached_adapters_supports_metadata( + CacheTestExtendedAdapter $extendedSource, + CacheTestExtendedAdapter $extendedCache + ) { $extendedSource->setMetadata('filename', array('metadata'))->shouldBeCalled(); $extendedCache->setMetadata('filename', array('metadata'))->shouldBeCalled(); @@ -43,7 +47,7 @@ function it_handles_metadata_when_cached_adapters_supports_metadata($extendedSou /** * @param \Gaufrette\Adapter $source */ - function it_delegates_is_directory_check_to_source($source) + function it_delegates_is_directory_check_to_source(Adapter $source) { $source->isDirectory('filename')->shouldBeCalled()->willReturn(true); @@ -54,7 +58,7 @@ function it_delegates_is_directory_check_to_source($source) * @param \Gaufrette\Adapter $source * @param \Gaufrette\Adapter $cache */ - function it_reads_from_cache_adapter($source, $cache) + function it_reads_from_cache_adapter(Adapter $source, Adapter $cache) { $source->read('filename')->shouldNotBeCalled(); $cache->read('filename')->shouldBeCalled()->willReturn('some content'); @@ -69,7 +73,7 @@ function it_reads_from_cache_adapter($source, $cache) * @param \Gaufrette\Adapter $source * @param \Gaufrette\Adapter $cache */ - function it_update_cache_adapter_when_source_file_is_modified($source, $cache) + function it_update_cache_adapter_when_source_file_is_modified(Adapter $source, Adapter $cache) { $source->read('filename')->shouldBeCalled()->willReturn('some other content'); $cache->read('filename')->shouldNotBeCalled(); @@ -85,7 +89,7 @@ function it_update_cache_adapter_when_source_file_is_modified($source, $cache) * @param \Gaufrette\Adapter $source * @param \Gaufrette\Adapter $cache */ - function it_rename_file_in_source_and_cache($source, $cache) + function it_rename_file_in_source_and_cache(Adapter $source, Adapter $cache) { $source->rename('filename', 'filename2')->shouldBeCalled()->willReturn(true); $cache->rename('filename', 'filename2')->shouldBeCalled()->willReturn(true); @@ -97,7 +101,7 @@ function it_rename_file_in_source_and_cache($source, $cache) * @param \Gaufrette\Adapter $source * @param \Gaufrette\Adapter $cache */ - function it_writes_file_to_source_and_cache($source, $cache) + function it_writes_file_to_source_and_cache(Adapter $source, Adapter $cache) { $source->write('filename', 'some content')->shouldBeCalled()->willReturn(12); $cache->write('filename', 'some content')->shouldBeCalled()->willReturn(12); @@ -109,7 +113,7 @@ function it_writes_file_to_source_and_cache($source, $cache) * @param \Gaufrette\Adapter $source * @param \Gaufrette\Adapter $cache */ - function it_deletes_file_from_source_and_cache($source, $cache) + function it_deletes_file_from_source_and_cache(Adapter $source, Adapter $cache) { $source->delete('filename')->shouldBeCalled()->willReturn(true); $cache->delete('filename')->shouldBeCalled()->willReturn(true); @@ -120,7 +124,7 @@ function it_deletes_file_from_source_and_cache($source, $cache) /** * @param \Gaufrette\Adapter $source */ - function it_check_if_exists_in_source($source) + function it_check_if_exists_in_source(Adapter $source) { $source->exists('filename')->shouldBeCalled()->willReturn(true); @@ -130,7 +134,7 @@ function it_check_if_exists_in_source($source) /** * @param \Gaufrette\Adapter $source */ - function it_get_mtime_from_source($source) + function it_get_mtime_from_source(Adapter $source) { $source->mtime('filename')->shouldBeCalled()->willReturn(1234); @@ -140,7 +144,7 @@ function it_get_mtime_from_source($source) /** * @param \Gaufrette\Adapter $source */ - function it_get_keys_from_source($source) + function it_get_keys_from_source(Adapter $source) { $source->keys()->willReturn(array('filename2', 'filename1', 'filename')); diff --git a/spec/Gaufrette/Adapter/DoctrineDbalSpec.php b/spec/Gaufrette/Adapter/DoctrineDbalSpec.php index 81f7fe221..09d5a3096 100644 --- a/spec/Gaufrette/Adapter/DoctrineDbalSpec.php +++ b/spec/Gaufrette/Adapter/DoctrineDbalSpec.php @@ -5,6 +5,8 @@ //hack - mock php built-in functions require_once 'functions.php'; +use Doctrine\DBAL\Connection; +use Doctrine\DBAL\Driver\Statement; use PhpSpec\ObjectBehavior; use Prophecy\Argument; @@ -13,7 +15,7 @@ class DoctrineDbalSpec extends ObjectBehavior /** * @param \Doctrine\DBAL\Connection $connection */ - function let($connection) + function let(Connection $connection) { $this->beConstructedWith($connection, 'someTableName'); } @@ -36,7 +38,7 @@ function it_does_not_handle_directories() /** * @param \Doctrine\DBAL\Connection $connection */ - function it_checks_if_file_exists($connection) + function it_checks_if_file_exists(Connection $connection) { $connection ->quoteIdentifier(Argument::any()) @@ -56,7 +58,7 @@ function it_checks_if_file_exists($connection) /** * @param \Doctrine\DBAL\Connection $connection */ - function it_writes_to_new_file($connection) + function it_writes_to_new_file(Connection $connection) { $connection ->quoteIdentifier(Argument::any()) @@ -81,7 +83,7 @@ function it_writes_to_new_file($connection) /** * @param \Doctrine\DBAL\Connection $connection */ - function it_write_file($connection) + function it_write_file(Connection $connection) { $connection ->quoteIdentifier(Argument::any()) @@ -108,7 +110,7 @@ function it_write_file($connection) /** * @param \Doctrine\DBAL\Connection $connection */ - function it_reads_file($connection) + function it_reads_file(Connection $connection) { $connection ->quoteIdentifier(Argument::any()) @@ -123,7 +125,7 @@ function it_reads_file($connection) /** * @param \Doctrine\DBAL\Connection $connection */ - function it_calculates_checksum($connection) + function it_calculates_checksum(Connection $connection) { $connection ->quoteIdentifier(Argument::any()) @@ -138,7 +140,7 @@ function it_calculates_checksum($connection) /** * @param \Doctrine\DBAL\Connection $connection */ - function it_gets_mtime($connection) + function it_gets_mtime(Connection $connection) { $connection ->quoteIdentifier(Argument::any()) @@ -153,7 +155,7 @@ function it_gets_mtime($connection) /** * @param \Doctrine\DBAL\Connection $connection */ - function it_renames_file($connection) + function it_renames_file(Connection $connection) { $connection ->quoteIdentifier(Argument::any()) @@ -177,7 +179,7 @@ function it_renames_file($connection) * @param \Doctrine\DBAL\Connection $connection * @param \Doctrine\DBAL\Statement $stmt */ - function it_get_keys($connection, $stmt) + function it_get_keys(Connection $connection, Statement $stmt) { $stmt->fetchAll(\PDO::FETCH_COLUMN)->willReturn(array('filename', 'filename1', 'filename2')); $connection @@ -193,7 +195,7 @@ function it_get_keys($connection, $stmt) /** * @param \Doctrine\DBAL\Connection $connection */ - function it_deletes_file($connection) + function it_deletes_file(Connection $connection) { $connection ->quoteIdentifier(Argument::any()) diff --git a/spec/Gaufrette/Adapter/DropboxSpec.php b/spec/Gaufrette/Adapter/DropboxSpec.php index 7aa78571c..788673445 100644 --- a/spec/Gaufrette/Adapter/DropboxSpec.php +++ b/spec/Gaufrette/Adapter/DropboxSpec.php @@ -2,6 +2,7 @@ namespace spec\Gaufrette\Adapter; +use Dropbox_API; use PhpSpec\ObjectBehavior; use Prophecy\Argument; @@ -10,7 +11,7 @@ class DropboxSpec extends ObjectBehavior /** * @param \Dropbox_API $dropbox */ - function let($dropbox) + function let(Dropbox_API $dropbox) { $this->beConstructedWith($dropbox); } @@ -23,7 +24,7 @@ function it_is_adapter() /** * @param \Dropbox_API $dropbox */ - function it_reads_file($dropbox) + function it_reads_file(Dropbox_API $dropbox) { $dropbox->getFile('filename')->willReturn('some content'); @@ -33,7 +34,7 @@ function it_reads_file($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_does_not_mask_exception_from_client_during_read($dropbox) + function it_does_not_mask_exception_from_client_during_read(Dropbox_API $dropbox) { $dropbox->getFile('filename')->willThrow(new \RuntimeException('read')); @@ -43,7 +44,7 @@ function it_does_not_mask_exception_from_client_during_read($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_does_not_read_file($dropbox) + function it_does_not_read_file(Dropbox_API $dropbox) { $dropbox ->getFile('filename') @@ -55,7 +56,7 @@ function it_does_not_read_file($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_checks_if_file_exists($dropbox) + function it_checks_if_file_exists(Dropbox_API $dropbox) { $dropbox ->getMetaData('filename', false) @@ -92,7 +93,7 @@ function it_checks_if_file_exists($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_does_not_mask_exception_from_client_during_check_if_exists($dropbox) + function it_does_not_mask_exception_from_client_during_check_if_exists(Dropbox_API $dropbox) { $dropbox ->getMetaData('filename', false) @@ -104,7 +105,7 @@ function it_does_not_mask_exception_from_client_during_check_if_exists($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_gets_keys($dropbox) + function it_gets_keys(Dropbox_API $dropbox) { $dropbox ->getMetaData('/', true) @@ -121,7 +122,7 @@ function it_gets_keys($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_does_not_mask_exception_from_client_during_getting_keys($dropbox) + function it_does_not_mask_exception_from_client_during_getting_keys(Dropbox_API $dropbox) { $dropbox ->getMetaData('/', true) @@ -134,7 +135,7 @@ function it_does_not_mask_exception_from_client_during_getting_keys($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_checks_if_given_key_is_directory($dropbox) + function it_checks_if_given_key_is_directory(Dropbox_API $dropbox) { $dropbox ->getMetaData('filename', false) @@ -157,7 +158,7 @@ function it_checks_if_given_key_is_directory($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_writes_file($dropbox) + function it_writes_file(Dropbox_API $dropbox) { $dropbox ->putFile('filename', Argument::any()) @@ -170,7 +171,7 @@ function it_writes_file($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_does_not_mask_exception_from_client_during_write($dropbox) + function it_does_not_mask_exception_from_client_during_write(Dropbox_API $dropbox) { $dropbox ->putFile('filename', Argument::any()) @@ -183,7 +184,7 @@ function it_does_not_mask_exception_from_client_during_write($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_deletes_file($dropbox) + function it_deletes_file(Dropbox_API $dropbox) { $dropbox ->delete('filename') @@ -196,7 +197,7 @@ function it_deletes_file($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_does_not_delete_file($dropbox) + function it_does_not_delete_file(Dropbox_API $dropbox) { $dropbox ->delete('filename') @@ -209,7 +210,7 @@ function it_does_not_delete_file($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_renames_file($dropbox) + function it_renames_file(Dropbox_API $dropbox) { $dropbox ->move('filename', 'filename2') @@ -222,7 +223,7 @@ function it_renames_file($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_does_not_rename_file($dropbox) + function it_does_not_rename_file(Dropbox_API $dropbox) { $dropbox ->move('filename', 'filename2') @@ -235,7 +236,7 @@ function it_does_not_rename_file($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_fetches_mtime($dropbox) + function it_fetches_mtime(Dropbox_API $dropbox) { $dropbox ->getMetaData('filename', false) @@ -250,7 +251,7 @@ function it_fetches_mtime($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_does_not_fetch_mtime_when_file_not_found($dropbox) + function it_does_not_fetch_mtime_when_file_not_found(Dropbox_API $dropbox) { $dropbox ->getMetaData('filename', false) @@ -263,7 +264,7 @@ function it_does_not_fetch_mtime_when_file_not_found($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_does_not_check_if_key_is_dir_when_file_not_found($dropbox) + function it_does_not_check_if_key_is_dir_when_file_not_found(Dropbox_API $dropbox) { $dropbox ->getMetaData('filename', false) @@ -276,7 +277,7 @@ function it_does_not_check_if_key_is_dir_when_file_not_found($dropbox) /** * @param \Dropbox_API $dropbox */ - function it_fails_checking_if_key_is_dir_when_dropbox_throws_exception($dropbox) + function it_fails_checking_if_key_is_dir_when_dropbox_throws_exception(Dropbox_API $dropbox) { $dropbox ->getMetaData('filename', false) diff --git a/spec/Gaufrette/Adapter/FtpSpec.php b/spec/Gaufrette/Adapter/FtpSpec.php index 4148cf0ef..d07f37bd3 100644 --- a/spec/Gaufrette/Adapter/FtpSpec.php +++ b/spec/Gaufrette/Adapter/FtpSpec.php @@ -5,6 +5,7 @@ //hack - mock php built-in functions require_once 'functions.php'; +use Gaufrette\Filesystem; use PhpSpec\ObjectBehavior; class FtpSpec extends ObjectBehavior @@ -154,7 +155,7 @@ function it_does_not_delete_file() /** * @param \Gaufrette\Filesystem $filesystem */ - function it_creates_file($filesystem) + function it_creates_file(Filesystem $filesystem) { $this->createFile('filename', $filesystem)->shouldReturnAnInstanceOf('\Gaufrette\File'); } @@ -162,7 +163,7 @@ function it_creates_file($filesystem) /** * @param \Gaufrette\Filesystem $filesystem */ - function it_creates_file_in_not_existing_directory($filesystem) + function it_creates_file_in_not_existing_directory(Filesystem $filesystem) { $this->createFile('bb/cc/dd/filename', $filesystem)->shouldReturnAnInstanceOf('\Gaufrette\File'); } diff --git a/spec/Gaufrette/Adapter/LazyOpenCloudSpec.php b/spec/Gaufrette/Adapter/LazyOpenCloudSpec.php index 6b0aeb1b4..31adde3c8 100644 --- a/spec/Gaufrette/Adapter/LazyOpenCloudSpec.php +++ b/spec/Gaufrette/Adapter/LazyOpenCloudSpec.php @@ -2,7 +2,10 @@ namespace spec\Gaufrette\Adapter; +use Gaufrette\Adapter\OpenStackCloudFiles\ObjectStoreFactoryInterface; use OpenCloud\ObjectStore\Exception\ObjectNotFoundException; +use OpenCloud\ObjectStore\Resource\Container; +use OpenCloud\ObjectStore\Service; use PhpSpec\ObjectBehavior; use Prophecy\Argument; @@ -16,7 +19,7 @@ class LazyOpenCloudSpec extends ObjectBehavior /** * @param \Gaufrette\Adapter\OpenStackCloudFiles\ObjectStoreFactoryInterface $objectStoreFactory */ - function let($objectStoreFactory) + function let(ObjectStoreFactoryInterface $objectStoreFactory) { $this->beConstructedWith($objectStoreFactory, 'test-container-name'); } @@ -31,7 +34,11 @@ function it_is_adapter() * @param \OpenCloud\ObjectStore\Service $objectStore * @param \OpenCloud\ObjectStore\Resource\Container $container */ - function it_initializes_object_store($objectStoreFactory, $objectStore, $container) + function it_initializes_object_store( + ObjectStoreFactoryInterface $objectStoreFactory, + Service $objectStore, + Container $container + ) { $objectStoreFactory->getObjectStore()->shouldBeCalled()->willReturn($objectStore); $objectStore->getContainer("test-container-name")->shouldBeCalled()->willReturn($container); diff --git a/spec/Gaufrette/Adapter/OpenCloudSpec.php b/spec/Gaufrette/Adapter/OpenCloudSpec.php index be8a7ad87..674501037 100644 --- a/spec/Gaufrette/Adapter/OpenCloudSpec.php +++ b/spec/Gaufrette/Adapter/OpenCloudSpec.php @@ -3,12 +3,14 @@ namespace spec\Gaufrette\Adapter; use Guzzle\Http\Exception\BadResponseException; +use OpenCloud\Common\Collection; use OpenCloud\Common\Exceptions\CreateUpdateError; use OpenCloud\Common\Exceptions\DeleteError; use OpenCloud\ObjectStore\Exception\ObjectNotFoundException; +use OpenCloud\ObjectStore\Resource\Container; use OpenCloud\ObjectStore\Resource\DataObject; +use OpenCloud\ObjectStore\Service; use PhpSpec\ObjectBehavior; -use Prophecy\Argument; /** * OpenCloudSpec @@ -22,7 +24,7 @@ class OpenCloudSpec extends ObjectBehavior * @param OpenCloud\ObjectStore\Service $objectStore * @param OpenCloud\ObjectStore\Resource\Container $container */ - function let($objectStore, $container) + function let(Service $objectStore, Container $container) { $objectStore->getContainer("test")->willReturn($container); $this->beConstructedWith($objectStore, 'test', false); @@ -37,7 +39,7 @@ function it_is_adapter() * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - function it_reads_file($container, $object) + function it_reads_file(Container $container, DataObject $object) { $object->getContent()->willReturn("Hello World"); $container->getObject("test")->willReturn($object); @@ -48,7 +50,7 @@ function it_reads_file($container, $object) /** * @param OpenCloud\ObjectStore\Resource\Container $container */ - function it_reads_file_on_error_returns_false($container) + function it_reads_file_on_error_returns_false(Container $container) { $container->getObject("test")->willThrow(new ObjectNotFoundException()); @@ -59,7 +61,7 @@ function it_reads_file_on_error_returns_false($container) * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - function it_writes_file_returns_size($container, $object) + function it_writes_file_returns_size(Container $container, DataObject $object) { $testData = "Hello World!"; $testDataSize = strlen($testData); @@ -73,7 +75,7 @@ function it_writes_file_returns_size($container, $object) /** * @param OpenCloud\ObjectStore\Resource\Container $container */ - function it_writes_file_and_write_fails_returns_false($container) + function it_writes_file_and_write_fails_returns_false(Container $container) { $testData = "Hello World!"; @@ -86,7 +88,7 @@ function it_writes_file_and_write_fails_returns_false($container) * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - function it_returns_true_if_key_exists($container, $object) + function it_returns_true_if_key_exists(Container $container, DataObject $object) { $container->getPartialObject('test')->willReturn($object); @@ -96,7 +98,7 @@ function it_returns_true_if_key_exists($container, $object) /** * @param OpenCloud\ObjectStore\Resource\Container $container */ - function it_returns_false_if_key_does_not_exist($container) + function it_returns_false_if_key_does_not_exist(Container $container) { $container->getPartialObject('test')->willThrow(new BadResponseException()); @@ -107,7 +109,7 @@ function it_returns_false_if_key_does_not_exist($container) * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - function it_deletes_file_on_success_returns_true($container, $object) + function it_deletes_file_on_success_returns_true(Container $container, DataObject $object) { $object->delete()->willReturn(null); $container->getObject("test")->willReturn($object); @@ -119,7 +121,7 @@ function it_deletes_file_on_success_returns_true($container, $object) * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - function it_deletes_file_returns_false_on_failure($container, $object) + function it_deletes_file_returns_false_on_failure(Container $container, DataObject $object) { $object->delete()->willThrow(new DeleteError()); $container->getObject("test")->willReturn($object); @@ -130,7 +132,7 @@ function it_deletes_file_returns_false_on_failure($container, $object) /** * @param OpenCloud\ObjectStore\Resource\Container $container */ - function it_deletes_file_if_file_does_not_exist_returns_false($container) + function it_deletes_file_if_file_does_not_exist_returns_false(Container $container) { $container->getObject("test")->willThrow(new ObjectNotFoundException()); @@ -141,7 +143,7 @@ function it_deletes_file_if_file_does_not_exist_returns_false($container) * @param OpenCloud\ObjectStore\Resource\Container $container * @param OpenCloud\ObjectStore\Resource\DataObject $object */ - function it_returns_checksum_if_file_exists($container, $object) + function it_returns_checksum_if_file_exists(Container $container, DataObject $object) { $object->getEtag()->willReturn("test String"); $container->getObject("test")->willReturn($object); @@ -152,7 +154,7 @@ function it_returns_checksum_if_file_exists($container, $object) /** * @param OpenCloud\ObjectStore\Resource\Container $container */ - function it_returns_false_when_file_does_not_exist($container) + function it_returns_false_when_file_does_not_exist(Container $container) { $container->getObject("test")->willThrow(new ObjectNotFoundException()); @@ -166,7 +168,7 @@ function it_returns_false_when_file_does_not_exist($container) * @param OpenCloud\ObjectStore\Resource\DataObject $object2 * @param OpenCloud\ObjectStore\Resource\DataObject $object3 */ - function it_returns_files_as_sorted_array($container, $objectList, $object1, $object2, $object3) + function it_returns_files_as_sorted_array(Container $container, Collection $objectList, DataObject $object1, DataObject $object2, DataObject $object3) { $outputArray = array('key1', 'key2', 'key5'); $index = 0; @@ -195,7 +197,7 @@ function () use ($objects, &$index) { /** * @param OpenCloud\ObjectStore\Service $objectStore */ - function it_throws_exception_if_container_does_not_exist($objectStore) + function it_throws_exception_if_container_does_not_exist(Service $objectStore) { $containerName = 'container-does-not-exist'; @@ -209,7 +211,7 @@ function it_throws_exception_if_container_does_not_exist($objectStore) * @param OpenCloud\ObjectStore\Service $objectStore * @param OpenCloud\ObjectStore\Resource\Container $container */ - function it_creates_container($objectStore, $container) + function it_creates_container(Service $objectStore, Container $container) { $containerName = 'container-does-not-yet-exist'; $filename = 'test'; @@ -226,7 +228,7 @@ function it_creates_container($objectStore, $container) /** * @param OpenCloud\ObjectStore\Service $objectStore */ - function it_throws_exeption_if_container_creation_fails($objectStore) + function it_throws_exeption_if_container_creation_fails(Service $objectStore) { $containerName = 'container-does-not-yet-exist'; @@ -238,14 +240,21 @@ function it_throws_exeption_if_container_creation_fails($objectStore) $this->shouldThrow('\RuntimeException')->duringExists('test'); } - function it_returns_false_if_the_object_does_not_exists_when_fetching_mtime($container) + /** + * @param OpenCloud\ObjectStore\Resource\Container $container + */ + function it_returns_false_if_the_object_does_not_exists_when_fetching_mtime(Container $container) { $container->getObject('foo')->willThrow(ObjectNotFoundException::class); $this->mtime('foo')->shouldReturn(false); } - function it_fetches_file_mtime(DataObject $object, $container) + /** + * @param OpenCloud\ObjectStore\Resource\DataObject $object + * @param OpenCloud\ObjectStore\Resource\Container $container + */ + function it_fetches_file_mtime(DataObject $object, Container $container) { $container->getObject('foo')->willReturn($object); $object->getLastModified()->willReturn('Tue, 13 Jun 2017 22:02:34 GMT'); diff --git a/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php b/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php index d4a624c24..3ee392120 100644 --- a/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php +++ b/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php @@ -10,15 +10,23 @@ define('NET_SFTP_TYPE_DIRECTORY', 2); } +use Gaufrette\Filesystem; use phpseclib\Net\SFTP as Base; use PhpSpec\ObjectBehavior; +class SFTP extends Base +{ + public function __construct() + { + } +} + class PhpseclibSftpSpec extends ObjectBehavior { /** * @param \spec\Gaufrette\Adapter\SFTP $sftp */ - function let($sftp) + function let(SFTP $sftp) { $this->beConstructedWith($sftp, '/home/l3l0', false, 'l3lo', 'password'); } @@ -41,7 +49,7 @@ function it_supports_native_list_keys() /** * @param \spec\Gaufrette\Adapter\SFTP $sftp */ - function it_fetches_keys($sftp) + function it_fetches_keys(SFTP $sftp) { $sftp ->file_exists('/home/l3l0/') @@ -68,7 +76,7 @@ function it_fetches_keys($sftp) /** * @param \spec\Gaufrette\Adapter\SFTP $sftp */ - function it_reads_file($sftp) + function it_reads_file(SFTP $sftp) { $sftp->get('/home/l3l0/filename')->willReturn('some content'); @@ -78,7 +86,7 @@ function it_reads_file($sftp) /** * @param \spec\Gaufrette\Adapter\SFTP $sftp */ - function it_creates_and_writes_file($sftp) + function it_creates_and_writes_file(SFTP $sftp) { $sftp->pwd()->willReturn('/home/l3l0'); $sftp->chdir('/home/l3l0')->willReturn(true); @@ -91,7 +99,7 @@ function it_creates_and_writes_file($sftp) /** * @param \spec\Gaufrette\Adapter\SFTP $sftp */ - function it_renames_file($sftp) + function it_renames_file(SFTP $sftp) { $sftp->pwd()->willReturn('/home/l3l0'); $sftp->chdir('/home/l3l0')->willReturn(true); @@ -106,7 +114,7 @@ function it_renames_file($sftp) /** * @param \spec\Gaufrette\Adapter\SFTP $sftp */ - function it_should_check_if_file_exists($sftp) + function it_should_check_if_file_exists(SFTP $sftp) { $sftp->pwd()->willReturn('/home/l3l0'); $sftp->chdir('/home/l3l0')->willReturn(true); @@ -122,7 +130,7 @@ function it_should_check_if_file_exists($sftp) /** * @param \spec\Gaufrette\Adapter\SFTP $sftp */ - function it_should_check_is_directory($sftp) + function it_should_check_is_directory(SFTP $sftp) { $sftp->pwd()->willReturn('/home/l3l0'); $sftp->chdir('/home/l3l0')->willReturn(true); @@ -137,7 +145,7 @@ function it_should_check_is_directory($sftp) * @param \spec\Gaufrette\Adapter\SFTP $sftp * @param \Gaufrette\Filesystem $filesystem */ - function it_should_create_file($sftp, $filesystem) + function it_should_create_file(SFTP $sftp, Filesystem $filesystem) { $sftp->stat('/home/l3l0/filename')->willReturn(array( 'name' => '/home/l3l0/filename', @@ -147,10 +155,3 @@ function it_should_create_file($sftp, $filesystem) $this->createFile('filename', $filesystem)->beAnInstanceOf('Gaufrette\File'); } } - -class SFTP extends Base -{ - public function __construct() - { - } -} diff --git a/spec/Gaufrette/FileSpec.php b/spec/Gaufrette/FileSpec.php index 422de463b..9b3fc6bf3 100644 --- a/spec/Gaufrette/FileSpec.php +++ b/spec/Gaufrette/FileSpec.php @@ -2,14 +2,19 @@ namespace spec\Gaufrette; +use Gaufrette\Filesystem; use PhpSpec\ObjectBehavior; +interface MetadataAdapter extends \Gaufrette\Adapter, + \Gaufrette\Adapter\MetadataSupporter +{} + class FileSpec extends ObjectBehavior { /** * @param \Gaufrette\Filesystem $filesystem */ - function let($filesystem) + function let(Filesystem $filesystem) { $this->beConstructedWith('filename', $filesystem); } @@ -27,7 +32,7 @@ function it_gives_access_to_key() /** * @param \Gaufrette\Filesystem $filesystem */ - function it_gets_content($filesystem) + function it_gets_content(Filesystem $filesystem) { $filesystem->read('filename')->shouldBeCalled()->willReturn('Some content'); @@ -37,7 +42,7 @@ function it_gets_content($filesystem) /** * @param \Gaufrette\Filesystem $filesystem */ - function it_gets_mtime($filesystem) + function it_gets_mtime(Filesystem $filesystem) { $filesystem->mtime('filename')->shouldBeCalled()->willReturn(1358797854); @@ -48,7 +53,7 @@ function it_gets_mtime($filesystem) * @param \Gaufrette\Filesystem $filesystem * @param \spec\Gaufrette\MetadataAdapter $adapter */ - function it_pass_metadata_when_write_content($filesystem, $adapter) + function it_pass_metadata_when_write_content(Filesystem $filesystem, MetadataAdapter $adapter) { $metadata = array('id' => '123'); $adapter->setMetadata('filename', $metadata)->shouldBeCalled(); @@ -62,7 +67,7 @@ function it_pass_metadata_when_write_content($filesystem, $adapter) * @param \Gaufrette\Filesystem $filesystem * @param \spec\Gaufrette\MetadataAdapter $adapter */ - function it_pass_metadata_when_read_content($filesystem, $adapter) + function it_pass_metadata_when_read_content(Filesystem $filesystem, MetadataAdapter $adapter) { $metadata = array('id' => '123'); $adapter->setMetadata('filename', $metadata)->shouldBeCalled(); @@ -76,7 +81,7 @@ function it_pass_metadata_when_read_content($filesystem, $adapter) * @param \Gaufrette\Filesystem $filesystem * @param \spec\Gaufrette\MetadataAdapter $adapter */ - function it_pass_metadata_when_delete_content($filesystem, $adapter) + function it_pass_metadata_when_delete_content(Filesystem $filesystem, MetadataAdapter $adapter) { $metadata = array('id' => '123'); $adapter->setMetadata('filename', $metadata)->shouldBeCalled(); @@ -90,7 +95,7 @@ function it_pass_metadata_when_delete_content($filesystem, $adapter) * @param \Gaufrette\Filesystem $filesystem * @param \spec\Gaufrette\MetadataAdapter $adapter */ - function it_sets_content_of_file($filesystem, $adapter) + function it_sets_content_of_file(Filesystem $filesystem, MetadataAdapter $adapter) { $adapter->setMetadata('filename', array())->shouldNotBeCalled(); $filesystem->getAdapter()->willReturn($adapter); @@ -114,7 +119,7 @@ function it_sets_name() /** * @param \Gaufrette\Filesystem $filesystem */ - function it_sets_size_for_new_file($filesystem) + function it_sets_size_for_new_file(Filesystem $filesystem) { $filesystem->write('filename', 'some content', true)->shouldBeCalled()->willReturn(21); @@ -125,7 +130,7 @@ function it_sets_size_for_new_file($filesystem) /** * @param \Gaufrette\Filesystem $filesystem */ - function it_calculates_size_from_filesystem($filesystem) + function it_calculates_size_from_filesystem(Filesystem $filesystem) { $filesystem->size('filename')->shouldBeCalled()->willReturn(12); @@ -135,7 +140,7 @@ function it_calculates_size_from_filesystem($filesystem) /** * @param \Gaufrette\Filesystem $filesystem */ - function it_allows_to_set_size($filesystem) + function it_allows_to_set_size(Filesystem $filesystem) { $filesystem->read('filename')->shouldNotBeCalled(); @@ -146,7 +151,7 @@ function it_allows_to_set_size($filesystem) /** * @param \Gaufrette\Filesystem $filesystem */ - function it_gets_zero_size_when_file_not_found($filesystem) + function it_gets_zero_size_when_file_not_found(Filesystem $filesystem) { $filesystem->size('filename')->willThrow(new \Gaufrette\Exception\FileNotFound('filename')); @@ -156,7 +161,7 @@ function it_gets_zero_size_when_file_not_found($filesystem) /** * @param \Gaufrette\Filesystem $filesystem */ - function it_check_if_file_with_key_exists_in_filesystem($filesystem) + function it_check_if_file_with_key_exists_in_filesystem(Filesystem $filesystem) { $filesystem->has('filename')->willReturn(true); $this->exists()->shouldReturn(true); @@ -168,19 +173,18 @@ function it_check_if_file_with_key_exists_in_filesystem($filesystem) /** * @param \Gaufrette\Filesystem $filesystem */ - function it_deletes_file_from_filesystem($filesystem) + function it_deletes_file_from_filesystem(Filesystem $filesystem) { $filesystem->delete('filename')->shouldBeCalled()->willReturn(true); $this->delete()->shouldReturn(true); } - function it_renames_file_from_filesystem($filesystem) + /** + * @param \Gaufrette\Filesystem $filesystem + */ + function it_renames_file_from_filesystem(Filesystem $filesystem) { $filesystem->rename('filename', 'newname')->shouldBeCalled(); $this->rename('newname'); } } - -interface MetadataAdapter extends \Gaufrette\Adapter, - \Gaufrette\Adapter\MetadataSupporter -{} diff --git a/spec/Gaufrette/FilesystemMapSpec.php b/spec/Gaufrette/FilesystemMapSpec.php index 3d543f36c..28b05b297 100644 --- a/spec/Gaufrette/FilesystemMapSpec.php +++ b/spec/Gaufrette/FilesystemMapSpec.php @@ -2,6 +2,7 @@ namespace spec\Gaufrette; +use Gaufrette\Filesystem; use PhpSpec\ObjectBehavior; class FilesystemMapSpec extends ObjectBehavior @@ -14,7 +15,7 @@ function it_is_initializable() /** * @param Gaufrette\Filesystem $filesystem */ - function it_checks_if_has_mapped_filesystem($filesystem) + function it_checks_if_has_mapped_filesystem(Filesystem $filesystem) { $this->set('some', $filesystem); $this->has('some')->shouldReturn(true); @@ -24,7 +25,7 @@ function it_checks_if_has_mapped_filesystem($filesystem) /** * @param Gaufrette\Filesystem $filesystem */ - function it_sets_mapped_filesystem($filesystem) + function it_sets_mapped_filesystem(Filesystem $filesystem) { $this->set('some', $filesystem); $this->get('some')->shouldReturn($filesystem); @@ -41,7 +42,7 @@ function it_fails_when_get_filesystem_which_was_not_mapped() /** * @param Gaufrette\Filesystem $filesystem */ - function it_removes_mapped_filesystem($filesystem) + function it_removes_mapped_filesystem(Filesystem $filesystem) { $this->set('some', $filesystem); $this->remove('some'); @@ -60,7 +61,7 @@ function it_fails_when_try_to_remove_filesystem_which_was_not_mapped() /** * @param Gaufrette\Filesystem $filesystem */ - function it_removes_all_filesystems($filesystem) + function it_removes_all_filesystems(Filesystem $filesystem) { $this->set('some', $filesystem); $this->set('other', $filesystem); diff --git a/spec/Gaufrette/FilesystemSpec.php b/spec/Gaufrette/FilesystemSpec.php index d76663e5e..6b32fddf7 100644 --- a/spec/Gaufrette/FilesystemSpec.php +++ b/spec/Gaufrette/FilesystemSpec.php @@ -2,15 +2,25 @@ namespace spec\Gaufrette; +use Gaufrette\Adapter; +use Gaufrette\File; use PhpSpec\ObjectBehavior; use Prophecy\Argument; +interface ExtendedAdapter extends \Gaufrette\Adapter, + \Gaufrette\Adapter\FileFactory, + \Gaufrette\Adapter\StreamFactory, + \Gaufrette\Adapter\ChecksumCalculator, + \Gaufrette\Adapter\MetadataSupporter, + \Gaufrette\Adapter\MimeTypeProvider +{} + class FilesystemSpec extends ObjectBehavior { /** * @param \Gaufrette\Adapter $adapter */ - function let($adapter) + function let(Adapter $adapter) { $this->beConstructedWith($adapter); } @@ -24,7 +34,7 @@ function it_is_initializable() /** * @param \Gaufrette\Adapter $adapter */ - function it_gives_access_to_adapter($adapter) + function it_gives_access_to_adapter(Adapter $adapter) { $this->getAdapter()->shouldBe($adapter); } @@ -32,7 +42,7 @@ function it_gives_access_to_adapter($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_check_if_file_exists_using_adapter($adapter) + function it_check_if_file_exists_using_adapter(Adapter $adapter) { $adapter->exists('filename')->willReturn(true); $adapter->exists('otherFilename')->willReturn(false); @@ -44,7 +54,7 @@ function it_check_if_file_exists_using_adapter($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_renames_file($adapter) + function it_renames_file(Adapter $adapter) { $adapter->exists('filename')->shouldBeCalled()->willReturn(true); $adapter->exists('otherFilename')->shouldBeCalled()->willReturn(false); @@ -56,7 +66,7 @@ function it_renames_file($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_fails_when_renamed_source_file_does_not_exist($adapter) + function it_fails_when_renamed_source_file_does_not_exist(Adapter $adapter) { $adapter->exists('filename')->willReturn(false); @@ -69,7 +79,7 @@ function it_fails_when_renamed_source_file_does_not_exist($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_fails_when_renamed_target_file_exists($adapter) + function it_fails_when_renamed_target_file_exists(Adapter $adapter) { $adapter->exists('filename')->willReturn(true); $adapter->exists('otherFilename')->willReturn(true); @@ -83,7 +93,7 @@ function it_fails_when_renamed_target_file_exists($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_fails_when_rename_is_not_successful($adapter) + function it_fails_when_rename_is_not_successful(Adapter $adapter) { $adapter->exists('filename')->willReturn(true); $adapter->exists('otherFilename')->willReturn(false); @@ -98,7 +108,7 @@ function it_fails_when_rename_is_not_successful($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_creates_file_object_for_key($adapter) + function it_creates_file_object_for_key(Adapter $adapter) { $adapter->exists('filename')->willReturn(true); @@ -108,7 +118,7 @@ function it_creates_file_object_for_key($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_does_not_get_file_object_when_file_with_key_does_not_exist($adapter) + function it_does_not_get_file_object_when_file_with_key_does_not_exist(Adapter $adapter) { $adapter->exists('filename')->willReturn(false); @@ -121,7 +131,7 @@ function it_does_not_get_file_object_when_file_with_key_does_not_exist($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_gets_file_object_when_file_does_not_exist_but_can_be_created($adapter) + function it_gets_file_object_when_file_does_not_exist_but_can_be_created(Adapter $adapter) { $adapter->exists('filename')->willReturn(false); @@ -129,10 +139,10 @@ function it_gets_file_object_when_file_does_not_exist_but_can_be_created($adapte } /** - * @param \spec\Gaufrette\Adapter $extendedAdapter + * @param \spec\Gaufrette\ExtendedAdapter $extendedAdapter * @param \Gaufrette\File $file */ - function it_delegates_file_creation_to_adapter_when_adapter_is_file_factory($extendedAdapter, $file) + function it_delegates_file_creation_to_adapter_when_adapter_is_file_factory(ExtendedAdapter $extendedAdapter, File $file) { $this->beConstructedWith($extendedAdapter); $extendedAdapter->exists('filename')->willReturn(true); @@ -144,7 +154,7 @@ function it_delegates_file_creation_to_adapter_when_adapter_is_file_factory($ext /** * @param \Gaufrette\Adapter $adapter */ - function it_writes_content_to_new_file($adapter) + function it_writes_content_to_new_file(Adapter $adapter) { $adapter->exists('filename')->shouldBeCalled()->willReturn(false); $adapter->write('filename', 'some content to write')->shouldBeCalled()->willReturn(21); @@ -155,7 +165,7 @@ function it_writes_content_to_new_file($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_updates_content_of_file($adapter) + function it_updates_content_of_file(Adapter $adapter) { $adapter->write('filename', 'some content to write')->shouldBeCalled()->willReturn(21); @@ -165,7 +175,7 @@ function it_updates_content_of_file($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_does_not_update_content_of_file_when_file_cannot_be_overwriten($adapter) + function it_does_not_update_content_of_file_when_file_cannot_be_overwriten(Adapter $adapter) { $adapter->exists('filename')->willReturn(true); $adapter->write('filename', 'some content to write')->shouldNotBeCalled(); @@ -179,7 +189,7 @@ function it_does_not_update_content_of_file_when_file_cannot_be_overwriten($adap /** * @param \Gaufrette\Adapter $adapter */ - function it_fails_when_write_is_not_successful($adapter) + function it_fails_when_write_is_not_successful(Adapter $adapter) { $adapter->exists('filename')->willReturn(false); $adapter->write('filename', 'some content to write')->shouldBeCalled()->willReturn(false); @@ -193,7 +203,7 @@ function it_fails_when_write_is_not_successful($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_read_file($adapter) + function it_read_file(Adapter $adapter) { $adapter->exists('filename')->shouldBeCalled()->willReturn(true); $adapter->read('filename')->shouldBeCalled()->willReturn('Some content'); @@ -204,7 +214,7 @@ function it_read_file($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_does_not_read_file_which_does_not_exist($adapter) + function it_does_not_read_file_which_does_not_exist(Adapter $adapter) { $adapter->exists('filename')->willReturn(false); @@ -216,7 +226,7 @@ function it_does_not_read_file_which_does_not_exist($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_fails_when_read_is_not_successful($adapter) + function it_fails_when_read_is_not_successful(Adapter $adapter) { $adapter->exists('filename')->willReturn(true); $adapter->read('filename')->willReturn(false); @@ -230,7 +240,7 @@ function it_fails_when_read_is_not_successful($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_deletes_file($adapter) + function it_deletes_file(Adapter $adapter) { $adapter->exists('filename')->shouldBeCalled()->willReturn(true); $adapter->delete('filename')->shouldBeCalled()->willReturn(true); @@ -241,7 +251,7 @@ function it_deletes_file($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_does_not_delete_file_which_does_not_exist($adapter) + function it_does_not_delete_file_which_does_not_exist(Adapter $adapter) { $adapter->exists('filename')->willReturn(false); @@ -254,7 +264,7 @@ function it_does_not_delete_file_which_does_not_exist($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_fails_when_delete_is_not_successful($adapter) + function it_fails_when_delete_is_not_successful(Adapter $adapter) { $adapter->exists('filename')->willReturn(true); $adapter->delete('filename')->willReturn(false); @@ -268,7 +278,7 @@ function it_fails_when_delete_is_not_successful($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_should_get_all_keys($adapter) + function it_should_get_all_keys(Adapter $adapter) { $keys = array('filename', 'filename1', 'filename2'); $adapter->keys()->willReturn($keys); @@ -279,7 +289,7 @@ function it_should_get_all_keys($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_match_listed_keys_using_specified_pattern($adapter) + function it_match_listed_keys_using_specified_pattern(Adapter $adapter) { $keys = array('filename', 'filename1', 'filename2', 'testKey', 'KeyTest', 'testkey'); @@ -309,7 +319,7 @@ function it_match_listed_keys_using_specified_pattern($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_listing_directories_using_adapter_is_directory_method($adapter) + function it_listing_directories_using_adapter_is_directory_method(Adapter $adapter) { $keys = array('filename', 'filename1', 'filename2', 'testKey', 'KeyTest', 'testkey'); @@ -345,7 +355,7 @@ function it_listing_directories_using_adapter_is_directory_method($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_gets_mtime_of_file_using_adapter($adapter) + function it_gets_mtime_of_file_using_adapter(Adapter $adapter) { $adapter->exists('filename')->willReturn(true); $adapter->mtime('filename')->willReturn(1234567); @@ -356,7 +366,7 @@ function it_gets_mtime_of_file_using_adapter($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_does_not_get_mtime_of_file_which_does_not_exist($adapter) + function it_does_not_get_mtime_of_file_which_does_not_exist(Adapter $adapter) { $adapter->exists('filename')->willReturn(false); @@ -369,7 +379,7 @@ function it_does_not_get_mtime_of_file_which_does_not_exist($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_calculates_file_checksum($adapter) + function it_calculates_file_checksum(Adapter $adapter) { $adapter->exists('filename')->shouldBeCalled()->willReturn(true); $adapter->read('filename')->willReturn('some content'); @@ -380,7 +390,7 @@ function it_calculates_file_checksum($adapter) /** * @param \Gaufrette\Adapter $adapter */ - function it_does_not_calculate_checksum_of_file_which_does_not_exist($adapter) + function it_does_not_calculate_checksum_of_file_which_does_not_exist(Adapter $adapter) { $adapter->exists('filename')->shouldBeCalled()->willReturn(false); @@ -390,9 +400,9 @@ function it_does_not_calculate_checksum_of_file_which_does_not_exist($adapter) } /** - * @param \spec\Gaufrette\Adapter $extendedAdapter + * @param \spec\Gaufrette\ExtendedAdapter $extendedAdapter */ - function it_delegates_checksum_calculation_to_adapter_when_adapter_is_checksum_calculator($extendedAdapter) + function it_delegates_checksum_calculation_to_adapter_when_adapter_is_checksum_calculator(ExtendedAdapter $extendedAdapter) { $this->beConstructedWith($extendedAdapter); $extendedAdapter->exists('filename')->shouldBeCalled()->willReturn(true); @@ -403,9 +413,9 @@ function it_delegates_checksum_calculation_to_adapter_when_adapter_is_checksum_c } /** - * @param \spec\Gaufrette\Adapter $extendedAdapter + * @param \spec\Gaufrette\ExtendedAdapter $extendedAdapter */ - function it_delegates_mime_type_resolution_to_adapter_when_adapter_is_mime_type_provider($extendedAdapter) + function it_delegates_mime_type_resolution_to_adapter_when_adapter_is_mime_type_provider(ExtendedAdapter $extendedAdapter) { $this->beConstructedWith($extendedAdapter); $extendedAdapter->exists('filename')->willReturn(true); @@ -414,7 +424,7 @@ function it_delegates_mime_type_resolution_to_adapter_when_adapter_is_mime_type_ $this->mimeType('filename')->shouldReturn('text/plain'); } - function it_cannot_resolve_mime_type_if_the_adapter_cannot_provide_it($adapter) + function it_cannot_resolve_mime_type_if_the_adapter_cannot_provide_it(Adapter $adapter) { $adapter->exists('filename')->willReturn(true); $this @@ -422,11 +432,3 @@ function it_cannot_resolve_mime_type_if_the_adapter_cannot_provide_it($adapter) ->duringMimeType('filename'); } } - -interface Adapter extends \Gaufrette\Adapter, - \Gaufrette\Adapter\FileFactory, - \Gaufrette\Adapter\StreamFactory, - \Gaufrette\Adapter\ChecksumCalculator, - \Gaufrette\Adapter\MetadataSupporter, - \Gaufrette\Adapter\MimeTypeProvider -{} diff --git a/spec/Gaufrette/StreamWrapperSpec.php b/spec/Gaufrette/StreamWrapperSpec.php index 72dafabea..4661b9b97 100644 --- a/spec/Gaufrette/StreamWrapperSpec.php +++ b/spec/Gaufrette/StreamWrapperSpec.php @@ -2,6 +2,9 @@ namespace spec\Gaufrette; +use Gaufrette\FilesystemMap; +use Gaufrette\Filesystem; +use Gaufrette\Stream; use PhpSpec\ObjectBehavior; use Prophecy\Argument; @@ -12,7 +15,7 @@ class StreamWrapperSpec extends ObjectBehavior * @param \Gaufrette\Filesystem $filesystem * @param \Gaufrette\Stream $stream */ - function let($map, $filesystem, $stream) + function let(FilesystemMap $map, Filesystem $filesystem, Stream $stream) { $filesystem->createStream('filename')->willReturn($stream); $map->get('some')->willReturn($filesystem); @@ -27,7 +30,7 @@ function it_is_initializable() /** * @param \Gaufrette\Stream $stream */ - function it_opens_stream($stream) + function it_opens_stream(Stream $stream) { $stream->open(Argument::any())->willReturn(true); @@ -57,7 +60,7 @@ function it_does_not_read_from_stream_when_is_not_opened() /** * @param \Gaufrette\Stream $stream */ - function it_does_not_read_from_stream($stream) + function it_does_not_read_from_stream(Stream $stream) { $stream->open(Argument::any())->willReturn(true); $stream->read(4)->willReturn('some'); @@ -74,7 +77,7 @@ function it_does_not_write_to_stream_when_is_not_opened() /** * @param \Gaufrette\Stream $stream */ - function it_writes_to_stream($stream) + function it_writes_to_stream(Stream $stream) { $stream->open(Argument::any())->willReturn(true); $stream->write('some content')->shouldBeCalled()->willReturn(12); @@ -95,7 +98,7 @@ function it_does_not_close_stream_when_is_not_opened($stream) /** * @param \Gaufrette\Stream $stream */ - function it_closes_stream($stream) + function it_closes_stream(Stream $stream) { $stream->open(Argument::any())->willReturn(true); $stream->close()->shouldBeCalled(); @@ -106,7 +109,7 @@ function it_closes_stream($stream) /** * @param \Gaufrette\Stream $stream */ - function it_does_not_flush_stream_when_is_not_opened($stream) + function it_does_not_flush_stream_when_is_not_opened(Stream $stream) { $stream->flush()->shouldNotBeCalled(); $this->stream_flush(); @@ -115,7 +118,7 @@ function it_does_not_flush_stream_when_is_not_opened($stream) /** * @param \Gaufrette\Stream $stream */ - function it_flushes_stream($stream) + function it_flushes_stream(Stream $stream) { $stream->open(Argument::any())->willReturn(true); $stream->flush()->shouldBeCalled(); @@ -126,7 +129,7 @@ function it_flushes_stream($stream) /** * @param \Gaufrette\Stream $stream */ - function it_does_not_seek_in_stream_when_is_not_opened($stream) + function it_does_not_seek_in_stream_when_is_not_opened(Stream $stream) { $stream->seek(12, SEEK_SET)->shouldNotBeCalled(); $this->stream_seek(12, SEEK_SET); @@ -135,7 +138,7 @@ function it_does_not_seek_in_stream_when_is_not_opened($stream) /** * @param \Gaufrette\Stream $stream */ - function it_seeks_in_stream($stream) + function it_seeks_in_stream(Stream $stream) { $stream->open(Argument::any())->willReturn(true); $stream->seek(12, SEEK_SET)->shouldBeCalled()->willReturn(true); @@ -146,7 +149,7 @@ function it_seeks_in_stream($stream) /** * @param \Gaufrette\Stream $stream */ - function it_does_not_tell_about_position_in_stream_when_is_not_opened($stream) + function it_does_not_tell_about_position_in_stream_when_is_not_opened(Stream $stream) { $stream->tell()->shouldNotBeCalled(); $this->stream_tell(); @@ -155,7 +158,7 @@ function it_does_not_tell_about_position_in_stream_when_is_not_opened($stream) /** * @param \Gaufrette\Stream $stream */ - function it_does_tell_about_position_in_stream($stream) + function it_does_tell_about_position_in_stream(Stream $stream) { $stream->open(Argument::any())->willReturn(true); $stream->tell()->shouldBeCalled()->willReturn(12); @@ -166,7 +169,7 @@ function it_does_tell_about_position_in_stream($stream) /** * @param \Gaufrette\Stream $stream */ - function it_does_not_mark_as_eof_if_stream_is_not_opened($stream) + function it_does_not_mark_as_eof_if_stream_is_not_opened(Stream $stream) { $stream->eof()->shouldNotBeCalled(); $this->stream_eof(); @@ -175,7 +178,7 @@ function it_does_not_mark_as_eof_if_stream_is_not_opened($stream) /** * @param \Gaufrette\Stream $stream */ - function it_checks_if_eof($stream) + function it_checks_if_eof(Stream $stream) { $stream->open(Argument::any())->willReturn(true); $this->stream_open('gaufrette://some/filename', 'w+'); @@ -195,7 +198,7 @@ function it_does_not_get_stat_when_is_not_open() /** * @param \Gaufrette\Stream $stream */ - function it_stats_file($stream) + function it_stats_file(Stream $stream) { $stat = array( 'dev' => 1, @@ -222,7 +225,7 @@ function it_stats_file($stream) /** * @param \Gaufrette\Stream $stream */ - function it_should_stat_from_url($stream) + function it_should_stat_from_url(Stream $stream) { $stat = array( 'dev' => 1, @@ -246,9 +249,10 @@ function it_should_stat_from_url($stream) } /** + * @param \Gaufrette\Filesystem $stream * @param \Gaufrette\Stream $stream */ - function it_stats_even_if_it_cannot_be_open($filesystem, $stream) + function it_stats_even_if_it_cannot_be_open(Filesystem $filesystem, Stream $stream) { $filesystem->createStream('dir/')->willReturn($stream); $stream->open(Argument::any())->willThrow(new \RuntimeException); @@ -259,7 +263,7 @@ function it_stats_even_if_it_cannot_be_open($filesystem, $stream) /** * @param \Gaufrette\Stream $stream */ - function it_does_not_unlink_when_cannot_open($stream) + function it_does_not_unlink_when_cannot_open(Stream $stream) { $stream->open(Argument::any())->willThrow(new \RuntimeException); $this->unlink('gaufrette://some/filename')->shouldReturn(false); @@ -268,7 +272,7 @@ function it_does_not_unlink_when_cannot_open($stream) /** * @param \Gaufrette\Stream $stream */ - function it_unlinks_file($stream) + function it_unlinks_file(Stream $stream) { $stream->open(Argument::any())->willReturn(true); $stream->unlink()->willReturn(true); @@ -284,7 +288,7 @@ function it_does_not_cast_stream_if_is_not_opened() /** * @param \Gaufrette\Stream $stream */ - function it_casts_stream($stream) + function it_casts_stream(Stream $stream) { $stream->open(Argument::any())->willReturn(true); $stream->cast(STREAM_CAST_FOR_SELECT)->willReturn('resource'); diff --git a/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php b/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php index 78b329a10..d16fd042b 100644 --- a/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php +++ b/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php @@ -3,8 +3,9 @@ namespace Gaufrette\Functional\FileStream; use Gaufrette\StreamWrapper; +use PHPUnit\Framework\TestCase; -class FunctionalTestCase extends \PHPUnit_Framework_TestCase +class FunctionalTestCase extends TestCase { protected $filesystem; From a92f19e425f806584237af1c7cfb563dd6487d0c Mon Sep 17 00:00:00 2001 From: damijank Date: Wed, 12 Dec 2018 14:04:22 +0100 Subject: [PATCH 363/426] Implement ChecksumCalculator Interface --- src/Gaufrette/Adapter/AzureBlobStorage.php | 23 +++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/AzureBlobStorage.php b/src/Gaufrette/Adapter/AzureBlobStorage.php index e1ab1e57b..965c21d01 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage.php @@ -22,7 +22,8 @@ */ class AzureBlobStorage implements Adapter, MetadataSupporter, - SizeCalculator + SizeCalculator, + ChecksumCalculator { /** @@ -334,6 +335,26 @@ public function size($key) } + /** + * {@inheritdoc} + */ + public function checksum($key) + { + $this->init(); + list($containerName, $key) = $this->tokenizeKey($key); + + try { + $properties = $this->blobProxy->getBlobProperties($containerName, $key); + $checksumBase64 = $properties->getProperties()->getContentMD5(); + + return \bin2hex(\base64_decode($checksumBase64, true)); + } catch (ServiceException $e) { + $this->failIfContainerNotFound($e, sprintf('read content MD5 for key "%s"', $key), $containerName); + + return false; + } + } + /** * {@inheritdoc} * @throws \RuntimeException From b54a01eb9065c5171d83d3c35481321f520a6964 Mon Sep 17 00:00:00 2001 From: Nicolas MURE Date: Mon, 10 Dec 2018 10:34:15 +0100 Subject: [PATCH 364/426] remove old dockerfiles --- Dockerfile-php56 | 56 -------------------------------------- Dockerfile-php70 | 68 ---------------------------------------------- docker-compose.yml | 30 -------------------- 3 files changed, 154 deletions(-) delete mode 100644 Dockerfile-php56 delete mode 100644 Dockerfile-php70 diff --git a/Dockerfile-php56 b/Dockerfile-php56 deleted file mode 100644 index 97b543999..000000000 --- a/Dockerfile-php56 +++ /dev/null @@ -1,56 +0,0 @@ -FROM php:5.6-alpine - -ENV SRC_DIR /usr/src/gaufrette - -RUN apk add --no-cache --virtual .persistent-deps \ - git \ - zlib - -# PHP extensions -ENV MONGODB_VERSION="1.2.11" \ - SSH2_VERSION="0.13" - -RUN set -xe \ - && apk add --no-cache --virtual .build-deps \ - $PHPIZE_DEPS \ - libssh2-dev \ - zlib-dev \ - && docker-php-ext-install \ - zip \ - && pecl install \ - mongodb-${MONGODB_VERSION} \ - ssh2-${SSH2_VERSION} \ - && docker-php-ext-enable --ini-name 05-opcache.ini opcache \ - && docker-php-ext-enable \ - mongodb \ - ssh2 \ - && apk del .build-deps - -COPY docker/php.ini /usr/local/etc/php/php.ini - -COPY docker/install-composer.sh /usr/local/bin/install-composer -RUN chmod +x /usr/local/bin/install-composer - -RUN set -xe \ - && install-composer \ - && mv composer.phar /usr/local/bin/composer - -ENV COMPOSER_ALLOW_SUPERUSER 1 - -RUN composer global require "hirak/prestissimo" --prefer-dist --no-progress --no-suggest --optimize-autoloader --apcu-autoloader \ - && composer clear-cache - -WORKDIR ${SRC_DIR} - -COPY composer.json ./ -RUN composer update --prefer-dist --no-autoloader --no-scripts --no-progress --no-suggest \ - && composer clear-cache - -COPY spec spec/ -COPY src src/ -COPY tests tests/ -COPY bin/tests bin/tests - -RUN composer dump-autoload - -CMD ["bin/tests"] diff --git a/Dockerfile-php70 b/Dockerfile-php70 deleted file mode 100644 index 2fe95a537..000000000 --- a/Dockerfile-php70 +++ /dev/null @@ -1,68 +0,0 @@ -FROM php:7.0-alpine - -ENV SRC_DIR /usr/src/gaufrette - -RUN apk add --no-cache --virtual .persistent-deps \ - git \ - zlib - -# PHP extensions -ENV MONGODB_VERSION="1.2.11" \ - SSH2_VERSION="1.1.2" - -RUN set -xe \ - && apk add --no-cache --virtual .build-deps \ - $PHPIZE_DEPS \ - libssh2-dev \ - zlib-dev \ - && docker-php-ext-install \ - zip \ - && pecl install \ - mongodb-${MONGODB_VERSION} \ - ssh2-${SSH2_VERSION} \ - && docker-php-ext-enable --ini-name 05-opcache.ini opcache \ - && docker-php-ext-enable \ - mongodb \ - ssh2 \ - && apk del .build-deps - -COPY docker/php.ini /usr/local/etc/php/php.ini - -COPY docker/install-composer.sh /usr/local/bin/docker-gaufrette-install-composer -RUN chmod +x /usr/local/bin/docker-gaufrette-install-composer - -RUN set -xe \ - && apk add --no-cache --virtual .fetch-deps \ - openssl \ - && docker-gaufrette-install-composer \ - && mv composer.phar /usr/local/bin/composer \ - && apk del .fetch-deps - -COPY docker/php.ini /usr/local/etc/php/php.ini - -COPY docker/install-composer.sh /usr/local/bin/install-composer -RUN chmod +x /usr/local/bin/install-composer - -RUN set -xe \ - && install-composer \ - && mv composer.phar /usr/local/bin/composer - -ENV COMPOSER_ALLOW_SUPERUSER 1 - -RUN composer global require "hirak/prestissimo" --prefer-dist --no-progress --no-suggest --optimize-autoloader --apcu-autoloader \ - && composer clear-cache - -WORKDIR ${SRC_DIR} - -COPY composer.json ./ -RUN composer update --prefer-dist --no-autoloader --no-scripts --no-progress --no-suggest \ - && composer clear-cache - -COPY spec spec/ -COPY src src/ -COPY tests tests/ -COPY bin/tests bin/tests - -RUN composer dump-autoload - -CMD ["bin/tests"] diff --git a/docker-compose.yml b/docker-compose.yml index 8dbdc7040..7af5366b5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,36 +1,6 @@ version: '3' services: - php56: - build: - context: . - dockerfile: ./Dockerfile-php56 - volumes: - - './spec:/usr/src/gaufrette/spec:rw' - - './src:/usr/src/gaufrette/src:ro' - - './tests:/usr/src/gaufrette/tests:rw' - - './bin/tests:/usr/src/gaufrette/bin/tests:ro' - depends_on: - - mongodb - - sftp - - ftp - env_file: .env - - php70: - build: - context: . - dockerfile: ./Dockerfile-php70 - volumes: - - './spec:/usr/src/gaufrette/spec:rw' - - './src:/usr/src/gaufrette/src:ro' - - './tests:/usr/src/gaufrette/tests:rw' - - './bin/tests:/usr/src/gaufrette/bin/tests:ro' - depends_on: - - mongodb - - sftp - - ftp - env_file: .env - php71: build: context: . From cc83b7e1edb970b8ed31b3663ec95d4e962cc2d7 Mon Sep 17 00:00:00 2001 From: damijank Date: Thu, 13 Dec 2018 09:55:05 +0100 Subject: [PATCH 365/426] Add test for ChecksumCalculator in AzureBlobStorage adapter --- .../Adapter/AzureMultiContainerBlobStorageTest.php | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php index 00d16d80f..c73bfc910 100644 --- a/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php +++ b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php @@ -108,6 +108,20 @@ public function shouldGetSize() $this->assertEquals($contentSize, $this->filesystem->size($path)); } + /** + * @test + * @group functional + */ + public function shouldGetMd5Hash() + { + $path = $this->createUniqueContainerName('container') . '/foo'; + + $content = 'Some content'; + $this->filesystem->write($path, $content); + + $this->assertEquals(\md5($content), $this->filesystem->checksum($path)); + } + /** * @test * @group functional From 627a836f608b39c7bf12559625e27db76cd14f69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois-Xavier=20de=20Guillebon?= Date: Wed, 31 Oct 2018 10:13:07 +0100 Subject: [PATCH 366/426] Fix ini_get() for boolean values --- tests/Gaufrette/Functional/Adapter/ApcTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Gaufrette/Functional/Adapter/ApcTest.php b/tests/Gaufrette/Functional/Adapter/ApcTest.php index 8cc60e403..581b77001 100644 --- a/tests/Gaufrette/Functional/Adapter/ApcTest.php +++ b/tests/Gaufrette/Functional/Adapter/ApcTest.php @@ -11,7 +11,7 @@ public function setUp() { if (!extension_loaded('apc')) { return $this->markTestSkipped('The APC extension is not available.'); - } elseif (!ini_get('apc.enabled') || !ini_get('apc.enable_cli')) { + } elseif (!filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) || !filter_var(ini_get('apc.enable_cli'), FILTER_VALIDATE_BOOLEAN)) { return $this->markTestSkipped('The APC extension is available, but not enabled.'); } From a57e4017a62acaf4969d28206073714048c4ae0d Mon Sep 17 00:00:00 2001 From: Nicolas MURE Date: Thu, 13 Dec 2018 15:54:37 +0100 Subject: [PATCH 367/426] prepare v0.8 release --- CHANGELOG.md | 38 ++++++++++++++++++++++++++++++++++++++ composer.json | 2 +- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2f003e38b..91add7818 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,41 @@ +v0.8 +==== + +## New features + +- Implement `ChecksumCalculator` interface for `AzureBlobStorage` adapter #594 + +## Changes + +In #593 : +- Drop support for EOL php versions (5.6 and 7.0) +- Minimim requirement is now php 7.1 +- Add support for php 7.3 + +## Fixes + +- fix opencloud tests #579 +- fix appveyor build #589 +- Fix `ini_get()` for boolean values #595 + +Thank you @andreybolonin, @damijank, @deguif and @nicolasmure for your +contributions ! + +v0.7 +==== + +## Changes +- `FilesystemMap::set()` should expect `FilesystemInterface` instead of +`Filesystem` #576 + +## Fixes + +- Add PutObjectAcl in the required permission #566 +- Ensure correct return type from Flysystem adapter "exists" method #572 + +Thank you @andreybolonin, @clement-michelet, @jakob-stoeck, @nicolasmure, +@teohhanhui, @tristanbes for your contributions ! + v0.6 ==== diff --git a/composer.json b/composer.json index cd20ebea1..964a73140 100644 --- a/composer.json +++ b/composer.json @@ -63,7 +63,7 @@ }, "extra": { "branch-alias": { - "dev-master": "0.8.x-dev" + "dev-master": "0.9.x-dev" } } } From de40f5eae5e3740c76ff705f2c71f308e7ee9d40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=B4me=20Arzel?= Date: Thu, 10 Jan 2019 16:10:44 +0100 Subject: [PATCH 368/426] Cannot call `$this->delete($this->computePath($sourceKey))` with options['directory'] filled. --- src/Gaufrette/Adapter/GoogleCloudStorage.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/GoogleCloudStorage.php b/src/Gaufrette/Adapter/GoogleCloudStorage.php index 00ba518c9..7574a4628 100644 --- a/src/Gaufrette/Adapter/GoogleCloudStorage.php +++ b/src/Gaufrette/Adapter/GoogleCloudStorage.php @@ -256,7 +256,7 @@ public function rename($sourceKey, $targetKey) try { $this->service->objects->copy($this->bucket, $sourcePath, $this->bucket, $targetPath, $object); - $this->delete($sourcePath); + $this->service->objects->delete($this->bucket, $sourcePath); } catch (\Google_Service_Exception $e) { return false; } From d634708cc8b0d3d902909219a79b56459365b5e1 Mon Sep 17 00:00:00 2001 From: Nicolas MURE Date: Fri, 11 Jan 2019 10:27:30 +0100 Subject: [PATCH 369/426] prepare v0.8.1 release --- CHANGELOG.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91add7818..e9236406c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +v0.8.1 +====== + +## Fixes + +- Fix `rename` with `GoogleCloudStorage` adapter [#598](https://github.com/KnpLabs/Gaufrette/pull/598) + +Thank you @jerome-arzel for your contribution ! + v0.8 ==== From 17313c5862260bfc8e3769d68e4faac9be8851a5 Mon Sep 17 00:00:00 2001 From: Brian Sperduto Date: Sat, 26 Jan 2019 15:18:19 -0600 Subject: [PATCH 370/426] Adding SizeCalculator support to GridFS --- src/Gaufrette/Adapter/GridFS.php | 17 ++++++++++++++++- .../Gaufrette/Functional/Adapter/GridFSTest.php | 10 ++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/Gaufrette/Adapter/GridFS.php b/src/Gaufrette/Adapter/GridFS.php index fe2873183..fd5b28fca 100644 --- a/src/Gaufrette/Adapter/GridFS.php +++ b/src/Gaufrette/Adapter/GridFS.php @@ -17,7 +17,8 @@ class GridFS implements Adapter, ChecksumCalculator, MetadataSupporter, - ListKeysAware + ListKeysAware, + SizeCalculator { /** @var array */ private $metadata = []; @@ -205,4 +206,18 @@ public function listKeys($prefix = '') return $result; } + + public function size($key) + { + if (!$this->exists($key)) { + return false; + } + $size = $this->bucket->findOne(['filename' => $key], ['projection' => ['length' => 1,'_id' => 0]]); + if (!isset($size['length'])) { + return false; + } + + return $size['length']; + } + } diff --git a/tests/Gaufrette/Functional/Adapter/GridFSTest.php b/tests/Gaufrette/Functional/Adapter/GridFSTest.php index 447e80afb..436bd6ea2 100644 --- a/tests/Gaufrette/Functional/Adapter/GridFSTest.php +++ b/tests/Gaufrette/Functional/Adapter/GridFSTest.php @@ -89,4 +89,14 @@ public function testMetadataRetrieveAfterWrite() $this->assertEquals($this->filesystem->getAdapter()->getMetadata('metadatatest'), $fileadpt->getMetadata('metadatatest')); } + + /** + * @test + * Test to see if filesize works + */ + public function testSize() + { + $this->filesystem->write('sizetest.txt', 'data'); + $this->assertEquals(4, $this->filesystem->size('sizetest.txt')); + } } From c86cbacd187cccb232bc36d63ed41b88bd594188 Mon Sep 17 00:00:00 2001 From: Nicolas MURE Date: Tue, 19 Feb 2019 18:38:31 +0100 Subject: [PATCH 371/426] declare a FilesystemMapInterface --- spec/Gaufrette/FilesystemMapSpec.php | 2 +- src/Gaufrette/FilesystemMap.php | 64 ++++++++++-------------- src/Gaufrette/FilesystemMapInterface.php | 31 ++++++++++++ 3 files changed, 58 insertions(+), 39 deletions(-) create mode 100644 src/Gaufrette/FilesystemMapInterface.php diff --git a/spec/Gaufrette/FilesystemMapSpec.php b/spec/Gaufrette/FilesystemMapSpec.php index 28b05b297..9d3d56271 100644 --- a/spec/Gaufrette/FilesystemMapSpec.php +++ b/spec/Gaufrette/FilesystemMapSpec.php @@ -34,7 +34,7 @@ function it_sets_mapped_filesystem(Filesystem $filesystem) function it_fails_when_get_filesystem_which_was_not_mapped() { $this - ->shouldThrow(new \InvalidArgumentException('There is no filesystem defined for the "some" domain.')) + ->shouldThrow(new \InvalidArgumentException('There is no filesystem defined having "some" name.')) ->duringGet('some') ; } diff --git a/src/Gaufrette/FilesystemMap.php b/src/Gaufrette/FilesystemMap.php index 96deae392..b118f48e9 100644 --- a/src/Gaufrette/FilesystemMap.php +++ b/src/Gaufrette/FilesystemMap.php @@ -3,17 +3,17 @@ namespace Gaufrette; /** - * Associates filesystem instances to domains. + * Associates filesystem instances to their names. * * @author Antoine Hérault */ -class FilesystemMap +class FilesystemMap implements FilesystemMapInterface { private $filesystems = array(); /** * Returns an array of all the registered filesystems where the key is the - * domain and the value the filesystem. + * name and the value the filesystem. * * @return array */ @@ -23,76 +23,64 @@ public function all() } /** - * Register the given filesystem for the specified domain. + * Register the given filesystem for the specified name. * - * @param string $domain + * @param string $name * @param FilesystemInterface $filesystem * - * @throws \InvalidArgumentException when the specified domain contains + * @throws \InvalidArgumentException when the specified name contains * forbidden characters */ - public function set($domain, FilesystemInterface $filesystem) + public function set($name, FilesystemInterface $filesystem) { - if (!preg_match('/^[-_a-zA-Z0-9]+$/', $domain)) { + if (!preg_match('/^[-_a-zA-Z0-9]+$/', $name)) { throw new \InvalidArgumentException(sprintf( - 'The specified domain "%s" is not a valid domain.', - $domain + 'The specified name "%s" is not valid.', + $name )); } - $this->filesystems[$domain] = $filesystem; + $this->filesystems[$name] = $filesystem; } /** - * Indicates whether there is a filesystem registered for the specified - * domain. - * - * @param string $domain - * - * @return bool + * {@inheritdoc} */ - public function has($domain) + public function has($name) { - return isset($this->filesystems[$domain]); + return isset($this->filesystems[$name]); } /** - * Returns the filesystem registered for the specified domain. - * - * @param string $domain - * - * @return FilesystemInterface - * - * @throw \InvalidArgumentException when there is no filesystem registered - * for the specified domain + * {@inheritdoc} */ - public function get($domain) + public function get($name) { - if (!$this->has($domain)) { + if (!$this->has($name)) { throw new \InvalidArgumentException(sprintf( - 'There is no filesystem defined for the "%s" domain.', - $domain + 'There is no filesystem defined having "%s" name.', + $name )); } - return $this->filesystems[$domain]; + return $this->filesystems[$name]; } /** - * Removes the filesystem registered for the specified domain. + * Removes the filesystem registered for the specified name. * - * @param string $domain + * @param string $name */ - public function remove($domain) + public function remove($name) { - if (!$this->has($domain)) { + if (!$this->has($name)) { throw new \InvalidArgumentException(sprintf( 'Cannot remove the "%s" filesystem as it is not defined.', - $domain + $name )); } - unset($this->filesystems[$domain]); + unset($this->filesystems[$name]); } /** diff --git a/src/Gaufrette/FilesystemMapInterface.php b/src/Gaufrette/FilesystemMapInterface.php new file mode 100644 index 000000000..b2ced3bea --- /dev/null +++ b/src/Gaufrette/FilesystemMapInterface.php @@ -0,0 +1,31 @@ + Date: Thu, 30 May 2019 22:27:37 +0200 Subject: [PATCH 372/426] Change Filesystem to FilesystemInterface in File To extend functionality of Gaufrette to support `ResolvableFilesystem`, signature change is proposed to `FilesystemInterface` --- src/Gaufrette/File.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Gaufrette/File.php b/src/Gaufrette/File.php index 615d76940..998bae58b 100644 --- a/src/Gaufrette/File.php +++ b/src/Gaufrette/File.php @@ -50,9 +50,9 @@ class File /** * @param string $key - * @param Filesystem $filesystem + * @param FilesystemInterface $filesystem */ - public function __construct($key, Filesystem $filesystem) + public function __construct($key, FilesystemInterface $filesystem) { $this->key = $key; $this->name = $key; From 9260f3c06fcfa870762e2365c1f70aa5406860f7 Mon Sep 17 00:00:00 2001 From: Brian Sperduto Date: Sat, 26 Jan 2019 08:58:04 -0600 Subject: [PATCH 373/426] [GridFS] FIx : return empty array when object has no metadata --- src/Gaufrette/Adapter/GridFS.php | 9 ++++++--- .../Gaufrette/Functional/Adapter/GridFSTest.php | 17 ++++++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/Gaufrette/Adapter/GridFS.php b/src/Gaufrette/Adapter/GridFS.php index fd5b28fca..85446ab0e 100644 --- a/src/Gaufrette/Adapter/GridFS.php +++ b/src/Gaufrette/Adapter/GridFS.php @@ -171,10 +171,13 @@ public function getMetadata($key) return $this->metadata[$key]; } else { $meta = $this->bucket->findOne(['filename' => $key], ['projection' => ['metadata' => 1,'_id' => 0]]); - if ($meta === null) { + + if ($meta === null || !isset($meta['metadata'])) { return array(); } + $this->metadata[$key] = iterator_to_array($meta['metadata']); + return $this->metadata[$key]; } } @@ -206,7 +209,7 @@ public function listKeys($prefix = '') return $result; } - + public function size($key) { if (!$this->exists($key)) { @@ -219,5 +222,5 @@ public function size($key) return $size['length']; } - + } diff --git a/tests/Gaufrette/Functional/Adapter/GridFSTest.php b/tests/Gaufrette/Functional/Adapter/GridFSTest.php index 436bd6ea2..5f15a2ec5 100644 --- a/tests/Gaufrette/Functional/Adapter/GridFSTest.php +++ b/tests/Gaufrette/Functional/Adapter/GridFSTest.php @@ -74,7 +74,7 @@ public function shouldListKeys() $keys['keys'], '', 0, 10, true); } - + /** * @test * Tests metadata written to GridFS can be retrieved after writing @@ -83,10 +83,10 @@ public function testMetadataRetrieveAfterWrite() { //Create local copy of fileadapter $fileadpt = clone $this->filesystem->getAdapter(); - + $this->filesystem->getAdapter()->setMetadata('metadatatest', array('testing' => true)); $this->filesystem->write('metadatatest', 'test'); - + $this->assertEquals($this->filesystem->getAdapter()->getMetadata('metadatatest'), $fileadpt->getMetadata('metadatatest')); } @@ -99,4 +99,15 @@ public function testSize() $this->filesystem->write('sizetest.txt', 'data'); $this->assertEquals(4, $this->filesystem->size('sizetest.txt')); } + + /** + * @test + * Should retrieve empty metadata w/o errors + */ + public function testRetrieveWithoutMetadata() + { + $this->filesystem->write('no-metadata.txt', 'content'); + + $this->assertEquals(array(), $this->filesystem->getAdapter()->getMetadata('no-metadata.txt')); + } } From c85b964a9a288fbb38e98506e0f18f61dfc8f667 Mon Sep 17 00:00:00 2001 From: Alexis Thinardon Date: Fri, 14 Dec 2018 15:53:51 +0100 Subject: [PATCH 374/426] Fix directory deletion --- spec/Gaufrette/Adapter/LocalSpec.php | 5 ++ src/Gaufrette/Adapter/Local.php | 56 +++++++++++-- .../Functional/Adapter/LocalTest.php | 83 +++++++++---------- .../Functional/Adapter/SafeLocalTest.php | 7 +- .../Functional/FileStream/LocalTest.php | 11 ++- .../Functional/LocalDirectoryDeletor.php | 31 ------- 6 files changed, 109 insertions(+), 84 deletions(-) delete mode 100644 tests/Gaufrette/Functional/LocalDirectoryDeletor.php diff --git a/spec/Gaufrette/Adapter/LocalSpec.php b/spec/Gaufrette/Adapter/LocalSpec.php index cf68ea5e8..7f6a34c20 100644 --- a/spec/Gaufrette/Adapter/LocalSpec.php +++ b/spec/Gaufrette/Adapter/LocalSpec.php @@ -79,6 +79,11 @@ function it_deletes_file() $this->delete('filename1')->shouldReturn(false); } + function it_deletes_dir() + { + $this->delete('dir')->shouldReturn(true); + } + function it_checks_if_given_key_is_directory() { $this->isDirectory('dir')->shouldReturn(true); diff --git a/src/Gaufrette/Adapter/Local.php b/src/Gaufrette/Adapter/Local.php index b25a725b7..bab1f4dc1 100644 --- a/src/Gaufrette/Adapter/Local.php +++ b/src/Gaufrette/Adapter/Local.php @@ -53,6 +53,10 @@ public function __construct($directory, $create = false, $mode = 0777) */ public function read($key) { + if ($this->isDirectory($key)) { + return false; + } + return file_get_contents($this->computePath($key)); } @@ -141,17 +145,18 @@ public function mtime($key) /** * {@inheritdoc} * - * @throws \OutOfBoundsException If the computed path is out of the directory - * @throws \InvalidArgumentException if the directory already exists - * @throws \RuntimeException if the directory could not be created + * Can also delete a directory recursively when the given $key matches a + * directory. */ public function delete($key) { if ($this->isDirectory($key)) { - return rmdir($this->computePath($key)); + return $this->deleteDirectory($this->computePath($key)); + } elseif ($this->exists($key)) { + return unlink($this->computePath($key)); } - return unlink($this->computePath($key)); + return false; } /** @@ -309,4 +314,45 @@ protected function createDirectory($directory) throw new \RuntimeException(sprintf('The directory \'%s\' could not be created.', $directory)); } } + + /** + * @param string The directory's path to delete + * + * @throws \InvalidArgumentException When attempting to delete the root + * directory of this adapter. + * + * @return bool Wheter the operation succeeded or not + */ + private function deleteDirectory($directory) + { + if ($this->directory === $directory) { + throw new \InvalidArgumentException( + sprintf('Impossible to delete the root directory of this Local adapter ("%s").', $directory) + ); + } + + $status = true; + + if (file_exists($directory)) { + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator( + $directory, + \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS + ), + \RecursiveIteratorIterator::CHILD_FIRST + ); + + foreach ($iterator as $item) { + if ($item->isDir()) { + $status = $status && rmdir(strval($item)); + } else { + $status = $status && unlink(strval($item)); + } + } + + $status = $status && rmdir($directory); + } + + return $status; + } } diff --git a/tests/Gaufrette/Functional/Adapter/LocalTest.php b/tests/Gaufrette/Functional/Adapter/LocalTest.php index 7dbd9d27b..4958fdefb 100644 --- a/tests/Gaufrette/Functional/Adapter/LocalTest.php +++ b/tests/Gaufrette/Functional/Adapter/LocalTest.php @@ -4,7 +4,6 @@ use Gaufrette\Filesystem; use Gaufrette\Adapter\Local; -use Gaufrette\Functional\LocalDirectoryDeletor; class LocalTest extends FunctionalTestCase { @@ -23,9 +22,15 @@ public function setUp() public function tearDown() { + $adapter = $this->filesystem->getAdapter(); + + foreach ($this->filesystem->keys() as $key) { + $adapter->delete($key); + } + $this->filesystem = null; - LocalDirectoryDeletor::deleteDirectory($this->directory); + rmdir($this->directory); } /** @@ -51,11 +56,12 @@ public function shouldWorkWithSyslink() @unlink($linkname); symlink($dirname, $linkname); - $this->filesystem = new Filesystem(new Local($linkname)); - $this->filesystem->write('test.txt', 'abc 123'); + $fs = new Filesystem(new Local($linkname)); + $fs->write('test.txt', 'abc 123'); + + $this->assertSame('abc 123', $fs->read('test.txt')); + $fs->delete('test.txt'); - $this->assertSame('abc 123', $this->filesystem->read('test.txt')); - $this->filesystem->delete('test.txt'); @unlink($linkname); @rmdir($dirname); } @@ -67,13 +73,6 @@ public function shouldWorkWithSyslink() */ public function shouldListingOnlyGivenDirectory() { - $dirname = sprintf( - '%s/localDir', - $this->directory - ); - @mkdir($dirname); - - $this->filesystem = new Filesystem(new Local($this->directory)); $this->filesystem->write('aaa.txt', 'some content'); $this->filesystem->write('localDir/test.txt', 'some content'); @@ -89,10 +88,6 @@ public function shouldListingOnlyGivenDirectory() $this->assertCount(2, $dirs['keys']); $this->assertEquals('aaa.txt', $dirs['keys'][0]); $this->assertEquals('localDir/test.txt', $dirs['keys'][1]); - - @unlink($dirname.DIRECTORY_SEPARATOR.'test.txt'); - @unlink($this->directory.DIRECTORY_SEPARATOR.'aaa.txt'); - @rmdir($dirname); } /** @@ -102,29 +97,15 @@ public function shouldListingOnlyGivenDirectory() */ public function shouldListingAllKeys() { - $dirname = sprintf( - '%s/localDir', - $this->directory - ); - @mkdir($dirname); - - $this->filesystem = new Filesystem(new Local($this->directory)); $this->filesystem->write('aaa.txt', 'some content'); $this->filesystem->write('localDir/dir1/dir2/dir3/test.txt', 'some content'); $keys = $this->filesystem->keys(); $dirs = $this->filesystem->listKeys(); + $this->assertCount(6, $keys); $this->assertCount(4, $dirs['dirs']); $this->assertEquals('localDir/dir1/dir2/dir3/test.txt', $dirs['keys'][1]); - - foreach ($dirs['keys'] as $item) { - @unlink($item); - } - $reversed = array_reverse($dirs['dirs']); - foreach ($reversed as $item) { - @rmdir($item); - } } /** @@ -133,15 +114,6 @@ public function shouldListingAllKeys() */ public function shouldBeAbleToClearCache() { - $dirname = sprintf( - '%s/adapters/bbb', - dirname(__DIR__) - ); - - @mkdir($dirname); - - $this->filesystem = new Filesystem(new Local($dirname)); - $this->filesystem->get('test.txt', true); $this->filesystem->write('test.txt', '123', true); @@ -161,9 +133,32 @@ public function shouldBeAbleToClearCache() $fsRegister = $fsReflection->getProperty('fileRegister'); $fsRegister->setAccessible(true); $this->assertCount(0, $fsRegister->getValue($this->filesystem)); + } - $this->filesystem->delete('test.txt'); - $this->filesystem->delete('test2.txt'); - @rmdir($dirname); + /** + * @test + * @group functional + */ + public function shouldDeleteDirectory() + { + $path = $this->directory . DIRECTORY_SEPARATOR . 'delete-me.d'; + mkdir($path); + + $this->assertTrue(is_dir($path)); + + $this->assertTrue($this->filesystem->getAdapter()->delete('delete-me.d')); + + $this->assertFalse(is_dir($path)); + } + + /** + * @test + * @group functional + * @expectedException \InvalidArgumentException + * @expectedExceptionMessage Impossible to delete the root directory of this Local adapter + */ + public function shouldNotDeleteTheAdapterRootDirectory() + { + $this->filesystem->getAdapter()->delete('/'); } } diff --git a/tests/Gaufrette/Functional/Adapter/SafeLocalTest.php b/tests/Gaufrette/Functional/Adapter/SafeLocalTest.php index 257cabce5..efe299e8e 100644 --- a/tests/Gaufrette/Functional/Adapter/SafeLocalTest.php +++ b/tests/Gaufrette/Functional/Adapter/SafeLocalTest.php @@ -4,7 +4,6 @@ use Gaufrette\Filesystem; use Gaufrette\Adapter\SafeLocal; -use Gaufrette\Functional\LocalDirectoryDeletor; class SafeLocalTest extends FunctionalTestCase { @@ -19,9 +18,13 @@ public function setUp() public function tearDown() { + foreach ($this->filesystem->keys() as $key) { + $this->filesystem->delete($key); + } + $this->filesystem = null; - LocalDirectoryDeletor::deleteDirectory($this->getDirectory()); + rmdir($this->getDirectory()); } private function getDirectory() diff --git a/tests/Gaufrette/Functional/FileStream/LocalTest.php b/tests/Gaufrette/Functional/FileStream/LocalTest.php index 4837bedc7..c05c7fb0d 100644 --- a/tests/Gaufrette/Functional/FileStream/LocalTest.php +++ b/tests/Gaufrette/Functional/FileStream/LocalTest.php @@ -4,7 +4,6 @@ use Gaufrette\Filesystem; use Gaufrette\Adapter\Local as LocalAdapter; -use Gaufrette\Functional\LocalDirectoryDeletor; class LocalTest extends FunctionalTestCase { @@ -35,7 +34,15 @@ public function testDirectoryChmod() public function tearDown() { - LocalDirectoryDeletor::deleteDirectory($this->directory); + $adapter = $this->filesystem->getAdapter(); + + foreach ($this->filesystem->keys() as $key) { + $adapter->delete($key); + } + + $this->filesystem = null; + + rmdir($this->directory); } /** diff --git a/tests/Gaufrette/Functional/LocalDirectoryDeletor.php b/tests/Gaufrette/Functional/LocalDirectoryDeletor.php deleted file mode 100644 index 5974bbe71..000000000 --- a/tests/Gaufrette/Functional/LocalDirectoryDeletor.php +++ /dev/null @@ -1,31 +0,0 @@ -isDir()) { - rmdir(strval($item)); - } else { - unlink(strval($item)); - } - } - } - } -} From 8b23f732055411885520017e7688042ef368bb1b Mon Sep 17 00:00:00 2001 From: Nicolas MURE Date: Wed, 5 Jun 2019 09:40:10 +0200 Subject: [PATCH 375/426] add doc about Local adapter directory deletion feature --- doc/adapters/local.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/doc/adapters/local.md b/doc/adapters/local.md index af742e17d..820eeb292 100644 --- a/doc/adapters/local.md +++ b/doc/adapters/local.md @@ -22,3 +22,17 @@ use Gaufrette\Adapter\Local as LocalAdapter; $adapter = new LocalAdapter('/var/media', true, 0750); $filesystem = new Filesystem($adapter); ``` + +## Delete a directory + +Directory deletion is a feature only available on the `Local` adapter. It is +not supported by the `FilesysteInterface` which aims to only deal with objects. + +Following the above statement, you have to explicitely retrieve the adapter +in order to delete a directory : + +```php +$filesystem->getAdapter()->delete($dirKey); +``` + +Note that you can't delete the root directory of the Local adapter. From 512a96843534187d4897fbe8d0b357da5e087133 Mon Sep 17 00:00:00 2001 From: Nicolas MURE Date: Wed, 5 Jun 2019 18:35:48 +0200 Subject: [PATCH 376/426] prepare v0.8.2 release --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e9236406c..aaeb30179 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +v0.8.2 +====== + +## New features + +- Declare a FilesystemMapInterface (#604) +- Add SizeCalculator support to GridFS (#603) +- Local Adapter: directory deletion (#610) + +## Fixes + +- GridFS : return empty array when object has no metadata (#609) + v0.8.1 ====== From 5ce4f20fc7d863ca9b5fed25b3f5abfd700ac74a Mon Sep 17 00:00:00 2001 From: Nicolas MURE Date: Thu, 6 Jun 2019 11:26:12 +0200 Subject: [PATCH 377/426] prepare v0.8.3 release --- CHANGELOG.md | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index aaeb30179..4d48edc63 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +v0.8.3 +====== + +## Changes + +- Change Filesystem to FilesystemInterface in File's constructor (#608) + +Thank you @athlan for your contribution ! + v0.8.2 ====== @@ -11,6 +20,8 @@ v0.8.2 - GridFS : return empty array when object has no metadata (#609) +Thank you @athos7933, @bsperduto and @nicolasmure for your contributions ! + v0.8.1 ====== From f2f3822359ea7ab911ae23c542f676ed31fbff25 Mon Sep 17 00:00:00 2001 From: Nicolas MURE Date: Fri, 7 Sep 2018 13:34:15 +0200 Subject: [PATCH 378/426] fix docker setup --- .dockerignore | 15 +--- Dockerfile-php71 | 56 ------------- Makefile | 21 +++++ README.md | 56 +++++++++---- bin/tests | 1 - bin/tests-all | 7 -- docker-compose.yml | 44 +++++++--- docker/{Dockerfile-ftp => ftp/Dockerfile} | 0 docker/install-composer.sh | 21 ----- docker/php/Dockerfile | 98 +++++++++++++++++++++++ docker/{ => php}/php.ini | 0 docker/run-task | 15 ++++ 12 files changed, 212 insertions(+), 122 deletions(-) delete mode 100644 Dockerfile-php71 create mode 100644 Makefile delete mode 100755 bin/tests-all rename docker/{Dockerfile-ftp => ftp/Dockerfile} (100%) delete mode 100755 docker/install-composer.sh create mode 100644 docker/php/Dockerfile rename docker/{ => php}/php.ini (100%) create mode 100755 docker/run-task diff --git a/.dockerignore b/.dockerignore index c4021d6cd..5a31182db 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,13 +1,2 @@ -.idea/ -bin/tests-all -doc/ -vendor/ -.dockerignore -.editorconfig -.gitignore -.travis.yml -appveyor.yml -composer.lock -docker-compose.yml -LICENSE -README.md +# ignore all by default +* diff --git a/Dockerfile-php71 b/Dockerfile-php71 deleted file mode 100644 index ac5026ceb..000000000 --- a/Dockerfile-php71 +++ /dev/null @@ -1,56 +0,0 @@ -FROM php:7.1-alpine - -ENV SRC_DIR /usr/src/gaufrette - -RUN apk add --no-cache --virtual .persistent-deps \ - git \ - zlib - -# PHP extensions -ENV MONGODB_VERSION="1.2.11" \ - SSH2_VERSION="1.1.2" - -RUN set -xe \ - && apk add --no-cache --virtual .build-deps \ - $PHPIZE_DEPS \ - libssh2-dev \ - zlib-dev \ - && docker-php-ext-install \ - zip \ - && pecl install \ - mongodb-${MONGODB_VERSION} \ - ssh2-${SSH2_VERSION} \ - && docker-php-ext-enable --ini-name 05-opcache.ini opcache \ - && docker-php-ext-enable \ - mongodb \ - ssh2 \ - && apk del .build-deps - -COPY docker/php.ini /usr/local/etc/php/php.ini - -COPY docker/install-composer.sh /usr/local/bin/install-composer -RUN chmod +x /usr/local/bin/install-composer - -RUN set -xe \ - && install-composer \ - && mv composer.phar /usr/local/bin/composer - -ENV COMPOSER_ALLOW_SUPERUSER 1 - -RUN composer global require "hirak/prestissimo" --prefer-dist --no-progress --no-suggest --optimize-autoloader --apcu-autoloader \ - && composer clear-cache - -WORKDIR ${SRC_DIR} - -COPY composer.json ./ -RUN composer update --prefer-dist --no-autoloader --no-scripts --no-progress --no-suggest \ - && composer clear-cache - -COPY spec spec/ -COPY src src/ -COPY tests tests/ -COPY bin/tests bin/tests - -RUN composer dump-autoload - -CMD ["bin/tests"] diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..fbef71da8 --- /dev/null +++ b/Makefile @@ -0,0 +1,21 @@ +PHP_VERSION ?= 7.2 + +.PHONY: dev +dev: + cp .env.dist .env + +.PHONY: build +build: + docker-compose build php${PHP_VERSION} + +.PHONY: install-deps +install-deps: + docker/run-task php${PHP_VERSION} composer install + +.PHONY: tests +tests: + docker/run-task php${PHP_VERSION} bin/tests + +.PHONY: clear-deps +clear-deps: + rm -rf vendor/ composer.lock diff --git a/README.md b/README.md index 71c7da83c..601894bb4 100644 --- a/README.md +++ b/README.md @@ -32,8 +32,8 @@ Read the official [Gaufrette documentation](http://knplabs.github.io/Gaufrette/) ### Metapackages for adapters -Every maintained adapter now have a dedicated metapackage. You can [find the list on packagist](https://packagist.org/packages/gaufrette/). -**We highly recommend you to use them as they contain their own requirements**: you don't need to worry about third-party dependencies +Every maintained adapter now have a dedicated metapackage. You can [find the list on packagist](https://packagist.org/packages/gaufrette/). +**We highly recommend you to use them as they contain their own requirements**: you don't need to worry about third-party dependencies to install before using Gaufrette anymore. ### Symfony integration @@ -42,7 +42,7 @@ Symfony integration is available through [KnpLabs/KnpGaufretteBundle](https://gi ### Maintainers -Here is the list of dedicated maintainer(s) for every adapter not deprecated. If you don't receive any response to +Here is the list of dedicated maintainer(s) for every adapter not deprecated. If you don't receive any response to your issue or pull request in a timely manner, ping us: | Adapter | Referent | @@ -62,21 +62,47 @@ your issue or pull request in a timely manner, ping us: For `InMemory`, `Local` and `Zip` adapters everyone in this list is considered as a maintainer. -### Launch the Test Suite +### Development -Requires: - * docker +Requires the latest versions of : + * docker-ce * docker-compose -Build images: - - $ docker-compose build - -Launch the tests: - - $ bin/tests-all - -Is it green? +1) Create `.env` file : +```bash +$ make dev +``` +and configure it as you want. + +2) Build the php docker image : +```bash +$ make build +``` + +3) Install dependencies : +```bash +$ make install-deps +``` + +4) Run tests : +```bash +$ make tests +``` + +You can also use a different php version, simply set the `PHP_VERSION` env var +to any of these values when calling a make target : +- `7.1` +- `7.2` (default) +- `7.3` (the docker setup for php 7.3 is available, however the image fails +to build for now as the [ssh2 extension is not available for php 7/3 yet](https://serverpilot.io/docs/how-to-install-the-php-ssh2-extension)) +See the [`docker-compose.yml`](/docker-compose.yml) file for more details. + +You'll need to clear the previously installed dependencies when switching from +a version to an other, to do so, run : +```bash +$ make clear-deps +$ PHP_VERSION= make build install-deps +``` ### Note diff --git a/bin/tests b/bin/tests index d15f6a66a..953e4fd07 100755 --- a/bin/tests +++ b/bin/tests @@ -1,7 +1,6 @@ #!/bin/sh set -o nounset -set -o errexit set -o xtrace DIR=$(dirname $0)/.. diff --git a/bin/tests-all b/bin/tests-all deleted file mode 100755 index e88aefe9e..000000000 --- a/bin/tests-all +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/sh - -set -o xtrace - -docker-compose run --rm php56 -docker-compose run --rm php70 -docker-compose run --rm php71 diff --git a/docker-compose.yml b/docker-compose.yml index 7af5366b5..239b82b1b 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,16 +1,43 @@ -version: '3' +version: '3.4' services: - php71: + php7.1: build: context: . - dockerfile: ./Dockerfile-php71 + dockerfile: docker/php/Dockerfile + target: php7.1 + env_file: .env + volumes: + - './:/app' + - './docker/php/php.ini:/usr/local/etc/php/php.ini:ro' + depends_on: + - mongodb + - sftp + - ftp + + php7.2: + build: + context: . + dockerfile: docker/php/Dockerfile + target: php7.2 + env_file: .env volumes: - - './spec:/usr/src/gaufrette/spec:rw' - - './src:/usr/src/gaufrette/src:ro' - - './tests:/usr/src/gaufrette/tests:rw' - - './bin/tests:/usr/src/gaufrette/bin/tests:ro' + - './:/app' + - './docker/php/php.ini:/usr/local/etc/php/php.ini:ro' + depends_on: + - mongodb + - sftp + - ftp + + php7.3: + build: + context: . + dockerfile: docker/php/Dockerfile + target: php7.3 env_file: .env + volumes: + - './:/app' + - './docker/php/php.ini:/usr/local/etc/php/php.ini:ro' depends_on: - mongodb - sftp @@ -25,7 +52,6 @@ services: ftp: build: - context: docker - dockerfile: Dockerfile-ftp + context: ./docker/ftp environment: PUBLICHOST: 'ftp' diff --git a/docker/Dockerfile-ftp b/docker/ftp/Dockerfile similarity index 100% rename from docker/Dockerfile-ftp rename to docker/ftp/Dockerfile diff --git a/docker/install-composer.sh b/docker/install-composer.sh deleted file mode 100755 index f4ad41bfc..000000000 --- a/docker/install-composer.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/bin/sh - -# Copyright (c) Nils Adermann, Jordi Boggiano -# Origin: https://github.com/composer/composer/blob/master/doc/faqs/how-to-install-composer-programmatically.md -# Licence: MIT - -EXPECTED_SIGNATURE=$(wget -q -O - https://composer.github.io/installer.sig) -php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" -ACTUAL_SIGNATURE=$(php -r "echo hash_file('SHA384', 'composer-setup.php');") - -if [ "$EXPECTED_SIGNATURE" != "$ACTUAL_SIGNATURE" ] -then - >&2 echo 'ERROR: Invalid installer signature' - rm composer-setup.php - exit 1 -fi - -php composer-setup.php --quiet -RESULT=$? -rm composer-setup.php -exit $RESULT diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile new file mode 100644 index 000000000..68e2a1b49 --- /dev/null +++ b/docker/php/Dockerfile @@ -0,0 +1,98 @@ +# To copy composer binary file +FROM composer:1.8.5 as composer + +################################################################################ + +FROM php:7.1-alpine as php7.1 + +RUN apk add --no-cache \ + autoconf \ + g++\ + git \ + libssh2-dev \ + make \ + zlib-dev \ + && pecl install \ + mongodb \ + ssh2-1.1.2 + +RUN docker-php-ext-install \ + zip \ + && docker-php-ext-enable \ + mongodb \ + ssh2 + +COPY --from=composer /usr/bin/composer /usr/bin/composer +ENV COMPOSER_HOME /var/cache/composer +RUN mkdir /var/cache/composer \ + && chown 1000:1000 /var/cache/composer + +USER 1000 + +RUN composer global require "hirak/prestissimo" --prefer-dist + +WORKDIR /app + +################################################################################ + +FROM php:7.2-alpine as php7.2 + +RUN apk add --no-cache \ + autoconf \ + g++\ + git \ + libssh2-dev \ + make \ + zlib-dev \ + && pecl install \ + mongodb \ + ssh2-1.1.2 + +RUN docker-php-ext-install \ + zip \ + && docker-php-ext-enable \ + mongodb \ + ssh2 + +COPY --from=composer /usr/bin/composer /usr/bin/composer +ENV COMPOSER_HOME /var/cache/composer +RUN mkdir /var/cache/composer \ + && chown 1000:1000 /var/cache/composer + +USER 1000 + +RUN composer global require "hirak/prestissimo" --prefer-dist + +WORKDIR /app + +################################################################################ + +FROM php:7.3-alpine as php7.3 + +RUN apk add --no-cache \ + autoconf \ + g++\ + git \ + libssh2-dev \ + make \ + zlib-dev \ + && pecl install \ + mongodb \ + ssh2-1.1.2 + +RUN docker-php-ext-install \ + zip \ + && docker-php-ext-enable \ + mongodb \ + ssh2 + +COPY --from=composer /usr/bin/composer /usr/bin/composer +ENV COMPOSER_HOME /var/cache/composer +RUN mkdir /var/cache/composer \ + && chown 1000:1000 /var/cache/composer + +USER 1000 + +RUN composer global require "hirak/prestissimo" --prefer-dist + +WORKDIR /app diff --git a/docker/php.ini b/docker/php/php.ini similarity index 100% rename from docker/php.ini rename to docker/php/php.ini diff --git a/docker/run-task b/docker/run-task new file mode 100755 index 000000000..c33840b90 --- /dev/null +++ b/docker/run-task @@ -0,0 +1,15 @@ +#!/bin/bash +# Usage: run-task SERVICE [CMD...] + +set -o nounset + +# env var used by the `docker-compose` command +COMPOSE_FILE="$(dirname $0)/docker-compose.yml" + +docker-compose run --rm "${@}" +EXIT_CODE="${?}" + +# stop the other services linked to the task +docker-compose down + +exit "${EXIT_CODE}" From cf2b581f79730a9e76817ad4e45a13976bc3dbac Mon Sep 17 00:00:00 2001 From: Nicolas MURE Date: Fri, 7 Jun 2019 16:27:00 +0200 Subject: [PATCH 379/426] do not install ssh2 ext for php 7.3 as it is not available yet See https://serverpilot.io/docs/how-to-install-the-php-ssh2-extension --- README.md | 5 +++-- docker/php/Dockerfile | 13 ++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 601894bb4..a38e4d6f1 100644 --- a/README.md +++ b/README.md @@ -93,8 +93,9 @@ You can also use a different php version, simply set the `PHP_VERSION` env var to any of these values when calling a make target : - `7.1` - `7.2` (default) -- `7.3` (the docker setup for php 7.3 is available, however the image fails -to build for now as the [ssh2 extension is not available for php 7/3 yet](https://serverpilot.io/docs/how-to-install-the-php-ssh2-extension)) +- `7.3` (the docker setup for php 7.3 is available, however the ssh2 extension +is not installed [as it is not available for php 7.3 yet](https://serverpilot.io/docs/how-to-install-the-php-ssh2-extension)) + See the [`docker-compose.yml`](/docker-compose.yml) file for more details. You'll need to clear the previously installed dependencies when switching from diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile index 68e2a1b49..30ed72c72 100644 --- a/docker/php/Dockerfile +++ b/docker/php/Dockerfile @@ -73,18 +73,21 @@ RUN apk add --no-cache \ autoconf \ g++\ git \ - libssh2-dev \ + # libssh2-dev \ + libzip-dev \ make \ zlib-dev \ && pecl install \ - mongodb \ - ssh2-1.1.2 + mongodb + # ssh2-1.1.2 + # ssh2 extension is not availble yet for php7.3 + # see https://serverpilot.io/docs/how-to-install-the-php-ssh2-extension RUN docker-php-ext-install \ zip \ && docker-php-ext-enable \ - mongodb \ - ssh2 + mongodb + # ssh2 COPY --from=composer /usr/bin/composer /usr/bin/composer ENV COMPOSER_HOME /var/cache/composer From 7a4d3458f7400b5d2f072e49be5bb52387611f41 Mon Sep 17 00:00:00 2001 From: Nicolas MURE Date: Tue, 11 Jun 2019 15:50:56 +0200 Subject: [PATCH 380/426] Define env vars in .env file only The tests are using env vars to run. These env vars are storing the credentials to the different backends. For the CI, these env vars are set on the project settings (on travis for instance). For the development of Gaufrette, and considering the merge of (#580), the tests should be ran using docker. The docker setup uses env vars defined in .env files, so we no longer need to define such vars in the phpunit.xml file. --- .env.dist | 22 +++++++++++----------- phpunit.xml.dist | 34 +--------------------------------- 2 files changed, 12 insertions(+), 44 deletions(-) diff --git a/.env.dist b/.env.dist index b3553749d..1e6a2ff15 100644 --- a/.env.dist +++ b/.env.dist @@ -2,14 +2,9 @@ AWS_KEY= AWS_SECRET= AWS_BUCKET= -MONGO_URI=mongodb://mongodb:27017 -MONGO_DBNAME=gridfs_test - -SFTP_HOST=sftp -SFTP_PORT=22 -SFTP_USER=gaufrette -SFTP_PASSWORD=gaufrette -SFTP_BASE_DIR=gaufrette +AZURE_ACCOUNT= +AZURE_KEY= +AZURE_CONTAINER= FTP_HOST=ftp FTP_PORT=21 @@ -17,10 +12,15 @@ FTP_USER=gaufrette FTP_PASSWORD=gaufrette FTP_BASE_DIR=/gaufrette -AZURE_ACCOUNT= -AZURE_KEY= -AZURE_CONTAINER= +MONGO_URI=mongodb://mongodb:27017 +MONGO_DBNAME=gridfs_test RACKSPACE_USER= RACKSPACE_APIKEY= RACKSPACE_CONTAINER= + +SFTP_HOST=sftp +SFTP_PORT=22 +SFTP_USER=gaufrette +SFTP_PASSWORD=gaufrette +SFTP_BASE_DIR=gaufrette diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ad116a588..456decf12 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -10,39 +10,7 @@ stopOnFailure="false" bootstrap="vendor/autoload.php" > - - - - - - - - - - - - - - - - - - - - + From 30c90ab345b446e6a7098faed26b4abdb7b1ab6a Mon Sep 17 00:00:00 2001 From: Nicolas MURE Date: Tue, 18 Jun 2019 17:33:07 +0200 Subject: [PATCH 381/426] php CS config --- .editorconfig | 3 +++ .gitignore | 7 +++--- .php_cs.dist | 59 +++++++++++++++++++++++++++++++++++++++++++++++++++ .travis.yml | 1 + Makefile | 18 ++++++++++++++++ README.md | 11 ++++++++++ 6 files changed, 96 insertions(+), 3 deletions(-) create mode 100644 .php_cs.dist diff --git a/.editorconfig b/.editorconfig index fed584a77..299bd7006 100644 --- a/.editorconfig +++ b/.editorconfig @@ -13,3 +13,6 @@ trim_trailing_whitespace = false [*.yml] indent_size = 2 + +[Makefile] +indent_style = tab diff --git a/.gitignore b/.gitignore index d5e9e375a..441023695 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,11 @@ bin/* !bin/tests !bin/tests-all +composer.lock doc/.couscous +.env +.php_cs.cache +phpunit.xml tests/adapters/* !tests/adapters/*.dist vendor/ -.env -composer.lock -phpunit.xml diff --git a/.php_cs.dist b/.php_cs.dist new file mode 100644 index 000000000..211cf146a --- /dev/null +++ b/.php_cs.dist @@ -0,0 +1,59 @@ +exclude('node_modules') + ->exclude('vendor') + ->in(__DIR__) +; + +return PhpCsFixer\Config::create() + ->setRules([ + '@PSR2' => true, + 'array_syntax' => [ 'syntax' => 'short' ], + 'binary_operator_spaces' => [ 'align_equals' => false, 'align_double_arrow' => false ], + 'cast_spaces' => true, + 'combine_consecutive_unsets' => true, + 'concat_space' => [ 'spacing' => 'one' ], + 'linebreak_after_opening_tag' => true, + 'no_blank_lines_after_class_opening' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_extra_consecutive_blank_lines' => true, + 'no_trailing_comma_in_singleline_array' => true, + 'no_whitespace_in_blank_line' => true, + 'no_spaces_around_offset' => true, + 'no_unused_imports' => true, + 'no_useless_else' => true, + 'no_useless_return' => true, + 'no_whitespace_before_comma_in_array' => true, + 'normalize_index_brace' => true, + 'phpdoc_indent' => true, + 'phpdoc_to_comment' => true, + 'phpdoc_trim' => true, + 'single_quote' => true, + 'ternary_to_null_coalescing' => true, + 'trailing_comma_in_multiline_array' => true, + 'trim_array_spaces' => true, + 'method_argument_space' => ['ensure_fully_multiline' => false], + 'no_break_comment' => false, + 'blank_line_before_statement' => true, + ]) + ->setFinder($finder) +; diff --git a/.travis.yml b/.travis.yml index 085c8fc42..4e6ca082c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,6 +31,7 @@ matrix: - php: '7.3' before_install: + - make php-cs-compare - echo "extension = mongodb.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; - phpenv config-rm xdebug.ini || echo "xdebug not available" - docker run -p ${SFTP_PORT}:22 -d atmoz/sftp:alpine ${SFTP_USER}:${SFTP_PASSWORD}:::${SFTP_BASE_DIR} diff --git a/Makefile b/Makefile index fbef71da8..dcc3b9e38 100644 --- a/Makefile +++ b/Makefile @@ -19,3 +19,21 @@ tests: .PHONY: clear-deps clear-deps: rm -rf vendor/ composer.lock + +.PHONY: php-cs-compare +php-cs-compare: + docker run --rm -u $$(id -u):$$(id -g) -v $$(pwd):/project \ + herloct/php-cs-fixer fix \ + --config=/project/.php_cs.dist \ + --diff \ + --dry-run \ + --show-progress=none \ + --verbose \ + src spec tests + +.PHONY: php-cs-fix +php-cs-fix: + docker run --rm -u $$(id -u):$$(id -g) -v $$(pwd):/project \ + herloct/php-cs-fixer fix \ + --config=/project/.php_cs.dist \ + src spec tests diff --git a/README.md b/README.md index a38e4d6f1..a16fc677a 100644 --- a/README.md +++ b/README.md @@ -105,6 +105,17 @@ $ make clear-deps $ PHP_VERSION= make build install-deps ``` +5) Apply Coding Standards + +You should check for CS violations by using +```bash +$ make php-cs-compare +``` +and fix them with +```bash +$ make php-cs-fix +``` + ### Note This project does not have any stable release yet but we do not want to break BC now. From d7fb1b109cd68f051e119f154831f4f809e0cebf Mon Sep 17 00:00:00 2001 From: Nicolas MURE Date: Tue, 18 Jun 2019 18:06:17 +0200 Subject: [PATCH 382/426] PHP CS fix (make php-cs-fix) --- .php_cs.dist | 42 ++-- .travis.yml | 2 +- Makefile | 12 +- composer.json | 3 +- .../Adapter/AclAwareAmazonS3Spec.php | 32 +-- spec/Gaufrette/Adapter/AmazonS3Spec.php | 136 ++++++------ spec/Gaufrette/Adapter/ApcSpec.php | 2 +- spec/Gaufrette/Adapter/AzureBlobStorage.php | 59 +++--- .../AzureBlobStorage/BlobProxyFactory.php | 6 +- spec/Gaufrette/Adapter/CacheSpec.php | 18 +- spec/Gaufrette/Adapter/DoctrineDbalSpec.php | 92 +++++---- spec/Gaufrette/Adapter/DropboxSpec.php | 64 +++--- spec/Gaufrette/Adapter/FlysystemSpec.php | 5 +- spec/Gaufrette/Adapter/FtpSpec.php | 58 +++--- .../Adapter/GoogleCloudStorageSpec.php | 8 +- spec/Gaufrette/Adapter/GridFSSpec.php | 1 - spec/Gaufrette/Adapter/InMemorySpec.php | 16 +- spec/Gaufrette/Adapter/LazyOpenCloudSpec.php | 10 +- spec/Gaufrette/Adapter/LocalSpec.php | 4 +- spec/Gaufrette/Adapter/MogileFSSpec.php | 2 +- spec/Gaufrette/Adapter/OpenCloudSpec.php | 30 +-- spec/Gaufrette/Adapter/PhpseclibSftpSpec.php | 28 +-- spec/Gaufrette/Adapter/SafeLocalSpec.php | 6 +- spec/Gaufrette/Adapter/SftpSpec.php | 4 +- spec/Gaufrette/Adapter/functions.php | 193 +++++++++--------- spec/Gaufrette/FileSpec.php | 11 +- spec/Gaufrette/FilesystemMapSpec.php | 2 +- spec/Gaufrette/FilesystemSpec.php | 57 +++--- spec/Gaufrette/StreamWrapperSpec.php | 44 ++-- spec/Gaufrette/Util/ChecksumSpec.php | 24 ++- src/Gaufrette/Adapter/AclAwareAmazonS3.php | 19 +- src/Gaufrette/Adapter/AmazonS3.php | 27 ++- src/Gaufrette/Adapter/Apc.php | 10 +- src/Gaufrette/Adapter/AwsS3.php | 15 +- src/Gaufrette/Adapter/AzureBlobStorage.php | 15 +- .../AzureBlobStorage/BlobProxyFactory.php | 4 +- src/Gaufrette/Adapter/Cache.php | 5 +- src/Gaufrette/Adapter/DoctrineDbal.php | 40 ++-- src/Gaufrette/Adapter/Dropbox.php | 6 +- src/Gaufrette/Adapter/Ftp.php | 101 ++++----- src/Gaufrette/Adapter/GoogleCloudStorage.php | 26 ++- src/Gaufrette/Adapter/GridFS.php | 22 +- src/Gaufrette/Adapter/InMemory.php | 19 +- src/Gaufrette/Adapter/LazyOpenCloud.php | 2 +- src/Gaufrette/Adapter/Local.php | 9 +- src/Gaufrette/Adapter/MogileFS.php | 45 ++-- src/Gaufrette/Adapter/OpenCloud.php | 5 +- src/Gaufrette/Adapter/PhpseclibSftp.php | 20 +- src/Gaufrette/Adapter/Sftp.php | 11 +- src/Gaufrette/Adapter/Zip.php | 18 +- src/Gaufrette/File.php | 6 +- src/Gaufrette/Filesystem.php | 12 +- src/Gaufrette/FilesystemInterface.php | 1 - src/Gaufrette/FilesystemMap.php | 4 +- src/Gaufrette/Stream/InMemoryBuffer.php | 9 +- src/Gaufrette/Stream/Local.php | 1 + src/Gaufrette/StreamWrapper.php | 10 +- src/Gaufrette/Util/Path.php | 5 +- .../Functional/Adapter/AwsS3Test.php | 3 +- .../Adapter/AzureBlobStorageTest.php | 4 +- .../Functional/Adapter/DoctrineDbalTest.php | 16 +- .../Functional/Adapter/DropboxTest.php | 2 - .../Gaufrette/Functional/Adapter/FtpTest.php | 8 +- .../Functional/Adapter/FunctionalTestCase.php | 6 +- .../Adapter/GoogleCloudStorageTest.php | 12 +- .../Functional/Adapter/GridFSTest.php | 12 +- .../Functional/Adapter/MogileFSTest.php | 2 - .../Functional/Adapter/PhpseclibSftpTest.php | 8 +- .../FileStream/FunctionalTestCase.php | 16 +- .../FileStream/InMemoryBufferTest.php | 2 +- .../Functional/FileStream/LocalTest.php | 4 +- 71 files changed, 754 insertions(+), 779 deletions(-) diff --git a/.php_cs.dist b/.php_cs.dist index 211cf146a..10089ee4a 100644 --- a/.php_cs.dist +++ b/.php_cs.dist @@ -1,49 +1,34 @@ exclude('node_modules') - ->exclude('vendor') - ->in(__DIR__) -; - return PhpCsFixer\Config::create() + ->setFinder(PhpCsFixer\Finder::create() + ->exclude('vendor') + ->in(__DIR__) + ) ->setRules([ + '@PSR1' => true, '@PSR2' => true, 'array_syntax' => [ 'syntax' => 'short' ], 'binary_operator_spaces' => [ 'align_equals' => false, 'align_double_arrow' => false ], + 'blank_line_before_statement' => true, 'cast_spaces' => true, 'combine_consecutive_unsets' => true, 'concat_space' => [ 'spacing' => 'one' ], 'linebreak_after_opening_tag' => true, + 'method_argument_space' => ['ensure_fully_multiline' => false], 'no_blank_lines_after_class_opening' => true, 'no_blank_lines_after_phpdoc' => true, + 'no_break_comment' => false, 'no_extra_consecutive_blank_lines' => true, - 'no_trailing_comma_in_singleline_array' => true, - 'no_whitespace_in_blank_line' => true, 'no_spaces_around_offset' => true, + 'no_trailing_comma_in_singleline_array' => true, 'no_unused_imports' => true, 'no_useless_else' => true, 'no_useless_return' => true, 'no_whitespace_before_comma_in_array' => true, + 'no_whitespace_in_blank_line' => true, 'normalize_index_brace' => true, + 'PedroTroller/phpspec' => true, 'phpdoc_indent' => true, 'phpdoc_to_comment' => true, 'phpdoc_trim' => true, @@ -51,9 +36,6 @@ return PhpCsFixer\Config::create() 'ternary_to_null_coalescing' => true, 'trailing_comma_in_multiline_array' => true, 'trim_array_spaces' => true, - 'method_argument_space' => ['ensure_fully_multiline' => false], - 'no_break_comment' => false, - 'blank_line_before_statement' => true, ]) - ->setFinder($finder) + ->registerCustomFixers(new PedroTroller\CS\Fixer\Fixers()) ; diff --git a/.travis.yml b/.travis.yml index 4e6ca082c..1562ca093 100644 --- a/.travis.yml +++ b/.travis.yml @@ -31,7 +31,6 @@ matrix: - php: '7.3' before_install: - - make php-cs-compare - echo "extension = mongodb.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; - phpenv config-rm xdebug.ini || echo "xdebug not available" - docker run -p ${SFTP_PORT}:22 -d atmoz/sftp:alpine ${SFTP_USER}:${SFTP_PASSWORD}:::${SFTP_BASE_DIR} @@ -42,5 +41,6 @@ install: - composer update --prefer-dist --no-progress --no-suggest --ansi script: + - php vendor/bin/php-cs-fixer fix --diff --dry-run --show-progress=none --verbose - php vendor/bin/phpspec run -fpretty --verbose - php vendor/bin/phpunit --verbose diff --git a/Makefile b/Makefile index dcc3b9e38..7b7a00852 100644 --- a/Makefile +++ b/Makefile @@ -22,18 +22,12 @@ clear-deps: .PHONY: php-cs-compare php-cs-compare: - docker run --rm -u $$(id -u):$$(id -g) -v $$(pwd):/project \ - herloct/php-cs-fixer fix \ - --config=/project/.php_cs.dist \ + docker/run-task php${PHP_VERSION} vendor/bin/php-cs-fixer fix \ --diff \ --dry-run \ --show-progress=none \ - --verbose \ - src spec tests + --verbose .PHONY: php-cs-fix php-cs-fix: - docker run --rm -u $$(id -u):$$(id -g) -v $$(pwd):/project \ - herloct/php-cs-fixer fix \ - --config=/project/.php_cs.dist \ - src spec tests + docker/run-task php${PHP_VERSION} vendor/bin/php-cs-fixer fix diff --git a/composer.json b/composer.json index 964a73140..d1d2488aa 100644 --- a/composer.json +++ b/composer.json @@ -35,7 +35,8 @@ "league/flysystem": "~1.0", "mongodb/mongodb": "^1.1", "microsoft/azure-storage-blob": "^1.0", - "akeneo/phpspec-skip-example-extension": "^4.0" + "akeneo/phpspec-skip-example-extension": "^4.0", + "pedrotroller/php-cs-custom-fixer": "^2.17" }, "suggest": { "knplabs/knp-gaufrette-bundle": "to use with Symfony2", diff --git a/spec/Gaufrette/Adapter/AclAwareAmazonS3Spec.php b/spec/Gaufrette/Adapter/AclAwareAmazonS3Spec.php index febb83553..8bd124ea2 100644 --- a/spec/Gaufrette/Adapter/AclAwareAmazonS3Spec.php +++ b/spec/Gaufrette/Adapter/AclAwareAmazonS3Spec.php @@ -48,7 +48,7 @@ function it_delegates_rename_and_update_acl(Adapter $adapter, AmazonS3 $service) $service ->set_object_acl('bucketName', 'filename2', \AmazonS3::ACL_PRIVATE) ->shouldBeCalled() - ->willReturn(new \CFResponse(array(), '', 200)) + ->willReturn(new \CFResponse([], '', 200)) ; $adapter ->rename('filename', 'filename2') @@ -72,7 +72,7 @@ function it_does_not_rename_when_cannot_update_acl(Adapter $adapter, AmazonS3 $s $service ->set_object_acl('bucketName', 'filename2', \AmazonS3::ACL_PRIVATE) ->shouldBeCalled() - ->willReturn(new \CFResponse(array(), '', 500)); + ->willReturn(new \CFResponse([], '', 500)); $adapter ->rename('filename', 'filename2') ->shouldBeCalled() @@ -91,16 +91,16 @@ function it_does_not_rename_when_cannot_update_acl(Adapter $adapter, AmazonS3 $s function it_updates_acl_with_users_array_when_rename(Adapter $adapter, AmazonS3 $service) { $service - ->set_object_acl('bucketName', 'filename2', array(array('id' => 'someId', 'permission' => \AmazonS3::GRANT_READ))) + ->set_object_acl('bucketName', 'filename2', [['id' => 'someId', 'permission' => \AmazonS3::GRANT_READ]]) ->shouldBeCalled() - ->willReturn(new \CFResponse(array(), '', 200)) + ->willReturn(new \CFResponse([], '', 200)) ; $adapter ->rename('filename', 'filename2') ->willReturn(true) ; - $this->setUsers(array(array('id' => 'someId', 'permission' => 'read'))); + $this->setUsers([['id' => 'someId', 'permission' => 'read']]); $this->rename('filename', 'filename2')->shouldReturn(true); } @@ -113,7 +113,7 @@ function it_delegates_write_and_update_acl(Adapter $adapter, AmazonS3 $service) $service ->set_object_acl('bucketName', 'filename', \AmazonS3::ACL_PRIVATE) ->shouldBeCalled() - ->willReturn(new \CFResponse(array(), '', 200)) + ->willReturn(new \CFResponse([], '', 200)) ; $adapter ->write('filename', 'some content') @@ -137,7 +137,7 @@ function it_does_not_write_when_cannot_update_acl(Adapter $adapter, AmazonS3 $se $service ->set_object_acl('bucketName', 'filename', \AmazonS3::ACL_PRIVATE) ->shouldBeCalled() - ->willReturn(new \CFResponse(array(), '', 500)) + ->willReturn(new \CFResponse([], '', 500)) ; $adapter ->write('filename', 'some content') @@ -159,16 +159,16 @@ function it_does_not_write_when_cannot_update_acl(Adapter $adapter, AmazonS3 $se function it_updates_acl_with_users_array_when_write(Adapter $adapter, AmazonS3 $service) { $service - ->set_object_acl('bucketName', 'filename', array(array('id' => 'someId', 'permission' => \AmazonS3::GRANT_READ))) + ->set_object_acl('bucketName', 'filename', [['id' => 'someId', 'permission' => \AmazonS3::GRANT_READ]]) ->shouldBeCalled() - ->willReturn(new \CFResponse(array(), '', 200)) + ->willReturn(new \CFResponse([], '', 200)) ; $adapter ->write('filename', 'some content') ->willReturn(12) ; - $this->setUsers(array(array('id' => 'someId', 'permission' => 'read'))); + $this->setUsers([['id' => 'someId', 'permission' => 'read']]); $this->write('filename', 'some content')->shouldReturn(12); } @@ -213,9 +213,9 @@ function it_delegates_directory_check(Adapter $adapter) */ function it_delegates_keys(Adapter $adapter) { - $adapter->keys()->willReturn(array('filename', 'filename2')); + $adapter->keys()->willReturn(['filename', 'filename2']); - $this->keys()->shouldReturn(array('filename', 'filename2')); + $this->keys()->shouldReturn(['filename', 'filename2']); } /** @@ -226,11 +226,11 @@ function it_delegates_metadata_handling(TestDelegateAdapter $extendedAdapter, Am { $this->beConstructedWith($extendedAdapter, $service, 'bucketName'); - $extendedAdapter->setMetadata('filename', array('some'))->shouldBeCalled(); - $extendedAdapter->getMetadata('filename')->shouldBeCalled()->willReturn(array('some2')); + $extendedAdapter->setMetadata('filename', ['some'])->shouldBeCalled(); + $extendedAdapter->getMetadata('filename')->shouldBeCalled()->willReturn(['some2']); - $this->setMetadata('filename', array('some')); - $this->getMetadata('filename')->shouldReturn(array('some2')); + $this->setMetadata('filename', ['some']); + $this->getMetadata('filename')->shouldReturn(['some2']); } } diff --git a/spec/Gaufrette/Adapter/AmazonS3Spec.php b/spec/Gaufrette/Adapter/AmazonS3Spec.php index 037775a68..494ad0521 100644 --- a/spec/Gaufrette/Adapter/AmazonS3Spec.php +++ b/spec/Gaufrette/Adapter/AmazonS3Spec.php @@ -31,12 +31,12 @@ function it_supports_metadata() */ function it_reads_file(AmazonS3 $service) { - $options = array( + $options = [ 'range' => 12, - 'response' => array( - 'content-language' => 'pl-pl' - ) - ); + 'response' => [ + 'content-language' => 'pl-pl', + ], + ]; $service ->if_bucket_exists('bucketName') @@ -71,7 +71,7 @@ function it_returns_false_when_cannot_read(AmazonS3 $service) ->get_object( 'bucketName', 'filename', - array() + [] ) ->shouldBeCalled() ->willReturn(new \CFResponse('header', 'some content', 500)) @@ -94,7 +94,7 @@ function it_is_verbose_and_throws_exceptions_when_read(AmazonS3 $service) ->get_object( 'bucketName', 'filename', - array() + [] ) ->willThrow(new \RuntimeException('read')) ; @@ -114,15 +114,15 @@ function it_rename_file(AmazonS3 $service) ; $service ->copy_object( - array( - 'bucket' => 'bucketName', + [ + 'bucket' => 'bucketName', 'filename' => 'filename1', - ), - array( - 'bucket' => 'bucketName', - 'filename' => 'filename2' - ), - array('acl' => \AmazonS3::ACL_OWNER_READ) + ], + [ + 'bucket' => 'bucketName', + 'filename' => 'filename2', + ], + ['acl' => \AmazonS3::ACL_OWNER_READ] ) ->shouldBeCalled() ->willReturn(new \CFResponse('header', 'some content', 200)) @@ -134,10 +134,10 @@ function it_rename_file(AmazonS3 $service) Argument::any() ) ->shouldBeCalled() - ->willReturn(new \CFResponse(array(), 'some', 200)) + ->willReturn(new \CFResponse([], 'some', 200)) ; - $this->setMetadata('filename1', array('acl' => \AmazonS3::ACL_OWNER_READ)); + $this->setMetadata('filename1', ['acl' => \AmazonS3::ACL_OWNER_READ]); $this->rename('filename1', 'filename2')->shouldReturn(true); } @@ -156,7 +156,7 @@ function it_is_verbose_and_throws_exceptions_when_rename(AmazonS3 $service) ->willThrow(new \RuntimeException('rename')) ; - $this->shouldThrow(new \RuntimeException('rename'))->duringRename('filename', 'filename1'); + $this->shouldThrow(new \RuntimeException('rename'))->duringRename('filename', 'filename1'); } /** @@ -171,15 +171,15 @@ function it_returns_false_when_cannot_rename(AmazonS3 $service) ; $service ->copy_object( - array( - 'bucket' => 'bucketName', + [ + 'bucket' => 'bucketName', 'filename' => 'filename1', - ), - array( - 'bucket' => 'bucketName', - 'filename' => 'filename2' - ), - array() + ], + [ + 'bucket' => 'bucketName', + 'filename' => 'filename2', + ], + [] ) ->shouldBeCalled() ->willReturn(new \CFResponse('header', 'some content', 500)) @@ -202,16 +202,16 @@ function it_should_write_file(AmazonS3 $service) ->create_object( 'bucketName', 'filename', - array( + [ 'acl' => \AmazonS3::ACL_PRIVATE, - 'body' => 'some content' - ) + 'body' => 'some content', + ] ) ->shouldBeCalled() - ->willReturn(new \CFResponse(array('x-aws-requestheaders' => array('Content-Length' => 12)), 'some content', 200)) + ->willReturn(new \CFResponse(['x-aws-requestheaders' => ['Content-Length' => 12]], 'some content', 200)) ; - $this->setMetadata('filename', array('acl' => \AmazonS3::ACL_PRIVATE, 'body' => 'other content')); + $this->setMetadata('filename', ['acl' => \AmazonS3::ACL_PRIVATE, 'body' => 'other content']); $this->write('filename', 'some content')->shouldReturn(12); } @@ -229,13 +229,13 @@ function it_returns_false_when_cannot_write(AmazonS3 $service) ->create_object( 'bucketName', 'filename', - array( + [ 'acl' => \AmazonS3::ACL_PUBLIC, - 'body' => 'some content' - ) + 'body' => 'some content', + ] ) ->shouldBeCalled() - ->willReturn(new \CFResponse(array('x-aws-requestheaders' => array('Content-Length' => 12)), 'some content', 500)) + ->willReturn(new \CFResponse(['x-aws-requestheaders' => ['Content-Length' => 12]], 'some content', 500)) ; $this->write('filename', 'some content')->shouldReturn(false); @@ -256,7 +256,7 @@ function it_is_verbose_and_throws_exceptions_when_write(AmazonS3 $service) ->willThrow(new \RuntimeException('write')) ; - $this->shouldThrow(new \RuntimeException('write'))->duringWrite('filename', 'some content'); + $this->shouldThrow(new \RuntimeException('write'))->duringWrite('filename', 'some content'); } /** @@ -292,7 +292,7 @@ function it_is_verbose_and_throws_exceptions_when_file_exists(AmazonS3 $service) ->willThrow(new \RuntimeException('exists')) ; - $this->shouldThrow(new \RuntimeException('exists'))->duringExists('filename'); + $this->shouldThrow(new \RuntimeException('exists'))->duringExists('filename'); } /** @@ -300,7 +300,7 @@ function it_is_verbose_and_throws_exceptions_when_file_exists(AmazonS3 $service) */ function it_should_get_file_mtime(AmazonS3 $service) { - $metadata = array('acl' => \AmazonS3::ACL_PUBLIC); + $metadata = ['acl' => \AmazonS3::ACL_PUBLIC]; $service ->if_bucket_exists('bucketName') ->shouldBeCalled() @@ -314,7 +314,7 @@ function it_should_get_file_mtime(AmazonS3 $service) $metadata ) ->shouldBeCalled() - ->willReturn(array('Headers' => array('last-modified' => '2012-01-01 23:10:10'))) + ->willReturn(['Headers' => ['last-modified' => '2012-01-01 23:10:10']]) ; $this->setMetadata('filename', $metadata); @@ -336,13 +336,13 @@ function it_returns_false_when_cannot_fetch_mtime(AmazonS3 $service) ->get_object_metadata( 'bucketName', 'filename', - array() + [] ) ->shouldBeCalled() - ->willReturn(array('Headers' => array())) + ->willReturn(['Headers' => []]) ; - $this->mtime('filename')->shouldReturn(false); + $this->mtime('filename')->shouldReturn(false); } /** @@ -360,7 +360,7 @@ function it_is_verbose_and_throws_exceptions_when_fetch_mtime(AmazonS3 $service) ->willThrow(new \RuntimeException('mtime')) ; - $this->shouldThrow(new \RuntimeException('mtime'))->duringMtime('filename'); + $this->shouldThrow(new \RuntimeException('mtime'))->duringMtime('filename'); } /** @@ -368,7 +368,7 @@ function it_is_verbose_and_throws_exceptions_when_fetch_mtime(AmazonS3 $service) */ function it_should_delete_file(AmazonS3 $service) { - $metadata = array('acl' => \AmazonS3::ACL_PRIVATE); + $metadata = ['acl' => \AmazonS3::ACL_PRIVATE]; $service ->if_bucket_exists('bucketName') @@ -381,7 +381,7 @@ function it_should_delete_file(AmazonS3 $service) 'filename', $metadata ) - ->willReturn(new \CFResponse(array(), 'some', 200)) + ->willReturn(new \CFResponse([], 'some', 200)) ; $this->setMetadata('filename', $metadata); @@ -406,7 +406,7 @@ function it_is_verbose_and_throws_exceptions_when_fetch_delete(AmazonS3 $service ->willThrow(new \RuntimeException('delete')) ; - $this->shouldThrow(new \RuntimeException('delete'))->duringDelete('filename'); + $this->shouldThrow(new \RuntimeException('delete'))->duringDelete('filename'); } /** @@ -423,9 +423,9 @@ function it_returns_false_when_cannot_delete(AmazonS3 $service) ->delete_object( 'bucketName', 'filename', - array() + [] ) - ->willReturn(new \CFResponse(array(), 'some', 500)) + ->willReturn(new \CFResponse([], 'some', 500)) ; $this->delete('filename')->shouldReturn(false); @@ -444,10 +444,10 @@ function it_should_get_keys(AmazonS3 $service) $service ->get_object_list('bucketName') ->shouldBeCalled() - ->willReturn(array('filename2', 'aaa/filename', 'filename1')) + ->willReturn(['filename2', 'aaa/filename', 'filename1']) ; - $this->keys()->shouldReturn(array('aaa', 'aaa/filename', 'filename1', 'filename2')); + $this->keys()->shouldReturn(['aaa', 'aaa/filename', 'filename1', 'filename2']); } /** @@ -464,7 +464,7 @@ function it_is_verbose_and_throws_exceptions_when_fetch_keys(AmazonS3 $service) ->willThrow(new \RuntimeException('keys')) ; - $this->shouldThrow(new \RuntimeException('keys'))->duringKeys(); + $this->shouldThrow(new \RuntimeException('keys'))->duringKeys(); } /** @@ -543,18 +543,18 @@ function it_creates_bucket_if_create_mode_is_enabled(AmazonS3 $service) ->if_bucket_exists('bucketName') ->willReturn(false) ; - $service->hostname = \AmazonS3::REGION_US_E1; + $service->hostname = \AmazonS3::REGION_US_E1; $service ->create_bucket('bucketName', \AmazonS3::REGION_US_E1) ->shouldBeCalled() - ->willReturn(new \CFResponse(array(), 'created', 201)) + ->willReturn(new \CFResponse([], 'created', 201)) ; $service ->if_object_exists('bucketName', 'filename') ->willReturn(false) ; - $this->beConstructedWith($service, 'bucketName', array('create' => true)); + $this->beConstructedWith($service, 'bucketName', ['create' => true]); $this->exists('filename'); } @@ -570,10 +570,10 @@ function it_fails_when_cannot_create_bucket(AmazonS3 $service) $service ->create_bucket('bucketName', Argument::any()) ->shouldBeCalled() - ->willReturn(new \CFResponse(array(), 'created', 500)) + ->willReturn(new \CFResponse([], 'created', 500)) ; - $this->beConstructedWith($service, 'bucketName', array('create' => true)); + $this->beConstructedWith($service, 'bucketName', ['create' => true]); $this ->shouldThrow(new \RuntimeException('Failed to create the configured bucket "bucketName".')) ->duringExists('filename') @@ -598,7 +598,7 @@ function it_allows_to_configure_reqion(AmazonS3 $service) ->willReturn(true) ; - $this->beConstructedWith($service, 'bucketName', array('region' => \AmazonS3::REGION_EU_W1)); + $this->beConstructedWith($service, 'bucketName', ['region' => \AmazonS3::REGION_EU_W1]); $this->exists('filename'); } @@ -615,14 +615,14 @@ function it_allows_to_configure_region_for_bucket(AmazonS3 $service) $service ->create_bucket('bucketName', \AmazonS3::REGION_EU_W1) ->shouldBeCalled() - ->willReturn(new \CFResponse(array(), 'created', 201)) + ->willReturn(new \CFResponse([], 'created', 201)) ; $service ->if_object_exists('bucketName', 'filename') ->willReturn(false) ; - $this->beConstructedWith($service, 'bucketName', array('create' => true, 'region' => \AmazonS3::REGION_EU_W1)); + $this->beConstructedWith($service, 'bucketName', ['create' => true, 'region' => \AmazonS3::REGION_EU_W1]); $this->exists('filename'); } @@ -641,13 +641,13 @@ function it_allows_to_configure_acl(AmazonS3 $service) ->create_object( 'bucketName', 'filename', - array( + [ 'acl' => '123abc', - 'body' => 'some content' - ) + 'body' => 'some content', + ] ) ->shouldBeCalled() - ->willReturn(new \CFResponse(array('x-aws-requestheaders' => array('Content-Length' => 12)), 'some content', 200)) + ->willReturn(new \CFResponse(['x-aws-requestheaders' => ['Content-Length' => 12]], 'some content', 200)) ; $this->write('filename', 'some content')->shouldReturn(12); @@ -669,16 +669,16 @@ function its_file_metadata_acl_are_more_important_than_global_acl_config(AmazonS ->create_object( 'bucketName', 'filename', - array( + [ 'acl' => 'more important acl', - 'body' => 'some content' - ) + 'body' => 'some content', + ] ) ->shouldBeCalled() - ->willReturn(new \CFResponse(array('x-aws-requestheaders' => array('Content-Length' => 12)), 'some content', 200)) + ->willReturn(new \CFResponse(['x-aws-requestheaders' => ['Content-Length' => 12]], 'some content', 200)) ; - $this->setMetadata('filename', array('acl' => 'more important acl')); + $this->setMetadata('filename', ['acl' => 'more important acl']); $this->write('filename', 'some content')->shouldReturn(12); } } diff --git a/spec/Gaufrette/Adapter/ApcSpec.php b/spec/Gaufrette/Adapter/ApcSpec.php index 6a3ae43cc..1d01f6670 100644 --- a/spec/Gaufrette/Adapter/ApcSpec.php +++ b/spec/Gaufrette/Adapter/ApcSpec.php @@ -17,7 +17,7 @@ function let() $this->beConstructedWith('prefix-apc-test/'); } - function letgo() + function letGo() { global $extensionLoaded; $extensionLoaded = null; diff --git a/spec/Gaufrette/Adapter/AzureBlobStorage.php b/spec/Gaufrette/Adapter/AzureBlobStorage.php index 9ff9065dc..3d1436fef 100644 --- a/spec/Gaufrette/Adapter/AzureBlobStorage.php +++ b/spec/Gaufrette/Adapter/AzureBlobStorage.php @@ -2,7 +2,7 @@ namespace spec\Gaufrette\Adapter; -use PHPSpec2\ObjectBehavior; +use PhpSpec\ObjectBehavior; use WindowsAzure\Blob\Models\Blob; use WindowsAzure\Common\ServiceException; @@ -12,12 +12,12 @@ class AzureBlobStorage extends ObjectBehavior /** * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory */ - public function let($blobProxyFactory) + function let($blobProxyFactory) { $this->beConstructedWith($blobProxyFactory, 'containerName'); } - public function it_should_be_initializable() + function it_should_be_initializable() { $this->shouldHaveType('Gaufrette\Adapter\AzureBlobStorage'); $this->shouldHaveType('Gaufrette\Adapter'); @@ -29,13 +29,13 @@ public function it_should_be_initializable() * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy * @param \WindowsAzure\Blob\Models\GetBlobResult $getBlobResult */ - public function it_should_read_file($blobProxyFactory, $blobProxy, $getBlobResult) + function it_should_read_file($blobProxyFactory, $blobProxy, $getBlobResult) { $getBlobResult ->getContentStream() ->shouldBeCalled() //azure blob content is handled as stream so we need to fake it - ->willReturn(fopen('data://text/plain,some content','r')); + ->willReturn(fopen('data://text/plain,some content', 'r')); $blobProxy ->getBlob('containerName', 'filename') @@ -54,7 +54,7 @@ public function it_should_read_file($blobProxyFactory, $blobProxy, $getBlobResul * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy */ - public function it_should_return_false_when_cannot_read($blobProxyFactory, $blobProxy) + function it_should_return_false_when_cannot_read($blobProxyFactory, $blobProxy) { $blobProxy ->getBlob('containerName', 'filename') @@ -73,7 +73,7 @@ public function it_should_return_false_when_cannot_read($blobProxyFactory, $blob * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy */ - public function it_should_not_mask_exception_when_read($blobProxyFactory, $blobProxy) + function it_should_not_mask_exception_when_read($blobProxyFactory, $blobProxy) { $blobProxy ->getBlob('containerName', 'filename') @@ -92,7 +92,7 @@ public function it_should_not_mask_exception_when_read($blobProxyFactory, $blobP * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy */ - public function it_should_rename_file($blobProxyFactory, $blobProxy) + function it_should_rename_file($blobProxyFactory, $blobProxy) { $blobProxy ->copyBlob('containerName', 'filename2', 'containerName', 'filename1') @@ -114,7 +114,7 @@ public function it_should_rename_file($blobProxyFactory, $blobProxy) * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy */ - public function it_should_return_false_when_cannot_rename($blobProxyFactory, $blobProxy) + function it_should_return_false_when_cannot_rename($blobProxyFactory, $blobProxy) { $blobProxy ->copyBlob('containerName', 'filename2', 'containerName', 'filename1') @@ -133,7 +133,7 @@ public function it_should_return_false_when_cannot_rename($blobProxyFactory, $bl * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy */ - public function it_should_not_mask_exception_when_rename($blobProxyFactory, $blobProxy) + function it_should_not_mask_exception_when_rename($blobProxyFactory, $blobProxy) { $blobProxy ->copyBlob('containerName', 'filename2', 'containerName', 'filename1') @@ -152,7 +152,7 @@ public function it_should_not_mask_exception_when_rename($blobProxyFactory, $blo * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy */ - public function it_should_write_file($blobProxyFactory, $blobProxy) + function it_should_write_file($blobProxyFactory, $blobProxy) { $blobProxy ->createBlockBlob('containerName', 'filename', 'some content', @@ -171,7 +171,7 @@ public function it_should_write_file($blobProxyFactory, $blobProxy) * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy */ - public function it_should_return_false_when_cannot_write($blobProxyFactory, $blobProxy) + function it_should_return_false_when_cannot_write($blobProxyFactory, $blobProxy) { $blobProxy ->createBlockBlob('containerName', 'filename', 'some content', @@ -190,7 +190,7 @@ public function it_should_return_false_when_cannot_write($blobProxyFactory, $blo * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy */ - public function it_should_not_mask_exception_when_write($blobProxyFactory, $blobProxy) + function it_should_not_mask_exception_when_write($blobProxyFactory, $blobProxy) { $blobProxy ->createBlockBlob('containerName', 'filename', 'some content', @@ -210,7 +210,7 @@ public function it_should_not_mask_exception_when_write($blobProxyFactory, $blob * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy * @param \WindowsAzure\Blob\Models\GetBlobResult $getBlobResult */ - public function it_should_check_if_file_exists($blobProxyFactory, $blobProxy, $getBlobResult) + function it_should_check_if_file_exists($blobProxyFactory, $blobProxy, $getBlobResult) { $blobProxyFactory ->create() @@ -236,7 +236,7 @@ public function it_should_check_if_file_exists($blobProxyFactory, $blobProxy, $g * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy */ - public function it_should_not_mask_exception_when_check_if_file_exists($blobProxyFactory, $blobProxy) + function it_should_not_mask_exception_when_check_if_file_exists($blobProxyFactory, $blobProxy) { $blobProxyFactory ->create() @@ -257,7 +257,7 @@ public function it_should_not_mask_exception_when_check_if_file_exists($blobProx * @param \WindowsAzure\Blob\Models\GetBlobPropertiesResult $getBlobPropertiesResult * @param \WindowsAzure\Blob\Models\BlobProperties $blobProperties */ - public function it_should_get_file_mtime($blobProxyFactory, $blobProxy, $getBlobPropertiesResult, $blobProperties) + function it_should_get_file_mtime($blobProxyFactory, $blobProxy, $getBlobPropertiesResult, $blobProperties) { $blobProxyFactory ->create() @@ -286,7 +286,7 @@ public function it_should_get_file_mtime($blobProxyFactory, $blobProxy, $getBlob * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy */ - public function it_should_return_false_when_cannot_mtime($blobProxyFactory, $blobProxy) + function it_should_return_false_when_cannot_mtime($blobProxyFactory, $blobProxy) { $blobProxyFactory ->create() @@ -305,7 +305,7 @@ public function it_should_return_false_when_cannot_mtime($blobProxyFactory, $blo * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy */ - public function it_should_not_mask_exception_when_get_mtime($blobProxyFactory, $blobProxy) + function it_should_not_mask_exception_when_get_mtime($blobProxyFactory, $blobProxy) { $blobProxyFactory ->create() @@ -324,7 +324,7 @@ public function it_should_not_mask_exception_when_get_mtime($blobProxyFactory, $ * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy */ - public function it_should_delete_file($blobProxyFactory, $blobProxy) + function it_should_delete_file($blobProxyFactory, $blobProxy) { $blobProxyFactory ->create() @@ -342,7 +342,7 @@ public function it_should_delete_file($blobProxyFactory, $blobProxy) * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy */ - public function it_should_return_false_when_cannot_delete_file($blobProxyFactory, $blobProxy) + function it_should_return_false_when_cannot_delete_file($blobProxyFactory, $blobProxy) { $blobProxyFactory ->create() @@ -361,7 +361,7 @@ public function it_should_return_false_when_cannot_delete_file($blobProxyFactory * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy */ - public function it_should_not_mask_exception_when_delete($blobProxyFactory, $blobProxy) + function it_should_not_mask_exception_when_delete($blobProxyFactory, $blobProxy) { $blobProxyFactory ->create() @@ -381,10 +381,10 @@ public function it_should_not_mask_exception_when_delete($blobProxyFactory, $blo * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy * @param \WindowsAzure\Blob\Models\ListBlobsResult $listBlobResult */ - public function it_should_get_keys($blobProxyFactory, $blobProxy, $listBlobResult) + function it_should_get_keys($blobProxyFactory, $blobProxy, $listBlobResult) { - $fileNames = array('aaa', 'aaa/filename', 'filename1', 'filename2'); - $blobs = array(); + $fileNames = ['aaa', 'aaa/filename', 'filename1', 'filename2']; + $blobs = []; foreach ($fileNames as $fileName) { $blob = new Blob(); $blob->setName($fileName); @@ -406,14 +406,14 @@ public function it_should_get_keys($blobProxyFactory, $blobProxy, $listBlobResul ->shouldBeCalled() ->willReturn($blobs); - $this->keys()->shouldReturn(array('aaa', 'aaa/filename', 'filename1', 'filename2')); + $this->keys()->shouldReturn(['aaa', 'aaa/filename', 'filename1', 'filename2']); } /** * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy */ - public function it_should_not_mask_exception_when_get_keys($blobProxyFactory, $blobProxy) + function it_should_not_mask_exception_when_get_keys($blobProxyFactory, $blobProxy) { $blobProxyFactory ->create() @@ -432,7 +432,7 @@ public function it_should_not_mask_exception_when_get_keys($blobProxyFactory, $b * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy */ - public function it_should_handle_dirs($blobProxyFactory, $blobProxy) + function it_should_handle_dirs($blobProxyFactory, $blobProxy) { $blobProxyFactory ->create() @@ -458,7 +458,7 @@ public function it_should_handle_dirs($blobProxyFactory, $blobProxy) * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy */ - public function it_should_create_container($blobProxyFactory, $blobProxy) + function it_should_create_container($blobProxyFactory, $blobProxy) { $blobProxyFactory ->create() @@ -476,7 +476,7 @@ public function it_should_create_container($blobProxyFactory, $blobProxy) * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy */ - public function it_should_fail_when_cannot_create_container($blobProxyFactory, $blobProxy) + function it_should_fail_when_cannot_create_container($blobProxyFactory, $blobProxy) { $blobProxyFactory ->create() @@ -490,5 +490,4 @@ public function it_should_fail_when_cannot_create_container($blobProxyFactory, $ $this->shouldThrow(new \RuntimeException('Failed to create the configured container "containerName": 0 ().', null))->duringCreateContainer('containerName'); } - } diff --git a/spec/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php b/spec/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php index c5d8178f5..88b9f50c9 100644 --- a/spec/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php +++ b/spec/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php @@ -2,19 +2,19 @@ namespace spec\Gaufrette\Adapter\AzureBlobStorage; -use PHPSpec2\ObjectBehavior; +use PhpSpec\ObjectBehavior; class BlobProxyFactory extends ObjectBehavior { /** * @param string $connectionString */ - public function let($connectionString) + function let($connectionString) { $this->beConstructedWith($connectionString); } - public function it_should_be_initializable() + function it_should_be_initializable() { $this->shouldHaveType('Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactory'); $this->shouldHaveType('Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface'); diff --git a/spec/Gaufrette/Adapter/CacheSpec.php b/spec/Gaufrette/Adapter/CacheSpec.php index 1dc36146e..7a739377e 100644 --- a/spec/Gaufrette/Adapter/CacheSpec.php +++ b/spec/Gaufrette/Adapter/CacheSpec.php @@ -33,15 +33,14 @@ function it_supports_metadata() function it_handles_metadata_when_cached_adapters_supports_metadata( CacheTestExtendedAdapter $extendedSource, CacheTestExtendedAdapter $extendedCache - ) - { - $extendedSource->setMetadata('filename', array('metadata'))->shouldBeCalled(); - $extendedCache->setMetadata('filename', array('metadata'))->shouldBeCalled(); - $extendedSource->getMetadata('filename')->shouldBeCalled()->willReturn(array('someMetadata')); + ) { + $extendedSource->setMetadata('filename', ['metadata'])->shouldBeCalled(); + $extendedCache->setMetadata('filename', ['metadata'])->shouldBeCalled(); + $extendedSource->getMetadata('filename')->shouldBeCalled()->willReturn(['someMetadata']); $this->beConstructedWith($extendedSource, $extendedCache); - $this->setMetadata('filename', array('metadata')); - $this->getMetadata('filename')->shouldReturn(array('someMetadata')); + $this->setMetadata('filename', ['metadata']); + $this->getMetadata('filename')->shouldReturn(['someMetadata']); } /** @@ -146,9 +145,9 @@ function it_get_mtime_from_source(Adapter $source) */ function it_get_keys_from_source(Adapter $source) { - $source->keys()->willReturn(array('filename2', 'filename1', 'filename')); + $source->keys()->willReturn(['filename2', 'filename1', 'filename']); - $this->keys()->shouldReturn(array('filename', 'filename1', 'filename2')); + $this->keys()->shouldReturn(['filename', 'filename1', 'filename2']); } } @@ -156,5 +155,4 @@ interface CacheTestExtendedAdapter extends \Gaufrette\Adapter, \Gaufrette\Adapter\ChecksumCalculator, \Gaufrette\Adapter\MetadataSupporter { - } diff --git a/spec/Gaufrette/Adapter/DoctrineDbalSpec.php b/spec/Gaufrette/Adapter/DoctrineDbalSpec.php index 09d5a3096..69f4c35a9 100644 --- a/spec/Gaufrette/Adapter/DoctrineDbalSpec.php +++ b/spec/Gaufrette/Adapter/DoctrineDbalSpec.php @@ -42,15 +42,17 @@ function it_checks_if_file_exists(Connection $connection) { $connection ->quoteIdentifier(Argument::any()) - ->will(function ($argument) { return sprintf('"%s"', $argument[0]); }); + ->will(function ($argument) { + return sprintf('"%s"', $argument[0]); + }); $connection - ->fetchColumn('SELECT COUNT("key") FROM "someTableName" WHERE "key" = :key', array('key' => 'filename')) + ->fetchColumn('SELECT COUNT("key") FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) ->willReturn(12); $this->exists('filename')->shouldReturn(true); $connection - ->fetchColumn('SELECT COUNT("key") FROM "someTableName" WHERE "key" = :key', array('key' => 'filename')) + ->fetchColumn('SELECT COUNT("key") FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) ->willReturn(0); $this->exists('filename')->shouldReturn(false); } @@ -62,19 +64,21 @@ function it_writes_to_new_file(Connection $connection) { $connection ->quoteIdentifier(Argument::any()) - ->will(function ($argument) { return sprintf('"%s"', $argument[0]); }); + ->will(function ($argument) { + return sprintf('"%s"', $argument[0]); + }); $connection - ->fetchColumn('SELECT COUNT("key") FROM "someTableName" WHERE "key" = :key', array('key' => 'filename')) + ->fetchColumn('SELECT COUNT("key") FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) ->willReturn(false); $connection ->insert( 'someTableName', - array( - '"content"' => 'some content', - '"mtime"' => strtotime('2012-10-10 23:10:10'), + [ + '"content"' => 'some content', + '"mtime"' => strtotime('2012-10-10 23:10:10'), '"checksum"' => '9893532233caff98cd083a116b013c0b', - '"key"' => 'filename' - )) + '"key"' => 'filename', + ]) ->shouldBeCalled(); $this->write('filename', 'some content')->shouldReturn(12); @@ -87,21 +91,23 @@ function it_write_file(Connection $connection) { $connection ->quoteIdentifier(Argument::any()) - ->will(function ($argument) { return sprintf('"%s"', $argument[0]); }); + ->will(function ($argument) { + return sprintf('"%s"', $argument[0]); + }); $connection - ->fetchColumn('SELECT COUNT("key") FROM "someTableName" WHERE "key" = :key', array('key' => 'filename')) + ->fetchColumn('SELECT COUNT("key") FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) ->willReturn(true); $connection ->update( 'someTableName', - array( - '"content"' => 'some content', - '"mtime"' => strtotime('2012-10-10 23:10:10'), + [ + '"content"' => 'some content', + '"mtime"' => strtotime('2012-10-10 23:10:10'), '"checksum"' => '9893532233caff98cd083a116b013c0b', - ), - array( - '"key"' => 'filename' - )) + ], + [ + '"key"' => 'filename', + ]) ->shouldBeCalled(); $this->write('filename', 'some content')->shouldReturn(12); @@ -114,9 +120,11 @@ function it_reads_file(Connection $connection) { $connection ->quoteIdentifier(Argument::any()) - ->will(function ($argument) { return sprintf('"%s"', $argument[0]); }); + ->will(function ($argument) { + return sprintf('"%s"', $argument[0]); + }); $connection - ->fetchColumn('SELECT "content" FROM "someTableName" WHERE "key" = :key', array('key' => 'filename')) + ->fetchColumn('SELECT "content" FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) ->willReturn('some content'); $this->read('filename')->shouldReturn('some content'); @@ -129,9 +137,11 @@ function it_calculates_checksum(Connection $connection) { $connection ->quoteIdentifier(Argument::any()) - ->will(function ($argument) { return sprintf('"%s"', $argument[0]); }); + ->will(function ($argument) { + return sprintf('"%s"', $argument[0]); + }); $connection - ->fetchColumn('SELECT "checksum" FROM "someTableName" WHERE "key" = :key', array('key' => 'filename')) + ->fetchColumn('SELECT "checksum" FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) ->willReturn(1234); $this->checksum('filename')->shouldReturn(1234); @@ -144,9 +154,11 @@ function it_gets_mtime(Connection $connection) { $connection ->quoteIdentifier(Argument::any()) - ->will(function ($argument) { return sprintf('"%s"', $argument[0]); }); + ->will(function ($argument) { + return sprintf('"%s"', $argument[0]); + }); $connection - ->fetchColumn('SELECT "mtime" FROM "someTableName" WHERE "key" = :key', array('key' => 'filename')) + ->fetchColumn('SELECT "mtime" FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) ->willReturn(1234); $this->mtime('filename')->shouldReturn(1234); @@ -159,16 +171,18 @@ function it_renames_file(Connection $connection) { $connection ->quoteIdentifier(Argument::any()) - ->will(function ($argument) { return sprintf('"%s"', $argument[0]); }); + ->will(function ($argument) { + return sprintf('"%s"', $argument[0]); + }); $connection ->update( 'someTableName', - array( - '"key"' => 'newFile', - ), - array( - '"key"' => 'filename' - )) + [ + '"key"' => 'newFile', + ], + [ + '"key"' => 'filename', + ]) ->shouldBeCalled() ->willReturn(1); @@ -181,15 +195,17 @@ function it_renames_file(Connection $connection) */ function it_get_keys(Connection $connection, Statement $stmt) { - $stmt->fetchAll(\PDO::FETCH_COLUMN)->willReturn(array('filename', 'filename1', 'filename2')); + $stmt->fetchAll(\PDO::FETCH_COLUMN)->willReturn(['filename', 'filename1', 'filename2']); $connection ->quoteIdentifier(Argument::any()) - ->will(function ($argument) { return sprintf('"%s"', $argument[0]); }); + ->will(function ($argument) { + return sprintf('"%s"', $argument[0]); + }); $connection ->executeQuery('SELECT "key" FROM "someTableName"') ->willReturn($stmt); - $this->keys()->shouldReturn(array('filename', 'filename1', 'filename2')); + $this->keys()->shouldReturn(['filename', 'filename1', 'filename2']); } /** @@ -199,9 +215,11 @@ function it_deletes_file(Connection $connection) { $connection ->quoteIdentifier(Argument::any()) - ->will(function ($argument) { return sprintf('"%s"', $argument[0]); }); + ->will(function ($argument) { + return sprintf('"%s"', $argument[0]); + }); $connection - ->delete('someTableName', array('"key"' => 'filename')) + ->delete('someTableName', ['"key"' => 'filename']) ->shouldBeCalled() ->willReturn(1); diff --git a/spec/Gaufrette/Adapter/DropboxSpec.php b/spec/Gaufrette/Adapter/DropboxSpec.php index 788673445..76f355d09 100644 --- a/spec/Gaufrette/Adapter/DropboxSpec.php +++ b/spec/Gaufrette/Adapter/DropboxSpec.php @@ -60,20 +60,20 @@ function it_checks_if_file_exists(Dropbox_API $dropbox) { $dropbox ->getMetaData('filename', false) - ->willReturn(array( - "size" => "225.4KB", - "rev" => "35e97029684fe", - "thumb_exists" => false, - "bytes" => 230783, - "modified" => "Tue, 19 Jul 2011 21:55:38 +0000", - "client_mtime" => "Mon, 18 Jul 2011 18:04:35 +0000", - "path" => "/filename", - "is_dir" => false, - "icon" => "page_white_acrobat", - "root" => "dropbox", - "mime_type" => "application/pdf", - "revision" => 220823 - )); + ->willReturn([ + 'size' => '225.4KB', + 'rev' => '35e97029684fe', + 'thumb_exists' => false, + 'bytes' => 230783, + 'modified' => 'Tue, 19 Jul 2011 21:55:38 +0000', + 'client_mtime' => 'Mon, 18 Jul 2011 18:04:35 +0000', + 'path' => '/filename', + 'is_dir' => false, + 'icon' => 'page_white_acrobat', + 'root' => 'dropbox', + 'mime_type' => 'application/pdf', + 'revision' => 220823, + ]); $this->exists('filename')->shouldReturn(true); @@ -85,7 +85,7 @@ function it_checks_if_file_exists(Dropbox_API $dropbox) $dropbox ->getMetaData('filename', false) - ->willReturn(array("is_deleted" => true)); + ->willReturn(['is_deleted' => true]); $this->exists('filename')->shouldReturn(false); } @@ -109,14 +109,14 @@ function it_gets_keys(Dropbox_API $dropbox) { $dropbox ->getMetaData('/', true) - ->willReturn(array( - 'contents' => array( - array('path' => '/filename'), - array('path' => '/aaa/filename') - ) - )); - - $this->keys()->shouldReturn(array('aaa', 'aaa/filename', 'filename')); + ->willReturn([ + 'contents' => [ + ['path' => '/filename'], + ['path' => '/aaa/filename'], + ], + ]); + + $this->keys()->shouldReturn(['aaa', 'aaa/filename', 'filename']); } /** @@ -139,18 +139,18 @@ function it_checks_if_given_key_is_directory(Dropbox_API $dropbox) { $dropbox ->getMetaData('filename', false) - ->willReturn(array( - "is_dir" => true - )) + ->willReturn([ + 'is_dir' => true, + ]) ; $this->isDirectory('filename')->shouldReturn(true); $dropbox ->getMetaData('filename', false) - ->willReturn(array( - "is_dir" => false - )); + ->willReturn([ + 'is_dir' => false, + ]); $this->isDirectory('filename')->shouldReturn(false); } @@ -240,9 +240,9 @@ function it_fetches_mtime(Dropbox_API $dropbox) { $dropbox ->getMetaData('filename', false) - ->willReturn(array( - "modified" => "Tue, 19 Jul 2011 21:55:38 +0000", - )) + ->willReturn([ + 'modified' => 'Tue, 19 Jul 2011 21:55:38 +0000', + ]) ; $this->mtime('filename')->shouldReturn(1311112538); diff --git a/spec/Gaufrette/Adapter/FlysystemSpec.php b/spec/Gaufrette/Adapter/FlysystemSpec.php index 2fbfbba43..48f9934da 100644 --- a/spec/Gaufrette/Adapter/FlysystemSpec.php +++ b/spec/Gaufrette/Adapter/FlysystemSpec.php @@ -3,7 +3,6 @@ namespace spec\Gaufrette\Adapter; use PhpSpec\ObjectBehavior; -use Prophecy\Argument; use League\Flysystem\AdapterInterface; use League\Flysystem\Config; @@ -33,9 +32,9 @@ function it_reads_file(AdapterInterface $adapter) function it_writes_file(AdapterInterface $adapter, Config $config) { - $adapter->write('filename', 'Hello.', $config)->willReturn(array()); + $adapter->write('filename', 'Hello.', $config)->willReturn([]); - $this->write('filename', 'Hello.')->shouldReturn(array()); + $this->write('filename', 'Hello.')->shouldReturn([]); } function it_checks_if_file_exists(AdapterInterface $adapter) diff --git a/spec/Gaufrette/Adapter/FtpSpec.php b/spec/Gaufrette/Adapter/FtpSpec.php index d07f37bd3..5c5b4441f 100644 --- a/spec/Gaufrette/Adapter/FtpSpec.php +++ b/spec/Gaufrette/Adapter/FtpSpec.php @@ -87,49 +87,49 @@ function it_does_not_not_rename_file_when_target_file_is_invalid() function it_fetches_keys_without_directories_dots() { - $this->keys()->shouldReturn(array('filename', 'filename.exe', '.htaccess', 'aaa', 'aaa/filename')); + $this->keys()->shouldReturn(['filename', 'filename.exe', '.htaccess', 'aaa', 'aaa/filename']); } function it_fetches_keys_with_spaces_and_unicode_chars() { $this->beConstructedWith('/home/l3l2', 'localhost'); - $this->keys()->shouldReturn(array('Žľuťoučký kůň.pdf', 'a b c d -> žežulička', 'a b c d -> žežulička/do re mi.pdf')); + $this->keys()->shouldReturn(['Žľuťoučký kůň.pdf', 'a b c d -> žežulička', 'a b c d -> žežulička/do re mi.pdf']); } function it_fetches_keys_recursive() { $this->beConstructedWith('/home/l3l3', 'localhost'); - $this->keys()->shouldReturn(array('filename', 'filename.exe', '.htaccess', 'aaa', 'www', 'aaa/filename', 'www/filename', 'www/some', 'www/some/otherfilename')); + $this->keys()->shouldReturn(['filename', 'filename.exe', '.htaccess', 'aaa', 'www', 'aaa/filename', 'www/filename', 'www/some', 'www/some/otherfilename']); } function it_lists_keys() { - $this->listKeys()->shouldReturn(array( - 'keys' => array('filename', 'filename.exe', '.htaccess', 'aaa/filename'), - 'dirs' => array('aaa') - )); + $this->listKeys()->shouldReturn([ + 'keys' => ['filename', 'filename.exe', '.htaccess', 'aaa/filename'], + 'dirs' => ['aaa'], + ]); - $this->listKeys('file')->shouldReturn(array( - 'keys' => array('filename', 'filename.exe'), - 'dirs' => array() - )); + $this->listKeys('file')->shouldReturn([ + 'keys' => ['filename', 'filename.exe'], + 'dirs' => [], + ]); - $this->listKeys('name')->shouldReturn(array( - 'keys' => array(), - 'dirs' => array() - )); + $this->listKeys('name')->shouldReturn([ + 'keys' => [], + 'dirs' => [], + ]); - $this->listKeys('aaa')->shouldReturn(array( - 'keys' => array('aaa/filename'), - 'dirs' => array('aaa') - )); + $this->listKeys('aaa')->shouldReturn([ + 'keys' => ['aaa/filename'], + 'dirs' => ['aaa'], + ]); - $this->listKeys('aaa/')->shouldReturn(array( - 'keys' => array('aaa/filename'), - 'dirs' => array() - )); + $this->listKeys('aaa/')->shouldReturn([ + 'keys' => ['aaa/filename'], + 'dirs' => [], + ]); } function it_fetches_mtime() @@ -178,7 +178,7 @@ function it_fetches_keys_with_hidden_files() { $this->beConstructedWith('/home/l3l1', 'localhost'); - $this->keys()->shouldReturn(array('filename', '.htaccess')); + $this->keys()->shouldReturn(['filename', '.htaccess']); } function it_checks_if_hidden_file_exists() @@ -193,9 +193,9 @@ function it_creates_base_directory_without_warning() global $createdDirectory; $createdDirectory = ''; - $this->beConstructedWith('/home/l3l0/new', 'localhost', array('create' => true)); + $this->beConstructedWith('/home/l3l0/new', 'localhost', ['create' => true]); - $this->listDirectory()->shouldReturn(array('keys' => array(), 'dirs' => array())); + $this->listDirectory()->shouldReturn(['keys' => [], 'dirs' => []]); } function it_does_not_create_base_directory_and_should_throw_exception() @@ -203,16 +203,16 @@ function it_does_not_create_base_directory_and_should_throw_exception() global $createdDirectory; $createdDirectory = ''; - $this->beConstructedWith('/home/l3l0/new', 'localhost', array('create' => false)); + $this->beConstructedWith('/home/l3l0/new', 'localhost', ['create' => false]); - $this->shouldThrow(new \RuntimeException("The directory '/home/l3l0/new' does not exist."))->during('listDirectory', array()); + $this->shouldThrow(new \RuntimeException("The directory '/home/l3l0/new' does not exist."))->during('listDirectory', []); } function it_fetches_keys_for_windows() { $this->beConstructedWith('C:\Ftp', 'localhost'); - $this->keys()->shouldReturn(array('archive', 'file1.zip', 'file2.zip')); + $this->keys()->shouldReturn(['archive', 'file1.zip', 'file2.zip']); } function it_supports_sizecalculator() diff --git a/spec/Gaufrette/Adapter/GoogleCloudStorageSpec.php b/spec/Gaufrette/Adapter/GoogleCloudStorageSpec.php index bb109e13a..6fd449fa9 100644 --- a/spec/Gaufrette/Adapter/GoogleCloudStorageSpec.php +++ b/spec/Gaufrette/Adapter/GoogleCloudStorageSpec.php @@ -6,22 +6,22 @@ class GoogleCloudStorageSpec extends ObjectBehavior { - public function let(\Google_Service_Storage $service) + function let(\Google_Service_Storage $service) { $this->beConstructedWith($service, 'bucketName'); } - public function it_is_adapter() + function it_is_adapter() { $this->shouldHaveType('Gaufrette\Adapter'); } - public function it_supports_metadata() + function it_supports_metadata() { $this->shouldHaveType('Gaufrette\Adapter\MetadataSupporter'); } - public function it_is_list_keys_aware() + function it_is_list_keys_aware() { $this->shouldHaveType('Gaufrette\Adapter\ListKeysAware'); } diff --git a/spec/Gaufrette/Adapter/GridFSSpec.php b/spec/Gaufrette/Adapter/GridFSSpec.php index 1d5bb064f..2c9d864e0 100644 --- a/spec/Gaufrette/Adapter/GridFSSpec.php +++ b/spec/Gaufrette/Adapter/GridFSSpec.php @@ -7,7 +7,6 @@ use MongoDB\GridFS\Exception\FileNotFoundException; use MongoDB\Model\BSONDocument; use PhpSpec\ObjectBehavior; -use Prophecy\Argument; class GridFSSpec extends ObjectBehavior { diff --git a/spec/Gaufrette/Adapter/InMemorySpec.php b/spec/Gaufrette/Adapter/InMemorySpec.php index d895201a0..89728cb78 100644 --- a/spec/Gaufrette/Adapter/InMemorySpec.php +++ b/spec/Gaufrette/Adapter/InMemorySpec.php @@ -8,10 +8,10 @@ class InMemorySpec extends ObjectBehavior { function let() { - $this->beConstructedWith(array( - 'filename' => array('mtime' => 12345, 'content' => 'content'), - 'filename2' => 'other content' - )); + $this->beConstructedWith([ + 'filename' => ['mtime' => 12345, 'content' => 'content'], + 'filename2' => 'other content', + ]); } function it_is_adapter() @@ -41,9 +41,9 @@ function it_writes_file() function it_renames_file() { - $this->rename('filename', 'aaa/filename2')->shouldReturn(true); - $this->exists('filename')->shouldReturn(false); - $this->exists('aaa/filename2')->shouldReturn(true); + $this->rename('filename', 'aaa/filename2')->shouldReturn(true); + $this->exists('filename')->shouldReturn(false); + $this->exists('aaa/filename2')->shouldReturn(true); } function it_checks_if_file_exists() @@ -54,7 +54,7 @@ function it_checks_if_file_exists() function it_fetches_keys() { - $this->keys()->shouldReturn(array('filename', 'filename2')); + $this->keys()->shouldReturn(['filename', 'filename2']); } function it_fetches_mtime() diff --git a/spec/Gaufrette/Adapter/LazyOpenCloudSpec.php b/spec/Gaufrette/Adapter/LazyOpenCloudSpec.php index 31adde3c8..a31e43e0c 100644 --- a/spec/Gaufrette/Adapter/LazyOpenCloudSpec.php +++ b/spec/Gaufrette/Adapter/LazyOpenCloudSpec.php @@ -7,7 +7,6 @@ use OpenCloud\ObjectStore\Resource\Container; use OpenCloud\ObjectStore\Service; use PhpSpec\ObjectBehavior; -use Prophecy\Argument; /** * LazyOpenCloudSpec @@ -38,12 +37,11 @@ function it_initializes_object_store( ObjectStoreFactoryInterface $objectStoreFactory, Service $objectStore, Container $container - ) - { + ) { $objectStoreFactory->getObjectStore()->shouldBeCalled()->willReturn($objectStore); - $objectStore->getContainer("test-container-name")->shouldBeCalled()->willReturn($container); - $container->getObject("test-file-name")->willThrow(new ObjectNotFoundException()); + $objectStore->getContainer('test-container-name')->shouldBeCalled()->willReturn($container); + $container->getObject('test-file-name')->willThrow(new ObjectNotFoundException()); - $this->read("test-file-name"); + $this->read('test-file-name'); } } diff --git a/spec/Gaufrette/Adapter/LocalSpec.php b/spec/Gaufrette/Adapter/LocalSpec.php index 7f6a34c20..b76cce0a4 100644 --- a/spec/Gaufrette/Adapter/LocalSpec.php +++ b/spec/Gaufrette/Adapter/LocalSpec.php @@ -10,7 +10,7 @@ class LocalSpec extends ObjectBehavior function let() { vfsStream::setup('test'); - vfsStream::copyFromFileSystem(__DIR__.'/MockFilesystem'); + vfsStream::copyFromFileSystem(__DIR__ . '/MockFilesystem'); $this->beConstructedWith(vfsStream::url('test')); } @@ -62,7 +62,7 @@ function it_checks_if_file_exists() function it_fetches_keys() { - $expectedKeys = array('filename', 'dir', 'dir/file'); + $expectedKeys = ['filename', 'dir', 'dir/file']; sort($expectedKeys); $this->keys()->shouldReturn($expectedKeys); } diff --git a/spec/Gaufrette/Adapter/MogileFSSpec.php b/spec/Gaufrette/Adapter/MogileFSSpec.php index 09f8ce967..cf6471984 100644 --- a/spec/Gaufrette/Adapter/MogileFSSpec.php +++ b/spec/Gaufrette/Adapter/MogileFSSpec.php @@ -8,7 +8,7 @@ class MogileFSSpec extends ObjectBehavior { function let() { - $this->beConstructedWith('http://domain.com', array('localhost')); + $this->beConstructedWith('http://domain.com', ['localhost']); } function it_is_adapter() diff --git a/spec/Gaufrette/Adapter/OpenCloudSpec.php b/spec/Gaufrette/Adapter/OpenCloudSpec.php index 674501037..7d1738f2f 100644 --- a/spec/Gaufrette/Adapter/OpenCloudSpec.php +++ b/spec/Gaufrette/Adapter/OpenCloudSpec.php @@ -26,7 +26,7 @@ class OpenCloudSpec extends ObjectBehavior */ function let(Service $objectStore, Container $container) { - $objectStore->getContainer("test")->willReturn($container); + $objectStore->getContainer('test')->willReturn($container); $this->beConstructedWith($objectStore, 'test', false); } @@ -41,8 +41,8 @@ function it_is_adapter() */ function it_reads_file(Container $container, DataObject $object) { - $object->getContent()->willReturn("Hello World"); - $container->getObject("test")->willReturn($object); + $object->getContent()->willReturn('Hello World'); + $container->getObject('test')->willReturn($object); $this->read('test')->shouldReturn('Hello World'); } @@ -52,7 +52,7 @@ function it_reads_file(Container $container, DataObject $object) */ function it_reads_file_on_error_returns_false(Container $container) { - $container->getObject("test")->willThrow(new ObjectNotFoundException()); + $container->getObject('test')->willThrow(new ObjectNotFoundException()); $this->read('test')->shouldReturn(false); } @@ -63,7 +63,7 @@ function it_reads_file_on_error_returns_false(Container $container) */ function it_writes_file_returns_size(Container $container, DataObject $object) { - $testData = "Hello World!"; + $testData = 'Hello World!'; $testDataSize = strlen($testData); $object->getContentLength()->willReturn($testDataSize); @@ -77,7 +77,7 @@ function it_writes_file_returns_size(Container $container, DataObject $object) */ function it_writes_file_and_write_fails_returns_false(Container $container) { - $testData = "Hello World!"; + $testData = 'Hello World!'; $container->uploadObject('test', $testData)->willThrow(new CreateUpdateError()); @@ -112,7 +112,7 @@ function it_returns_false_if_key_does_not_exist(Container $container) function it_deletes_file_on_success_returns_true(Container $container, DataObject $object) { $object->delete()->willReturn(null); - $container->getObject("test")->willReturn($object); + $container->getObject('test')->willReturn($object); $this->delete('test')->shouldReturn(true); } @@ -124,7 +124,7 @@ function it_deletes_file_on_success_returns_true(Container $container, DataObjec function it_deletes_file_returns_false_on_failure(Container $container, DataObject $object) { $object->delete()->willThrow(new DeleteError()); - $container->getObject("test")->willReturn($object); + $container->getObject('test')->willReturn($object); $this->delete('test')->shouldReturn(false); } @@ -134,7 +134,7 @@ function it_deletes_file_returns_false_on_failure(Container $container, DataObje */ function it_deletes_file_if_file_does_not_exist_returns_false(Container $container) { - $container->getObject("test")->willThrow(new ObjectNotFoundException()); + $container->getObject('test')->willThrow(new ObjectNotFoundException()); $this->delete('test')->shouldReturn(false); } @@ -145,10 +145,10 @@ function it_deletes_file_if_file_does_not_exist_returns_false(Container $contain */ function it_returns_checksum_if_file_exists(Container $container, DataObject $object) { - $object->getEtag()->willReturn("test String"); - $container->getObject("test")->willReturn($object); + $object->getEtag()->willReturn('test String'); + $container->getObject('test')->willReturn($object); - $this->checksum('test')->shouldReturn("test String"); + $this->checksum('test')->shouldReturn('test String'); } /** @@ -156,7 +156,7 @@ function it_returns_checksum_if_file_exists(Container $container, DataObject $ob */ function it_returns_false_when_file_does_not_exist(Container $container) { - $container->getObject("test")->willThrow(new ObjectNotFoundException()); + $container->getObject('test')->willThrow(new ObjectNotFoundException()); $this->checksum('test')->shouldReturn(false); } @@ -170,14 +170,14 @@ function it_returns_false_when_file_does_not_exist(Container $container) */ function it_returns_files_as_sorted_array(Container $container, Collection $objectList, DataObject $object1, DataObject $object2, DataObject $object3) { - $outputArray = array('key1', 'key2', 'key5'); + $outputArray = ['key1', 'key2', 'key5']; $index = 0; $object1->getName()->willReturn('key5'); $object2->getName()->willReturn('key2'); $object3->getName()->willReturn('key1'); - $objects = array($object1, $object2, $object3); + $objects = [$object1, $object2, $object3]; $objectList->next()->will( function () use ($objects, &$index) { diff --git a/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php b/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php index 3ee392120..7f6703cdd 100644 --- a/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php +++ b/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php @@ -56,21 +56,21 @@ function it_fetches_keys(SFTP $sftp) ->willReturn(true); $sftp ->rawlist('/home/l3l0/') - ->willReturn(array( - 'filename' => array('type' => NET_SFTP_TYPE_REGULAR), - 'filename1' => array('type' => NET_SFTP_TYPE_REGULAR), - 'aaa' => array('type' => NET_SFTP_TYPE_DIRECTORY) - )); + ->willReturn([ + 'filename' => ['type' => NET_SFTP_TYPE_REGULAR], + 'filename1' => ['type' => NET_SFTP_TYPE_REGULAR], + 'aaa' => ['type' => NET_SFTP_TYPE_DIRECTORY], + ]); $sftp ->file_exists('/home/l3l0/aaa') ->willReturn(true); $sftp ->rawlist('/home/l3l0/aaa') - ->willReturn(array( - 'filename' => array('type' => NET_SFTP_TYPE_REGULAR), - )); + ->willReturn([ + 'filename' => ['type' => NET_SFTP_TYPE_REGULAR], + ]); - $this->keys()->shouldReturn(array('filename', 'filename1', 'aaa', 'aaa/filename')); + $this->keys()->shouldReturn(['filename', 'filename1', 'aaa', 'aaa/filename']); } /** @@ -118,9 +118,9 @@ function it_should_check_if_file_exists(SFTP $sftp) { $sftp->pwd()->willReturn('/home/l3l0'); $sftp->chdir('/home/l3l0')->willReturn(true); - $sftp->stat('/home/l3l0/filename')->willReturn(array( - 'name' => '/home/l3l0/filename' - )); + $sftp->stat('/home/l3l0/filename')->willReturn([ + 'name' => '/home/l3l0/filename', + ]); $sftp->stat('/home/l3l0/filename1')->willReturn(false); $this->exists('filename')->shouldReturn(true); @@ -147,10 +147,10 @@ function it_should_check_is_directory(SFTP $sftp) */ function it_should_create_file(SFTP $sftp, Filesystem $filesystem) { - $sftp->stat('/home/l3l0/filename')->willReturn(array( + $sftp->stat('/home/l3l0/filename')->willReturn([ 'name' => '/home/l3l0/filename', 'size' => '30', - )); + ]); $this->createFile('filename', $filesystem)->beAnInstanceOf('Gaufrette\File'); } diff --git a/spec/Gaufrette/Adapter/SafeLocalSpec.php b/spec/Gaufrette/Adapter/SafeLocalSpec.php index b938b9881..0badbe4c6 100644 --- a/spec/Gaufrette/Adapter/SafeLocalSpec.php +++ b/spec/Gaufrette/Adapter/SafeLocalSpec.php @@ -10,7 +10,7 @@ class SafeLocalSpec extends ObjectBehavior function let() { vfsStream::setup('test'); - vfsStream::copyFromFileSystem(__DIR__.'/MockFilesystem'); + vfsStream::copyFromFileSystem(__DIR__ . '/MockFilesystem'); $this->beConstructedWith(vfsStream::url('test')); } @@ -21,12 +21,12 @@ function it_is_local_adapter() function it_computes_path_using_base64() { - rename(vfsStream::url('test/filename'), vfsStream::url('test/'.base64_encode('filename'))); + rename(vfsStream::url('test/filename'), vfsStream::url('test/' . base64_encode('filename'))); $this->read('filename')->shouldReturn("content\n"); } function it_computes_key_back_using_base64() { - $this->keys()->shouldReturn(array(base64_decode('dir'), base64_decode('dir/file'), base64_decode('filename'))); + $this->keys()->shouldReturn([base64_decode('dir'), base64_decode('dir/file'), base64_decode('filename')]); } } diff --git a/spec/Gaufrette/Adapter/SftpSpec.php b/spec/Gaufrette/Adapter/SftpSpec.php index f5d62ce0a..cf115ddec 100644 --- a/spec/Gaufrette/Adapter/SftpSpec.php +++ b/spec/Gaufrette/Adapter/SftpSpec.php @@ -41,10 +41,10 @@ function it_fetches_keys($sftp) ; $sftp ->listDirectory('/home/l3l0', true) - ->willReturn(array('files' => array('/home/l3l0/filename', '/home/l3l0/filename1', '/home/l3l0/aaa/filename'))) + ->willReturn(['files' => ['/home/l3l0/filename', '/home/l3l0/filename1', '/home/l3l0/aaa/filename']]) ; - $this->keys()->shouldReturn(array('aaa', 'aaa/filename', 'filename', 'filename1')); + $this->keys()->shouldReturn(['aaa', 'aaa/filename', 'filename', 'filename1']); } /** diff --git a/spec/Gaufrette/Adapter/functions.php b/spec/Gaufrette/Adapter/functions.php index 4e3a464ec..d2c7e4ff4 100644 --- a/spec/Gaufrette/Adapter/functions.php +++ b/spec/Gaufrette/Adapter/functions.php @@ -49,14 +49,14 @@ function ftp_fget($connection, &$fileResource, $path, $mode) function ftp_chdir($connection, $dirname) { - if (in_array($dirname, array('/home/l3l0', '/home/l3l0/aaa', '/home/l3l0/relative', '/home/l3l0/relative/some', '/home/l3l1', '/home/l3l2', '/home/l3l2/a b c d -> žežulička', '/home/l3l3', 'C:\Ftp'))) { - return true; + if (in_array($dirname, ['/home/l3l0', '/home/l3l0/aaa', '/home/l3l0/relative', '/home/l3l0/relative/some', '/home/l3l1', '/home/l3l2', '/home/l3l2/a b c d -> žežulička', '/home/l3l3', 'C:\Ftp'])) { + return true; } global $createdDirectory; if ($createdDirectory && $createdDirectory === $dirname) { - return true; + return true; } trigger_error(sprintf('%s: No such file or directory', $dirname), E_USER_WARNING); @@ -66,7 +66,7 @@ function ftp_chdir($connection, $dirname) function ftp_mkdir($connection, $dirname) { - if (in_array($dirname, array('/home/l3l0/new'))) { + if (in_array($dirname, ['/home/l3l0/new'])) { global $createdDirectory; $createdDirectory = $dirname; @@ -93,124 +93,113 @@ function ftp_close($connection) function ftp_rawlist($connection, $directory, $recursive = false) { $arguments = explode(' ', $directory, 2); - if ('/home/l3l0' === end($arguments)) - { - return array( - "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .", - "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..", - "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 aaa", - "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename", - "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename.exe", - "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .htaccess", - "lrwxrwxrwx 1 vincent vincent 11 Jul 12 12:16 www -> aaa", - "lrwxrwxrwx 1 vincent vincent 11 Jul 12 12:16 vendor -> bbb", - ); + if ('/home/l3l0' === end($arguments)) { + return [ + 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .', + 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..', + 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 aaa', + '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename', + '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename.exe', + '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .htaccess', + 'lrwxrwxrwx 1 vincent vincent 11 Jul 12 12:16 www -> aaa', + 'lrwxrwxrwx 1 vincent vincent 11 Jul 12 12:16 vendor -> bbb', + ]; } - if ('/home/l3l0/aaa' === end($arguments)) - { - return array( - "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename", - ); + if ('/home/l3l0/aaa' === end($arguments)) { + return [ + '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename', + ]; } - if ('/home/l3l0/relative' === end($arguments)) - { - return array( - "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename", - "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 some", - ); + if ('/home/l3l0/relative' === end($arguments)) { + return [ + '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename', + '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 some', + ]; } - if ('/home/l3l0/relative/some' === end($arguments)) - { - return array( - "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 otherfilename", - ); + if ('/home/l3l0/relative/some' === end($arguments)) { + return [ + '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 otherfilename', + ]; } - if ('/home/l3l1' === end($arguments) && 0 === strpos(reset($arguments), '-al')) - { - return array( - "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .", - "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..", - "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename", - "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .htaccess", - ); + if ('/home/l3l1' === end($arguments) && 0 === strpos(reset($arguments), '-al')) { + return [ + 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .', + 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..', + '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename', + '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .htaccess', + ]; } - if ('/home/l3l1' === end($arguments) && false === strpos(reset($arguments), '-al')) - { - return array( - "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .", - "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..", - "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename", - ); + if ('/home/l3l1' === end($arguments) && false === strpos(reset($arguments), '-al')) { + return [ + 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .', + 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..', + '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename', + ]; } - if ('/home/l3l2' === end($arguments)) - { - return array( - "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .", - "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..", - "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 a b c d -> žežulička", - "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 Žľuťoučký kůň.pdf", - ); + if ('/home/l3l2' === end($arguments)) { + return [ + 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .', + 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..', + 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 a b c d -> žežulička', + '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 Žľuťoučký kůň.pdf', + ]; } - if ('/home/l3l2/a b c d -> žežulička' === end($arguments)) - { - return array( - "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .", - "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..", - "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 do re mi.pdf", - ); + if ('/home/l3l2/a b c d -> žežulička' === end($arguments)) { + return [ + 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .', + 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..', + '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 do re mi.pdf', + ]; } - if ('/home/l3l3' === end($arguments) && '-alR' === reset($arguments)) - { - return array( - "/home/l3l3:", - "total: 12", - "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .", - "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..", - "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 aaa", - "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename", - "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename.exe", - "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .htaccess", - "drwxrwxrwx 1 vincent vincent 11 Jul 12 12:16 www", - "lrwxrwxrwx 1 vincent vincent 11 Jul 12 12:16 vendor -> bbb", - "", - "/home/l3l3/aaa:", - "total: 8", - "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename", - "", - "/home/l3l3/www:", - "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .", - "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..", - "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename", - "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 some", - "", - "/home/l3l3/www/some:", - "total 5", - "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .", - "drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..", - "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 otherfilename", - ); + if ('/home/l3l3' === end($arguments) && '-alR' === reset($arguments)) { + return [ + '/home/l3l3:', + 'total: 12', + 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .', + 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..', + 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 aaa', + '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename', + '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename.exe', + '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .htaccess', + 'drwxrwxrwx 1 vincent vincent 11 Jul 12 12:16 www', + 'lrwxrwxrwx 1 vincent vincent 11 Jul 12 12:16 vendor -> bbb', + '', + '/home/l3l3/aaa:', + 'total: 8', + '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename', + '', + '/home/l3l3/www:', + 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .', + 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..', + '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename', + 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 some', + '', + '/home/l3l3/www/some:', + 'total 5', + 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .', + 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..', + '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 otherfilename', + ]; } - // https://github.com/KnpLabs/Gaufrette/issues/147 - if ('C:\Ftp' === end($arguments)) - { - return array( - "05-26-12 08:03PM archive", - "12-04-12 06:57PM 16142 file1.zip", - "12-05-12 04:01PM 16142 file2.zip", - ); + if ('C:\Ftp' === end($arguments)) { + return [ + '05-26-12 08:03PM archive', + '12-04-12 06:57PM 16142 file1.zip', + '12-05-12 04:01PM 16142 file2.zip', + ]; } - return array(); + return []; } function ftp_login($connection, $username, $password) @@ -231,7 +220,7 @@ function file_exists($path) { //fake it for ssh+ssl: protocol for SFTP testing, otherwise delegate to global if (strpos($path, 'ssh+ssl:') === 0) { - return in_array($path, array('/home/l3l0/filename', '/home/somedir/filename', 'ssh+ssl://localhost/home/l3l0/filename')) ? true : false; + return in_array($path, ['/home/l3l0/filename', '/home/somedir/filename', 'ssh+ssl://localhost/home/l3l0/filename']) ? true : false; } return \file_exists($path); diff --git a/spec/Gaufrette/FileSpec.php b/spec/Gaufrette/FileSpec.php index 9b3fc6bf3..f45608784 100644 --- a/spec/Gaufrette/FileSpec.php +++ b/spec/Gaufrette/FileSpec.php @@ -7,7 +7,8 @@ interface MetadataAdapter extends \Gaufrette\Adapter, \Gaufrette\Adapter\MetadataSupporter -{} +{ +} class FileSpec extends ObjectBehavior { @@ -55,7 +56,7 @@ function it_gets_mtime(Filesystem $filesystem) */ function it_pass_metadata_when_write_content(Filesystem $filesystem, MetadataAdapter $adapter) { - $metadata = array('id' => '123'); + $metadata = ['id' => '123']; $adapter->setMetadata('filename', $metadata)->shouldBeCalled(); $filesystem->write('filename', 'some content', true)->willReturn(12); $filesystem->getAdapter()->willReturn($adapter); @@ -69,7 +70,7 @@ function it_pass_metadata_when_write_content(Filesystem $filesystem, MetadataAda */ function it_pass_metadata_when_read_content(Filesystem $filesystem, MetadataAdapter $adapter) { - $metadata = array('id' => '123'); + $metadata = ['id' => '123']; $adapter->setMetadata('filename', $metadata)->shouldBeCalled(); $filesystem->read('filename')->willReturn('some content'); $filesystem->getAdapter()->willReturn($adapter); @@ -83,7 +84,7 @@ function it_pass_metadata_when_read_content(Filesystem $filesystem, MetadataAdap */ function it_pass_metadata_when_delete_content(Filesystem $filesystem, MetadataAdapter $adapter) { - $metadata = array('id' => '123'); + $metadata = ['id' => '123']; $adapter->setMetadata('filename', $metadata)->shouldBeCalled(); $filesystem->delete('filename')->willReturn(true); $filesystem->getAdapter()->willReturn($adapter); @@ -97,7 +98,7 @@ function it_pass_metadata_when_delete_content(Filesystem $filesystem, MetadataAd */ function it_sets_content_of_file(Filesystem $filesystem, MetadataAdapter $adapter) { - $adapter->setMetadata('filename', array())->shouldNotBeCalled(); + $adapter->setMetadata('filename', [])->shouldNotBeCalled(); $filesystem->getAdapter()->willReturn($adapter); $filesystem->write('filename', 'some content', true)->shouldBeCalled()->willReturn(21); diff --git a/spec/Gaufrette/FilesystemMapSpec.php b/spec/Gaufrette/FilesystemMapSpec.php index 9d3d56271..358b94241 100644 --- a/spec/Gaufrette/FilesystemMapSpec.php +++ b/spec/Gaufrette/FilesystemMapSpec.php @@ -69,6 +69,6 @@ function it_removes_all_filesystems(Filesystem $filesystem) $this->has('some')->shouldReturn(false); $this->has('other')->shouldReturn(false); - $this->all()->shouldReturn(array()); + $this->all()->shouldReturn([]); } } diff --git a/spec/Gaufrette/FilesystemSpec.php b/spec/Gaufrette/FilesystemSpec.php index 6b32fddf7..1c90f6c68 100644 --- a/spec/Gaufrette/FilesystemSpec.php +++ b/spec/Gaufrette/FilesystemSpec.php @@ -13,7 +13,8 @@ interface ExtendedAdapter extends \Gaufrette\Adapter, \Gaufrette\Adapter\ChecksumCalculator, \Gaufrette\Adapter\MetadataSupporter, \Gaufrette\Adapter\MimeTypeProvider -{} +{ +} class FilesystemSpec extends ObjectBehavior { @@ -280,7 +281,7 @@ function it_fails_when_delete_is_not_successful(Adapter $adapter) */ function it_should_get_all_keys(Adapter $adapter) { - $keys = array('filename', 'filename1', 'filename2'); + $keys = ['filename', 'filename1', 'filename2']; $adapter->keys()->willReturn($keys); $this->keys()->shouldReturn($keys); @@ -291,28 +292,28 @@ function it_should_get_all_keys(Adapter $adapter) */ function it_match_listed_keys_using_specified_pattern(Adapter $adapter) { - $keys = array('filename', 'filename1', 'filename2', 'testKey', 'KeyTest', 'testkey'); + $keys = ['filename', 'filename1', 'filename2', 'testKey', 'KeyTest', 'testkey']; $adapter->keys()->willReturn($keys); $adapter->isDirectory(Argument::any())->willReturn(false); $this->listKeys()->shouldReturn( - array( - 'keys' => array('filename', 'filename1', 'filename2', 'testKey', 'KeyTest', 'testkey'), - 'dirs' => array() - ) + [ + 'keys' => ['filename', 'filename1', 'filename2', 'testKey', 'KeyTest', 'testkey'], + 'dirs' => [], + ] ); $this->listKeys('filename')->shouldReturn( - array( - 'keys' => array('filename', 'filename1', 'filename2'), - 'dirs' => array() - ) + [ + 'keys' => ['filename', 'filename1', 'filename2'], + 'dirs' => [], + ] ); $this->listKeys('Key')->shouldReturn( - array( - 'keys' => array('KeyTest'), - 'dirs' => array() - ) + [ + 'keys' => ['KeyTest'], + 'dirs' => [], + ] ); } @@ -321,7 +322,7 @@ function it_match_listed_keys_using_specified_pattern(Adapter $adapter) */ function it_listing_directories_using_adapter_is_directory_method(Adapter $adapter) { - $keys = array('filename', 'filename1', 'filename2', 'testKey', 'KeyTest', 'testkey'); + $keys = ['filename', 'filename1', 'filename2', 'testKey', 'KeyTest', 'testkey']; $adapter->keys()->willReturn($keys); $adapter->isDirectory('filename')->willReturn(false); @@ -333,22 +334,22 @@ function it_listing_directories_using_adapter_is_directory_method(Adapter $adapt $adapter->isDirectory('testKey')->willReturn(true); $this->listKeys()->shouldReturn( - array( - 'keys' => array('filename', 'filename2', 'KeyTest', 'testkey'), - 'dirs' => array('filename1', 'testKey') - ) + [ + 'keys' => ['filename', 'filename2', 'KeyTest', 'testkey'], + 'dirs' => ['filename1', 'testKey'], + ] ); $this->listKeys('filename')->shouldReturn( - array( - 'keys' => array('filename', 'filename2'), - 'dirs' => array('filename1') - ) + [ + 'keys' => ['filename', 'filename2'], + 'dirs' => ['filename1'], + ] ); $this->listKeys('Key')->shouldReturn( - array( - 'keys' => array('KeyTest'), - 'dirs' => array() - ) + [ + 'keys' => ['KeyTest'], + 'dirs' => [], + ] ); } diff --git a/spec/Gaufrette/StreamWrapperSpec.php b/spec/Gaufrette/StreamWrapperSpec.php index 4661b9b97..00eafce40 100644 --- a/spec/Gaufrette/StreamWrapperSpec.php +++ b/spec/Gaufrette/StreamWrapperSpec.php @@ -200,21 +200,21 @@ function it_does_not_get_stat_when_is_not_open() */ function it_stats_file(Stream $stream) { - $stat = array( - 'dev' => 1, - 'ino' => 12, - 'mode' => 0777, + $stat = [ + 'dev' => 1, + 'ino' => 12, + 'mode' => 0777, 'nlink' => 0, - 'uid' => 123, - 'gid' => 1, - 'rdev' => 0, - 'size' => 666, + 'uid' => 123, + 'gid' => 1, + 'rdev' => 0, + 'size' => 666, 'atime' => 1348030800, 'mtime' => 1348030800, 'ctime' => 1348030800, 'blksize' => 5, - 'blocks' => 1, - ); + 'blocks' => 1, + ]; $stream->open(Argument::any())->willReturn(true); $stream->stat()->willReturn($stat); @@ -227,21 +227,21 @@ function it_stats_file(Stream $stream) */ function it_should_stat_from_url(Stream $stream) { - $stat = array( - 'dev' => 1, - 'ino' => 12, - 'mode' => 0777, + $stat = [ + 'dev' => 1, + 'ino' => 12, + 'mode' => 0777, 'nlink' => 0, - 'uid' => 123, - 'gid' => 1, - 'rdev' => 0, - 'size' => 666, + 'uid' => 123, + 'gid' => 1, + 'rdev' => 0, + 'size' => 666, 'atime' => 1348030800, 'mtime' => 1348030800, 'ctime' => 1348030800, 'blksize' => 5, - 'blocks' => 1, - ); + 'blocks' => 1, + ]; $stream->open(Argument::any())->willReturn(true); $stream->stat()->willReturn($stat); @@ -256,8 +256,8 @@ function it_stats_even_if_it_cannot_be_open(Filesystem $filesystem, Stream $stre { $filesystem->createStream('dir/')->willReturn($stream); $stream->open(Argument::any())->willThrow(new \RuntimeException); - $stream->stat(Argument::any())->willReturn(array('mode' => 16893)); - $this->url_stat('gaufrette://some/dir/', STREAM_URL_STAT_LINK)->shouldReturn(array('mode' => 16893)); + $stream->stat(Argument::any())->willReturn(['mode' => 16893]); + $this->url_stat('gaufrette://some/dir/', STREAM_URL_STAT_LINK)->shouldReturn(['mode' => 16893]); } /** diff --git a/spec/Gaufrette/Util/ChecksumSpec.php b/spec/Gaufrette/Util/ChecksumSpec.php index f61d35f8e..dc4546a13 100644 --- a/spec/Gaufrette/Util/ChecksumSpec.php +++ b/spec/Gaufrette/Util/ChecksumSpec.php @@ -2,30 +2,36 @@ namespace spec\Gaufrette\Util; -use PHPSpec\ObjectBehavior; +use PhpSpec\ObjectBehavior; class ChecksumSpec extends ObjectBehavior { function let() { - $path = __DIR__.DIRECTORY_SEPARATOR.'testFile'; - file_put_contents($path, 'some other content'); + file_put_contents($this->getTestFilePath(), 'some other content'); + } + + function letGo() + { + @unlink($this->getTestFilePath()); } function it_calculates_checksum_from_content() { - $this->fromContent('some content')->shouldReturn(md5('some content')); + $this->fromContent('some content') + ->shouldReturn(md5('some content')) + ; } function it_calculates_checksum_from_filepath() { - $path = __DIR__.DIRECTORY_SEPARATOR.'testFile'; - $this->fromFile($path)->shouldReturn(md5('some other content')); + $this->fromFile($this->getTestFilePath()) + ->shouldReturn(md5('some other content')) + ; } - function letgo() + private function getTestFilePath(): string { - $path = __DIR__.DIRECTORY_SEPARATOR.'testFile'; - @unlink(__DIR__.DIRECTORY_SEPARATOR.'testFile'); + return __DIR__ . DIRECTORY_SEPARATOR . 'testFile'; } } diff --git a/src/Gaufrette/Adapter/AclAwareAmazonS3.php b/src/Gaufrette/Adapter/AclAwareAmazonS3.php index f307922b1..6986276d8 100644 --- a/src/Gaufrette/Adapter/AclAwareAmazonS3.php +++ b/src/Gaufrette/Adapter/AclAwareAmazonS3.php @@ -5,7 +5,7 @@ use AmazonS3 as AmazonClient; use Gaufrette\Adapter; -@trigger_error('The '.__NAMESPACE__.'\AclAwareAmazonS3 adapter is deprecated since version 0.4 and will be removed in 1.0. Use the AwsS3 adapter instead.', E_USER_DEPRECATED); +@trigger_error('The ' . __NAMESPACE__ . '\AclAwareAmazonS3 adapter is deprecated since version 0.4 and will be removed in 1.0. Use the AwsS3 adapter instead.', E_USER_DEPRECATED); /** * Makes the AmazonS3 adapter ACL aware. Uses the AWS SDK for PHP v1.x. @@ -18,14 +18,13 @@ * * @deprecated The AclAwareAmazonS3 adapter is deprecated since version 0.4 and will be removed in 1.0. Use the AwsS3 adapter instead. */ -class AclAwareAmazonS3 implements Adapter, - MetadataSupporter +class AclAwareAmazonS3 implements Adapter, MetadataSupporter { protected $delegate; protected $s3; protected $bucketName; protected $aclConstant = AmazonClient::ACL_PRIVATE; - protected $users = array(); + protected $users = []; public function __construct(Adapter $delegate, AmazonClient $s3, $bucketName) { @@ -36,7 +35,7 @@ public function __construct(Adapter $delegate, AmazonClient $s3, $bucketName) public function setAclConstant($constant) { - if (!defined($constant = 'AmazonS3::ACL_'.strtoupper($constant))) { + if (!defined($constant = 'AmazonS3::ACL_' . strtoupper($constant))) { throw new \InvalidArgumentException(sprintf('The ACL constant "%s" does not exist on AmazonS3.', $constant)); } @@ -45,20 +44,20 @@ public function setAclConstant($constant) public function setUsers(array $users) { - $this->users = array(); + $this->users = []; foreach ($users as $user) { if (!isset($user['permission'])) { throw new \InvalidArgumentException(sprintf('setUsers() expects an array where each item contains a "permission" key, but got %s.', json_encode($user))); } - if (!defined($constant = 'AmazonS3::GRANT_'.strtoupper($user['permission']))) { + if (!defined($constant = 'AmazonS3::GRANT_' . strtoupper($user['permission']))) { throw new \InvalidArgumentException('The permission must be the suffix for one of the AmazonS3::GRANT_ constants.'); } $user['permission'] = constant($constant); if (isset($user['group'])) { - if (!defined($constant = 'AmazonS3::USERS_'.strtoupper($user['group']))) { + if (!defined($constant = 'AmazonS3::USERS_' . strtoupper($user['group']))) { throw new \InvalidArgumentException('The group must be the suffix for one of the AmazonS3::USERS_ constants.'); } $user['id'] = constant($constant); @@ -166,7 +165,7 @@ public function getMetadata($key) return $this->delegate->getMetadata($key); } - return array(); + return []; } /** @@ -190,7 +189,7 @@ private function updateAcl($key) { $response = $this->s3->set_object_acl($this->bucketName, $key, $this->getAcl()); if (200 != $response->status) { - throw new \RuntimeException('S3-ACL change failed: '.print_r($response, true)); + throw new \RuntimeException('S3-ACL change failed: ' . print_r($response, true)); } } } diff --git a/src/Gaufrette/Adapter/AmazonS3.php b/src/Gaufrette/Adapter/AmazonS3.php index e085c7f8d..470cd1cd4 100644 --- a/src/Gaufrette/Adapter/AmazonS3.php +++ b/src/Gaufrette/Adapter/AmazonS3.php @@ -5,7 +5,7 @@ use AmazonS3 as AmazonClient; use Gaufrette\Adapter; -@trigger_error('The '.__NAMESPACE__.'\AmazonS3 adapter is deprecated since version 0.4 and will be removed in 1.0. Use the AwsS3 adapter instead.', E_USER_DEPRECATED); +@trigger_error('The ' . __NAMESPACE__ . '\AmazonS3 adapter is deprecated since version 0.4 and will be removed in 1.0. Use the AwsS3 adapter instead.', E_USER_DEPRECATED); /** * Amazon S3 adapter using the AWS SDK for PHP v1.x. @@ -17,8 +17,7 @@ * * @deprecated The AmazonS3 adapter is deprecated since version 0.4 and will be removed in 1.0. Use the AwsS3 adapter instead. */ -class AmazonS3 implements Adapter, - MetadataSupporter +class AmazonS3 implements Adapter, MetadataSupporter { protected $service; protected $bucket; @@ -26,12 +25,12 @@ class AmazonS3 implements Adapter, protected $metadata; protected $options; - public function __construct(AmazonClient $service, $bucket, $options = array()) + public function __construct(AmazonClient $service, $bucket, $options = []) { $this->service = $service; $this->bucket = $bucket; $this->options = array_replace_recursive( - array('directory' => '', 'create' => false, 'region' => $service->hostname, 'acl' => AmazonClient::ACL_PUBLIC), + ['directory' => '', 'create' => false, 'region' => $service->hostname, 'acl' => AmazonClient::ACL_PUBLIC], $options ); } @@ -93,7 +92,7 @@ public function getMetadata($key) { $path = $this->computePath($key); - return isset($this->metadata[$path]) ? $this->metadata[$path] : array(); + return isset($this->metadata[$path]) ? $this->metadata[$path] : []; } /** @@ -124,14 +123,14 @@ public function rename($sourceKey, $targetKey) $this->ensureBucketExists(); $response = $this->service->copy_object( - array( // source + [ // source 'bucket' => $this->bucket, 'filename' => $this->computePath($sourceKey), - ), - array( // target + ], + [ // target 'bucket' => $this->bucket, 'filename' => $this->computePath($targetKey), - ), + ], $this->getMetadata($sourceKey) ); @@ -146,9 +145,9 @@ public function write($key, $content) $this->ensureBucketExists(); $opt = array_replace_recursive( - array('acl' => $this->options['acl']), + ['acl' => $this->options['acl']], $this->getMetadata($key), - array('body' => $content) + ['body' => $content] ); $response = $this->service->create_object( @@ -202,7 +201,7 @@ public function keys() $list = $this->service->get_object_list($this->bucket); - $keys = array(); + $keys = []; foreach ($list as $file) { if ('.' !== $dirname = \Gaufrette\Util\Path::dirname($file)) { $keys[] = $dirname; @@ -235,7 +234,7 @@ public function delete($key) */ public function isDirectory($key) { - if ($this->exists($key.'/')) { + if ($this->exists($key . '/')) { return true; } diff --git a/src/Gaufrette/Adapter/Apc.php b/src/Gaufrette/Adapter/Apc.php index 6025dba21..b2367c0e4 100644 --- a/src/Gaufrette/Adapter/Apc.php +++ b/src/Gaufrette/Adapter/Apc.php @@ -5,7 +5,7 @@ use Gaufrette\Adapter; use Gaufrette\Util; -@trigger_error('The '.__NAMESPACE__.'\Apc adapter is deprecated since version 0.4 and will be removed in 1.0.', E_USER_DEPRECATED); +@trigger_error('The ' . __NAMESPACE__ . '\Apc adapter is deprecated since version 0.4 and will be removed in 1.0.', E_USER_DEPRECATED); /** * Apc adapter, a non-persistent adapter for when this sort of thing is appropriate. @@ -75,10 +75,10 @@ public function keys() $cachedKeys = $this->getCachedKeysIterator(); if (null === $cachedKeys) { - return array(); + return []; } - $keys = array(); + $keys = []; foreach ($cachedKeys as $key => $value) { $pattern = sprintf('/^%s/', preg_quote($this->prefix, '/')); $keys[] = preg_replace($pattern, '', $key); @@ -135,7 +135,7 @@ public function isDirectory($key) */ public function computePath($key) { - return $this->prefix.$key; + return $this->prefix . $key; } /** @@ -146,7 +146,7 @@ public function computePath($key) */ protected function getCachedKeysIterator($key = '', $format = APC_ITER_NONE) { - $pattern = sprintf('/^%s/', preg_quote($this->prefix.$key, '/')); + $pattern = sprintf('/^%s/', preg_quote($this->prefix . $key, '/')); return new \APCIterator('user', $pattern, $format); } diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index 72e3a5988..853025a63 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -11,11 +11,7 @@ * * @author Michael Dowling */ -class AwsS3 implements Adapter, - MetadataSupporter, - ListKeysAware, - SizeCalculator, - MimeTypeProvider +class AwsS3 implements Adapter, MetadataSupporter, ListKeysAware, SizeCalculator, MimeTypeProvider { /** @var S3Client */ protected $service; @@ -79,7 +75,7 @@ public function getUrl($key, array $options = []) return $this->service->getObjectUrl( $this->bucket, $this->computePath($key), - isset($options['expires']) ? $options['expires'] : null, + $options['expires'] ?? null, $options ); } @@ -138,7 +134,7 @@ public function rename($sourceKey, $targetKey) $this->ensureBucketExists(); $options = $this->getOptions( $targetKey, - ['CopySource' => $this->bucket.'/'.$this->computePath($sourceKey)] + ['CopySource' => $this->bucket . '/' . $this->computePath($sourceKey)] ); try { @@ -267,7 +263,7 @@ public function isDirectory($key) { $result = $this->service->listObjects([ 'Bucket' => $this->bucket, - 'Prefix' => rtrim($this->computePath($key), '/').'/', + 'Prefix' => rtrim($this->computePath($key), '/') . '/', 'MaxKeys' => 1, ]); if (isset($result['Contents'])) { @@ -307,7 +303,7 @@ protected function ensureBucketExists() $this->service->createBucket([ 'Bucket' => $this->bucket, - 'LocationConstraint' => $this->service->getRegion() + 'LocationConstraint' => $this->service->getRegion(), ]); $this->bucketExists = true; @@ -370,6 +366,7 @@ public function mimeType($key) { try { $result = $this->service->headObject($this->getOptions($key)); + return ($result['ContentType']); } catch (\Exception $e) { return false; diff --git a/src/Gaufrette/Adapter/AzureBlobStorage.php b/src/Gaufrette/Adapter/AzureBlobStorage.php index 965c21d01..1bd3969fd 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage.php @@ -20,11 +20,7 @@ * @author Luciano Mammino * @author Paweł Czyżewski */ -class AzureBlobStorage implements Adapter, - MetadataSupporter, - SizeCalculator, - ChecksumCalculator - +class AzureBlobStorage implements Adapter, MetadataSupporter, SizeCalculator, ChecksumCalculator { /** * Error constants. @@ -271,9 +267,11 @@ public function keys() try { if ($this->multiContainerMode) { $containersList = $this->blobProxy->listContainers(); + return call_user_func_array('array_merge', array_map( - function(Container $container) { + function (Container $container) { $containerName = $container->getName(); + return $this->fetchBlobs($containerName, $containerName); }, $containersList->getContainers() @@ -332,7 +330,6 @@ public function size($key) return false; } - } /** @@ -564,12 +561,14 @@ private function tokenizeKey($key) private function fetchBlobs($containerName, $prefix = null) { $blobList = $this->blobProxy->listBlobs($containerName); + return array_map( function (Blob $blob) use ($prefix) { $name = $blob->getName(); if (null !== $prefix) { - $name = $prefix .'/'. $name; + $name = $prefix . '/' . $name; } + return $name; }, $blobList->getBlobs() diff --git a/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php b/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php index 8eea93c24..d675180aa 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php @@ -33,8 +33,8 @@ public function create() if (class_exists(ServicesBuilder::class)) { // for microsoft/azure-storage < 1.0 return ServicesBuilder::getInstance()->createBlobService($this->connectionString); - } else { - return BlobRestProxy::createBlobService($this->connectionString); } + + return BlobRestProxy::createBlobService($this->connectionString); } } diff --git a/src/Gaufrette/Adapter/Cache.php b/src/Gaufrette/Adapter/Cache.php index 4dba02cb4..8b2d37e95 100644 --- a/src/Gaufrette/Adapter/Cache.php +++ b/src/Gaufrette/Adapter/Cache.php @@ -6,7 +6,7 @@ use Gaufrette\Adapter; use Gaufrette\Adapter\InMemory as InMemoryAdapter; -@trigger_error('The '.__NAMESPACE__.'\Cache adapter is deprecated since version 0.4 and will be removed in 1.0.', E_USER_DEPRECATED); +@trigger_error('The ' . __NAMESPACE__ . '\Cache adapter is deprecated since version 0.4 and will be removed in 1.0.', E_USER_DEPRECATED); /** * Cache adapter. @@ -15,8 +15,7 @@ * * @deprecated The Cache adapter is deprecated since version 0.4 and will be removed in 1.0. */ -class Cache implements Adapter, - MetadataSupporter +class Cache implements Adapter, MetadataSupporter { /** * @var Adapter diff --git a/src/Gaufrette/Adapter/DoctrineDbal.php b/src/Gaufrette/Adapter/DoctrineDbal.php index d21deb6be..51d9a8094 100644 --- a/src/Gaufrette/Adapter/DoctrineDbal.php +++ b/src/Gaufrette/Adapter/DoctrineDbal.php @@ -13,25 +13,23 @@ * @author Antoine Hérault * @author Leszek Prabucki */ -class DoctrineDbal implements Adapter, - ChecksumCalculator, - ListKeysAware +class DoctrineDbal implements Adapter, ChecksumCalculator, ListKeysAware { protected $connection; protected $table; - protected $columns = array( + protected $columns = [ 'key' => 'key', 'content' => 'content', 'mtime' => 'mtime', 'checksum' => 'checksum', - ); + ]; /** * @param Connection $connection The DBAL connection * @param string $table The files table * @param array $columns The column names */ - public function __construct(Connection $connection, $table, array $columns = array()) + public function __construct(Connection $connection, $table, array $columns = []) { $this->connection = $connection; $this->table = $table; @@ -43,7 +41,7 @@ public function __construct(Connection $connection, $table, array $columns = arr */ public function keys() { - $keys = array(); + $keys = []; $stmt = $this->connection->executeQuery(sprintf( 'SELECT %s FROM %s', $this->getQuotedColumn('key'), @@ -60,8 +58,8 @@ public function rename($sourceKey, $targetKey) { return (boolean) $this->connection->update( $this->table, - array($this->getQuotedColumn('key') => $targetKey), - array($this->getQuotedColumn('key') => $sourceKey) + [$this->getQuotedColumn('key') => $targetKey], + [$this->getQuotedColumn('key') => $sourceKey] ); } @@ -93,7 +91,7 @@ public function exists($key) $this->getQuotedTable(), $this->getQuotedColumn('key') ), - array('key' => $key) + ['key' => $key] ); } @@ -112,7 +110,7 @@ public function delete($key) { return (boolean) $this->connection->delete( $this->table, - array($this->getQuotedColumn('key') => $key) + [$this->getQuotedColumn('key') => $key] ); } @@ -121,17 +119,17 @@ public function delete($key) */ public function write($key, $content) { - $values = array( + $values = [ $this->getQuotedColumn('content') => $content, $this->getQuotedColumn('mtime') => time(), $this->getQuotedColumn('checksum') => Util\Checksum::fromContent($content), - ); + ]; if ($this->exists($key)) { $this->connection->update( $this->table, $values, - array($this->getQuotedColumn('key') => $key) + [$this->getQuotedColumn('key') => $key] ); } else { $values[$this->getQuotedColumn('key')] = $key; @@ -158,7 +156,7 @@ private function getColumnValue($key, $column) $this->getQuotedTable(), $this->getQuotedColumn('key') ), - array('key' => $key) + ['key' => $key] ); return $value; @@ -178,16 +176,16 @@ public function listKeys($prefix = '') $this->getQuotedTable(), $this->getQuotedColumn('key') ), - array('pattern' => sprintf('%s%%', $prefix)) + ['pattern' => sprintf('%s%%', $prefix)] ); - return array( - 'dirs' => array(), + return [ + 'dirs' => [], 'keys' => array_map(function ($value) { - return $value['_key']; - }, + return $value['_key']; + }, $keys), - ); + ]; } private function getQuotedTable() diff --git a/src/Gaufrette/Adapter/Dropbox.php b/src/Gaufrette/Adapter/Dropbox.php index aa4406956..a39b7a220 100644 --- a/src/Gaufrette/Adapter/Dropbox.php +++ b/src/Gaufrette/Adapter/Dropbox.php @@ -8,7 +8,7 @@ use Dropbox_API as DropboxApi; use Dropbox_Exception_NotFound as DropboxNotFoundException; -@trigger_error('The '.__NAMESPACE__.'\Dropbox adapter is deprecated since version 0.4 and will be removed in 1.0. You can move to our Flysystem adapter and use their Dropbox adapter if needed.', E_USER_DEPRECATED); +@trigger_error('The ' . __NAMESPACE__ . '\Dropbox adapter is deprecated since version 0.4 and will be removed in 1.0. You can move to our Flysystem adapter and use their Dropbox adapter if needed.', E_USER_DEPRECATED); /** * Dropbox adapter. @@ -134,10 +134,10 @@ public function keys() { $metadata = $this->client->getMetaData('/', true); if (!isset($metadata['contents'])) { - return array(); + return []; } - $keys = array(); + $keys = []; foreach ($metadata['contents'] as $value) { $file = ltrim($value['path'], '/'); $keys[] = $file; diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index 05a228aeb..13797677c 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -11,10 +11,7 @@ * * @author Antoine Hérault */ -class Ftp implements Adapter, - FileFactory, - ListKeysAware, - SizeCalculator +class Ftp implements Adapter, FileFactory, ListKeysAware, SizeCalculator { protected $connection = null; protected $directory; @@ -27,7 +24,7 @@ class Ftp implements Adapter, protected $mode; protected $ssl; protected $timeout; - protected $fileData = array(); + protected $fileData = []; protected $utf8; /** @@ -35,7 +32,7 @@ class Ftp implements Adapter, * @param string $host The host of the ftp server * @param array $options The options like port, username, password, passive, create, mode */ - public function __construct($directory, $host, $options = array()) + public function __construct($directory, $host, $options = []) { if (!extension_loaded('ftp')) { throw new \RuntimeException('Unable to use Gaufrette\Adapter\Ftp as the FTP extension is not available.'); @@ -43,15 +40,15 @@ public function __construct($directory, $host, $options = array()) $this->directory = (string) $directory; $this->host = $host; - $this->port = isset($options['port']) ? $options['port'] : 21; - $this->username = isset($options['username']) ? $options['username'] : null; - $this->password = isset($options['password']) ? $options['password'] : null; - $this->passive = isset($options['passive']) ? $options['passive'] : false; - $this->create = isset($options['create']) ? $options['create'] : false; - $this->mode = isset($options['mode']) ? $options['mode'] : FTP_BINARY; - $this->ssl = isset($options['ssl']) ? $options['ssl'] : false; - $this->timeout = isset($options['timeout']) ? $options['timeout'] : 90; - $this->utf8 = isset($options['utf8']) ? $options['utf8'] : false; + $this->port = $options['port'] ?? 21; + $this->username = $options['username'] ?? null; + $this->password = $options['password'] ?? null; + $this->passive = $options['passive'] ?? false; + $this->create = $options['create'] ?? false; + $this->mode = $options['mode'] ?? FTP_BINARY; + $this->ssl = $options['ssl'] ?? false; + $this->timeout = $options['timeout'] ?? 90; + $this->utf8 = $options['utf8'] ?? false; } /** @@ -123,14 +120,14 @@ public function exists($key) { $this->ensureDirectoryExists($this->directory, $this->create); - $file = $this->computePath($key); + $file = $this->computePath($key); $lines = ftp_rawlist($this->getConnection(), '-al ' . \Gaufrette\Util\Path::dirname($file)); if (false === $lines) { return false; } - $pattern = '{(?) '.preg_quote(basename($file)).'( -> |$)}m'; + $pattern = '{(?) ' . preg_quote(basename($file)) . '( -> |$)}m'; foreach ($lines as $line) { if (preg_match($pattern, $line)) { return true; @@ -168,9 +165,9 @@ public function listKeys($prefix = '') return $keys; } - $filteredKeys = array(); - foreach (array('keys', 'dirs') as $hash) { - $filteredKeys[$hash] = array(); + $filteredKeys = []; + foreach (['keys', 'dirs'] as $hash) { + $filteredKeys[$hash] = []; foreach ($keys[$hash] as $key) { if (0 === strpos($key, $prefix)) { $filteredKeys[$hash][] = $key; @@ -237,21 +234,21 @@ public function listDirectory($directory = '') $directory = preg_replace('/^[\/]*([^\/].*)$/', '/$1', $directory); $items = $this->parseRawlist( - ftp_rawlist($this->getConnection(), '-al '.$this->directory.$directory) ?: array() + ftp_rawlist($this->getConnection(), '-al ' . $this->directory . $directory) ?: [] ); - $fileData = $dirs = array(); + $fileData = $dirs = []; foreach ($items as $itemData) { if ('..' === $itemData['name'] || '.' === $itemData['name']) { continue; } - $item = array( + $item = [ 'name' => $itemData['name'], - 'path' => trim(($directory ? $directory.'/' : '').$itemData['name'], '/'), + 'path' => trim(($directory ? $directory . '/' : '') . $itemData['name'], '/'), 'time' => $itemData['time'], 'size' => $itemData['size'], - ); + ]; if ('-' === substr($itemData['perms'], 0, 1)) { $fileData[$item['path']] = $item; @@ -262,10 +259,10 @@ public function listDirectory($directory = '') $this->fileData = array_merge($fileData, $this->fileData); - return array( + return [ 'keys' => array_keys($fileData), 'dirs' => $dirs, - ); + ]; } /** @@ -380,28 +377,28 @@ private function fetchKeys($directory = '', $onlyKeys = true) { $directory = preg_replace('/^[\/]*([^\/].*)$/', '/$1', $directory); - $lines = ftp_rawlist($this->getConnection(), '-alR '.$this->directory.$directory); + $lines = ftp_rawlist($this->getConnection(), '-alR ' . $this->directory . $directory); if (false === $lines) { - return array('keys' => array(), 'dirs' => array()); + return ['keys' => [], 'dirs' => []]; } - $regexDir = '/'.preg_quote($this->directory.$directory, '/').'\/?(.+):$/u'; + $regexDir = '/' . preg_quote($this->directory . $directory, '/') . '\/?(.+):$/u'; $regexItem = '/^(?:([d\-\d])\S+)\s+\S+(?:(?:\s+\S+){5})?\s+(\S+)\s+(.+?)$/'; $prevLine = null; - $directories = array(); - $keys = array('keys' => array(), 'dirs' => array()); + $directories = []; + $keys = ['keys' => [], 'dirs' => []]; foreach ((array) $lines as $line) { if ('' === $prevLine && preg_match($regexDir, $line, $match)) { $directory = $match[1]; unset($directories[$directory]); if ($onlyKeys) { - $keys = array( + $keys = [ 'keys' => array_merge($keys['keys'], $keys['dirs']), - 'dirs' => array(), - ); + 'dirs' => [], + ]; } } elseif (preg_match($regexItem, $line, $tokens)) { $name = $tokens[3]; @@ -410,7 +407,7 @@ private function fetchKeys($directory = '', $onlyKeys = true) continue; } - $path = ltrim($directory.'/'.$name, '/'); + $path = ltrim($directory . '/' . $name, '/'); if ('d' === $tokens[1] || '' === $tokens[2]) { $keys['dirs'][] = $path; @@ -423,10 +420,10 @@ private function fetchKeys($directory = '', $onlyKeys = true) } if ($onlyKeys) { - $keys = array( + $keys = [ 'keys' => array_merge($keys['keys'], $keys['dirs']), - 'dirs' => array(), - ); + 'dirs' => [], + ]; } foreach (array_keys($directories) as $directory) { @@ -445,30 +442,30 @@ private function fetchKeys($directory = '', $onlyKeys = true) */ private function parseRawlist(array $rawlist) { - $parsed = array(); + $parsed = []; foreach ($rawlist as $line) { $infos = preg_split("/[\s]+/", $line, 9); if ($this->isLinuxListing($infos)) { - $infos[7] = (strrpos($infos[7], ':') != 2) ? ($infos[7].' 00:00') : (date('Y').' '.$infos[7]); + $infos[7] = (strrpos($infos[7], ':') != 2) ? ($infos[7] . ' 00:00') : (date('Y') . ' ' . $infos[7]); if ('total' !== $infos[0]) { - $parsed[] = array( + $parsed[] = [ 'perms' => $infos[0], 'num' => $infos[1], 'size' => $infos[4], - 'time' => strtotime($infos[5].' '.$infos[6].'. '.$infos[7]), + 'time' => strtotime($infos[5] . ' ' . $infos[6] . '. ' . $infos[7]), 'name' => $infos[8], - ); + ]; } } elseif (count($infos) >= 4) { $isDir = (boolean) ('' === $infos[2]); - $parsed[] = array( + $parsed[] = [ 'perms' => $isDir ? 'd' : '-', 'num' => '', 'size' => $isDir ? '' : $infos[2], - 'time' => strtotime($infos[0].' '.$infos[1]), + 'time' => strtotime($infos[0] . ' ' . $infos[1]), 'name' => $infos[3], - ); + ]; } } @@ -482,7 +479,7 @@ private function parseRawlist(array $rawlist) */ private function computePath($key) { - return rtrim($this->directory, '/').'/'.$key; + return rtrim($this->directory, '/') . '/' . $key; } /** @@ -542,18 +539,20 @@ private function connect() // login ftp user if (!@ftp_login($this->connection, $username, $password)) { $this->close(); + throw new \RuntimeException(sprintf('Could not login as %s.', $username)); } // switch to passive mode if needed if ($this->passive && !ftp_pasv($this->connection, true)) { $this->close(); + throw new \RuntimeException('Could not turn passive mode on.'); } // enable utf8 mode if configured - if($this->utf8 == true) { - ftp_raw($this->connection, "OPTS UTF8 ON"); + if ($this->utf8 == true) { + ftp_raw($this->connection, 'OPTS UTF8 ON'); } // ensure the adapter's directory exists @@ -562,12 +561,14 @@ private function connect() $this->ensureDirectoryExists($this->directory, $this->create); } catch (\RuntimeException $e) { $this->close(); + throw $e; } // change the current directory for the adapter's directory if (!ftp_chdir($this->connection, $this->directory)) { $this->close(); + throw new \RuntimeException(sprintf('Could not change current directory for the \'%s\' directory.', $this->directory)); } } diff --git a/src/Gaufrette/Adapter/GoogleCloudStorage.php b/src/Gaufrette/Adapter/GoogleCloudStorage.php index 7574a4628..24c834003 100644 --- a/src/Gaufrette/Adapter/GoogleCloudStorage.php +++ b/src/Gaufrette/Adapter/GoogleCloudStorage.php @@ -10,15 +10,13 @@ * * @author Patrik Karisch */ -class GoogleCloudStorage implements Adapter, - MetadataSupporter, - ListKeysAware +class GoogleCloudStorage implements Adapter, MetadataSupporter, ListKeysAware { protected $service; protected $bucket; protected $options; protected $bucketExists; - protected $metadata = array(); + protected $metadata = []; protected $detectContentType; /** @@ -31,16 +29,16 @@ class GoogleCloudStorage implements Adapter, public function __construct( \Google_Service_Storage $service, $bucket, - array $options = array(), + array $options = [], $detectContentType = false ) { $this->service = $service; $this->bucket = $bucket; $this->options = array_replace( - array( + [ 'directory' => '', 'acl' => 'private', - ), + ], $options ); @@ -126,10 +124,10 @@ public function write($key, $content) $path = $this->computePath($key); $metadata = $this->getMetadata($key); - $options = array( + $options = [ 'uploadType' => 'multipart', 'data' => $content, - ); + ]; /* * If the ContentType was not already set in the metadata, then we autodetect @@ -269,7 +267,7 @@ public function rename($sourceKey, $targetKey) */ public function isDirectory($key) { - if ($this->exists($key.'/')) { + if ($this->exists($key . '/')) { return true; } @@ -283,7 +281,7 @@ public function listKeys($prefix = '') { $this->ensureBucketExists(); - $options = array(); + $options = []; if ((string) $prefix != '') { $options['prefix'] = $this->computePath($prefix); } elseif (!empty($this->options['directory'])) { @@ -291,7 +289,7 @@ public function listKeys($prefix = '') } $list = $this->service->objects->listObjects($this->bucket, $options); - $keys = array(); + $keys = []; // FIXME: Temporary workaround for google/google-api-php-client#375 $reflectionClass = new \ReflectionClass('Google_Service_Storage_Objects'); @@ -326,7 +324,7 @@ public function getMetadata($key) { $path = $this->computePath($key); - return isset($this->metadata[$path]) ? $this->metadata[$path] : array(); + return isset($this->metadata[$path]) ? $this->metadata[$path] : []; } /** @@ -372,7 +370,7 @@ protected function computePath($key) * * @return bool|\Google_Service_Storage_StorageObject */ - private function getObjectData($path, $options = array()) + private function getObjectData($path, $options = []) { try { return $this->service->objects->get($this->bucket, $path, $options); diff --git a/src/Gaufrette/Adapter/GridFS.php b/src/Gaufrette/Adapter/GridFS.php index 85446ab0e..49d9b93fb 100644 --- a/src/Gaufrette/Adapter/GridFS.php +++ b/src/Gaufrette/Adapter/GridFS.php @@ -14,11 +14,7 @@ * @author Antoine Hérault * @author Leszek Prabucki */ -class GridFS implements Adapter, - ChecksumCalculator, - MetadataSupporter, - ListKeysAware, - SizeCalculator +class GridFS implements Adapter, ChecksumCalculator, MetadataSupporter, ListKeysAware, SizeCalculator { /** @var array */ private $metadata = []; @@ -169,17 +165,16 @@ public function getMetadata($key) { if (isset($this->metadata[$key])) { return $this->metadata[$key]; - } else { - $meta = $this->bucket->findOne(['filename' => $key], ['projection' => ['metadata' => 1,'_id' => 0]]); + } + $meta = $this->bucket->findOne(['filename' => $key], ['projection' => ['metadata' => 1,'_id' => 0]]); - if ($meta === null || !isset($meta['metadata'])) { - return array(); - } + if ($meta === null || !isset($meta['metadata'])) { + return []; + } - $this->metadata[$key] = iterator_to_array($meta['metadata']); + $this->metadata[$key] = iterator_to_array($meta['metadata']); - return $this->metadata[$key]; - } + return $this->metadata[$key]; } /** @@ -222,5 +217,4 @@ public function size($key) return $size['length']; } - } diff --git a/src/Gaufrette/Adapter/InMemory.php b/src/Gaufrette/Adapter/InMemory.php index 80fd15f16..644955786 100644 --- a/src/Gaufrette/Adapter/InMemory.php +++ b/src/Gaufrette/Adapter/InMemory.php @@ -12,15 +12,14 @@ * * @author Antoine Hérault */ -class InMemory implements Adapter, - MimeTypeProvider +class InMemory implements Adapter, MimeTypeProvider { - protected $files = array(); + protected $files = []; /** * @param array $files An array of files */ - public function __construct(array $files = array()) + public function __construct(array $files = []) { $this->setFiles($files); } @@ -32,16 +31,16 @@ public function __construct(array $files = array()) */ public function setFiles(array $files) { - $this->files = array(); + $this->files = []; foreach ($files as $key => $file) { if (!is_array($file)) { - $file = array('content' => $file); + $file = ['content' => $file]; } - $file = array_merge(array( + $file = array_merge([ 'content' => null, 'mtime' => null, - ), $file); + ], $file); $this->setFile($key, $file['content'], $file['mtime']); } @@ -60,10 +59,10 @@ public function setFile($key, $content = null, $mtime = null) $mtime = time(); } - $this->files[$key] = array( + $this->files[$key] = [ 'content' => (string) $content, 'mtime' => (integer) $mtime, - ); + ]; } /** diff --git a/src/Gaufrette/Adapter/LazyOpenCloud.php b/src/Gaufrette/Adapter/LazyOpenCloud.php index 190f790d6..f5fec63db 100644 --- a/src/Gaufrette/Adapter/LazyOpenCloud.php +++ b/src/Gaufrette/Adapter/LazyOpenCloud.php @@ -4,7 +4,7 @@ use Gaufrette\Adapter\OpenStackCloudFiles\ObjectStoreFactoryInterface; -@trigger_error('The '.__NAMESPACE__.'\LazyOpenCloud adapter is deprecated since version 0.4 and will be removed in 1.0. Use the OpenCloud adapter instead.', E_USER_DEPRECATED); +@trigger_error('The ' . __NAMESPACE__ . '\LazyOpenCloud adapter is deprecated since version 0.4 and will be removed in 1.0. Use the OpenCloud adapter instead.', E_USER_DEPRECATED); /** * LazyOpenCloud. diff --git a/src/Gaufrette/Adapter/Local.php b/src/Gaufrette/Adapter/Local.php index bab1f4dc1..81b253f8a 100644 --- a/src/Gaufrette/Adapter/Local.php +++ b/src/Gaufrette/Adapter/Local.php @@ -6,18 +6,13 @@ use Gaufrette\Adapter; use Gaufrette\Stream; - /** * Adapter for the local filesystem. * * @author Antoine Hérault * @author Leszek Prabucki */ -class Local implements Adapter, - StreamFactory, - ChecksumCalculator, - SizeCalculator, - MimeTypeProvider +class Local implements Adapter, StreamFactory, ChecksumCalculator, SizeCalculator, MimeTypeProvider { protected $directory; private $create; @@ -255,7 +250,7 @@ protected function computePath($key) { $this->ensureDirectoryExists($this->directory, $this->create); - return $this->normalizePath($this->directory.'/'.$key); + return $this->normalizePath($this->directory . '/' . $key); } /** diff --git a/src/Gaufrette/Adapter/MogileFS.php b/src/Gaufrette/Adapter/MogileFS.php index 813ea7206..5b35c5947 100644 --- a/src/Gaufrette/Adapter/MogileFS.php +++ b/src/Gaufrette/Adapter/MogileFS.php @@ -5,7 +5,7 @@ use Gaufrette\Adapter; use Gaufrette\Util; -@trigger_error('The '.__NAMESPACE__.'\MogileFS adapter is deprecated since version 0.4 and will be removed in 1.0.', E_USER_DEPRECATED); +@trigger_error('The ' . __NAMESPACE__ . '\MogileFS adapter is deprecated since version 0.4 and will be removed in 1.0.', E_USER_DEPRECATED); /** * Adapter for the MogileFS filesystem. @@ -67,6 +67,7 @@ public function read($key) } fclose($fh); + break; } } @@ -82,7 +83,7 @@ public function write($key, $content, array $metadata = null) $closeres = false; if (mb_strlen($content) > 0) { - $res = $this->doRequest('CREATE_OPEN', array('key' => $key, 'class' => $metadata['mogile_class'])); + $res = $this->doRequest('CREATE_OPEN', ['key' => $key, 'class' => $metadata['mogile_class']]); if ($res && preg_match('/^http:\/\/([a-z0-9.-]*):([0-9]*)\/(.*)$/', $res['path'], $matches)) { $host = $matches[1]; @@ -92,8 +93,8 @@ public function write($key, $content, array $metadata = null) $status = $this->putFile($res['path'], $content); if ($status) { - $params = array('key' => $key, 'class' => $metadata['mogile_class'], 'devid' => $res['devid'], - 'fid' => $res['fid'], 'path' => urldecode($res['path']), ); + $params = ['key' => $key, 'class' => $metadata['mogile_class'], 'devid' => $res['devid'], + 'fid' => $res['fid'], 'path' => urldecode($res['path']), ]; $closeres = $this->doRequest('CREATE_CLOSE', $params); } } @@ -111,7 +112,7 @@ public function write($key, $content, array $metadata = null) */ public function delete($key) { - $this->doRequest('DELETE', array('key' => $key)); + $this->doRequest('DELETE', ['key' => $key]); return true; } @@ -121,10 +122,10 @@ public function delete($key) */ public function rename($sourceKey, $targetKey) { - $this->doRequest('RENAME', array( + $this->doRequest('RENAME', [ 'from_key' => $sourceKey, 'to_key' => $targetKey, - )); + ]); return true; } @@ -152,7 +153,7 @@ public function keys() $result = $this->doRequest('LIST_KEYS'); } catch (\RuntimeException $e) { if (self::ERR_NONE_MATCH === $e->getCode()) { - return array(); + return []; } throw $e; @@ -191,18 +192,18 @@ public function getDomains() return false; } - $domains = array(); + $domains = []; for ($i = 1; $i <= $res['domains']; ++$i) { - $dom = 'domain'.$i; - $classes = array(); + $dom = 'domain' . $i; + $classes = []; // Associate classes to current domain (class name => mindevcount) - for ($j = 1; $j <= $res[$dom.'classes']; ++$j) { - $classes[$res[$dom.'class'.$j.'name']] = $res[$dom.'class'.$j.'mindevcount']; + for ($j = 1; $j <= $res[$dom . 'classes']; ++$j) { + $classes[$res[$dom . 'class' . $j . 'name']] = $res[$dom . 'class' . $j . 'mindevcount']; } - $domains[] = array('name' => $res[$dom], 'classes' => $classes); + $domains[] = ['name' => $res[$dom], 'classes' => $classes]; } return $domains; @@ -259,7 +260,7 @@ private function close() * * @return mixed Array on success, false on failure */ - private function doRequest($cmd, $args = array()) + private function doRequest($cmd, $args = []) { clearstatcache(); $args['domain'] = $this->domain; @@ -280,15 +281,19 @@ private function doRequest($cmd, $args = array()) switch ($errorName) { case 'unknown_key': $errorCode = static::ERR_UNKNOWN_KEY; + break; case 'empty_file': $errorCode = static::ERR_EMPTY_FILE; + break; case 'none_match': $errorCode = static::ERR_NONE_MATCH; + break; case 'key_exists': $errorCode = static::ERR_KEY_EXISTS; + break; default: $errorCode = static::ERR_OTHER; @@ -312,7 +317,7 @@ private function doRequest($cmd, $args = array()) */ private function getPaths($key) { - $res = $this->doRequest('GET_PATHS', array('key' => $key)); + $res = $this->doRequest('GET_PATHS', ['key' => $key]); unset($res['paths']); return $res; @@ -342,11 +347,11 @@ private function putFile($path, $data) stream_set_blocking($fp, true); stream_set_timeout($fp, 30, 200000); - $out = 'PUT '.$url['path'].' HTTP/1.1'.$b; - $out .= 'Host: '.$url['host'].$b; - $out .= 'Content-Length: '.Util\Size::fromContent($data).$b.$b; + $out = 'PUT ' . $url['path'] . ' HTTP/1.1' . $b; + $out .= 'Host: ' . $url['host'] . $b; + $out .= 'Content-Length: ' . Util\Size::fromContent($data) . $b . $b; $out .= $data; - $out .= $b.$b; + $out .= $b . $b; fwrite($fp, $out); fflush($fp); diff --git a/src/Gaufrette/Adapter/OpenCloud.php b/src/Gaufrette/Adapter/OpenCloud.php index 3877177bc..1a739c0c0 100644 --- a/src/Gaufrette/Adapter/OpenCloud.php +++ b/src/Gaufrette/Adapter/OpenCloud.php @@ -17,8 +17,7 @@ * @author James Watson * @author Daniel Richter */ -class OpenCloud implements Adapter, - ChecksumCalculator +class OpenCloud implements Adapter, ChecksumCalculator { /** * @var Service @@ -141,7 +140,7 @@ public function exists($key) public function keys() { $objectList = $this->getContainer()->objectList(); - $keys = array(); + $keys = []; while ($object = $objectList->next()) { $keys[] = $object->getName(); diff --git a/src/Gaufrette/Adapter/PhpseclibSftp.php b/src/Gaufrette/Adapter/PhpseclibSftp.php index 19e82477d..e8b460f15 100644 --- a/src/Gaufrette/Adapter/PhpseclibSftp.php +++ b/src/Gaufrette/Adapter/PhpseclibSftp.php @@ -7,9 +7,7 @@ use Gaufrette\Filesystem; use Gaufrette\File; -class PhpseclibSftp implements Adapter, - FileFactory, - ListKeysAware +class PhpseclibSftp implements Adapter, FileFactory, ListKeysAware { protected $sftp; protected $directory; @@ -119,9 +117,9 @@ public function listKeys($prefix = '') return $keys; } - $filteredKeys = array(); - foreach (array('keys', 'dirs') as $hash) { - $filteredKeys[$hash] = array(); + $filteredKeys = []; + foreach (['keys', 'dirs'] as $hash) { + $filteredKeys[$hash] = []; foreach ($keys[$hash] as $key) { if (0 === strpos($key, $prefix)) { $filteredKeys[$hash][] = $key; @@ -141,7 +139,7 @@ public function mtime($key) $stat = $this->sftp->stat($this->computePath($key)); - return isset($stat['mtime']) ? $stat['mtime'] : false; + return $stat['mtime'] ?? false; } /** @@ -199,12 +197,12 @@ protected function ensureDirectoryExists($directory, $create) protected function computePath($key) { - return $this->directory.'/'.ltrim($key, '/'); + return $this->directory . '/' . ltrim($key, '/'); } protected function fetchKeys($directory = '', $onlyKeys = true) { - $keys = array('keys' => array(), 'dirs' => array()); + $keys = ['keys' => [], 'dirs' => []]; $computedPath = $this->computePath($directory); if (!$this->sftp->file_exists($computedPath)) { @@ -217,7 +215,7 @@ protected function fetchKeys($directory = '', $onlyKeys = true) continue; } - $path = ltrim($directory.'/'.$filename, '/'); + $path = ltrim($directory . '/' . $filename, '/'); if (isset($stat['type']) && $stat['type'] === NET_SFTP_TYPE_DIRECTORY) { $keys['dirs'][] = $path; } else { @@ -229,7 +227,7 @@ protected function fetchKeys($directory = '', $onlyKeys = true) if ($onlyKeys && !empty($dirs)) { $keys['keys'] = array_merge($keys['keys'], $dirs); - $keys['dirs'] = array(); + $keys['dirs'] = []; } foreach ($dirs as $dir) { diff --git a/src/Gaufrette/Adapter/Sftp.php b/src/Gaufrette/Adapter/Sftp.php index bc40e447b..96af99185 100644 --- a/src/Gaufrette/Adapter/Sftp.php +++ b/src/Gaufrette/Adapter/Sftp.php @@ -5,13 +5,12 @@ use Gaufrette\Adapter; use Ssh\Sftp as SftpClient; -@trigger_error('The '.__NAMESPACE__.'\Sftp adapter is deprecated since version 0.4 and will be removed in 1.0.', E_USER_DEPRECATED); +@trigger_error('The ' . __NAMESPACE__ . '\Sftp adapter is deprecated since version 0.4 and will be removed in 1.0.', E_USER_DEPRECATED); /** * @deprecated The Sftp adapter is deprecated since version 0.4 and will be removed in 1.0. */ -class Sftp implements Adapter, - ChecksumCalculator +class Sftp implements Adapter, ChecksumCalculator { protected $sftp; protected $directory; @@ -102,9 +101,9 @@ public function keys() { $this->initialize(); $results = $this->sftp->listDirectory($this->directory, true); - $files = array_map(array($this, 'computeKey'), $results['files']); + $files = array_map([$this, 'computeKey'], $results['files']); - $dirs = array(); + $dirs = []; foreach ($files as $file) { if ('.' !== $dirname = \Gaufrette\Util\Path::dirname($file)) { $dirs[] = $dirname; @@ -176,7 +175,7 @@ public function computeKey($path) */ protected function computePath($key) { - return $this->directory.'/'.ltrim($key, '/'); + return $this->directory . '/' . ltrim($key, '/'); } /** diff --git a/src/Gaufrette/Adapter/Zip.php b/src/Gaufrette/Adapter/Zip.php index 35fc1323d..0f68a8a8d 100644 --- a/src/Gaufrette/Adapter/Zip.php +++ b/src/Gaufrette/Adapter/Zip.php @@ -78,7 +78,7 @@ public function exists($key) */ public function keys() { - $keys = array(); + $keys = []; for ($i = 0; $i < $this->zipArchive->numFiles; ++$i) { $keys[$i] = $this->zipArchive->getNameIndex($i); @@ -104,7 +104,7 @@ public function mtime($key) { $stat = $this->getStat($key); - return isset($stat['mtime']) ? $stat['mtime'] : false; + return $stat['mtime'] ?? false; } /** @@ -143,7 +143,7 @@ public function getStat($key) { $stat = $this->zipArchive->statName($key); if (false === $stat) { - return array(); + return []; } return $stat; @@ -168,33 +168,43 @@ protected function reinitZipArchive() switch ($resultCode) { case ZipArchive::ER_EXISTS: $errMsg = 'File already exists.'; + break; case ZipArchive::ER_INCONS: $errMsg = 'Zip archive inconsistent.'; + break; case ZipArchive::ER_INVAL: $errMsg = 'Invalid argument.'; + break; case ZipArchive::ER_MEMORY: $errMsg = 'Malloc failure.'; + break; case ZipArchive::ER_NOENT: $errMsg = 'Invalid argument.'; + break; case ZipArchive::ER_NOZIP: $errMsg = 'Not a zip archive.'; + break; case ZipArchive::ER_OPEN: $errMsg = 'Can\'t open file.'; + break; case ZipArchive::ER_READ: $errMsg = 'Read error.'; + break; - case ZipArchive::ER_SEEK; + case ZipArchive::ER_SEEK: $errMsg = 'Seek error.'; + break; default: $errMsg = 'Unknown error.'; + break; } diff --git a/src/Gaufrette/File.php b/src/Gaufrette/File.php index 998bae58b..e2c0729be 100644 --- a/src/Gaufrette/File.php +++ b/src/Gaufrette/File.php @@ -78,7 +78,7 @@ public function getKey() * * @return string */ - public function getContent($metadata = array()) + public function getContent($metadata = []) { if (isset($this->content)) { return $this->content; @@ -140,7 +140,7 @@ public function setSize($size) * @return int The number of bytes that were written into the file, or * FALSE on failure */ - public function setContent($content, $metadata = array()) + public function setContent($content, $metadata = []) { $this->content = $content; $this->setMetadata($metadata); @@ -176,7 +176,7 @@ public function exists() * * @return bool TRUE on success */ - public function delete($metadata = array()) + public function delete($metadata = []) { $this->setMetadata($metadata); diff --git a/src/Gaufrette/Filesystem.php b/src/Gaufrette/Filesystem.php index 04860f573..e08d1921d 100644 --- a/src/Gaufrette/Filesystem.php +++ b/src/Gaufrette/Filesystem.php @@ -19,7 +19,7 @@ class Filesystem implements FilesystemInterface * * @var array */ - protected $fileRegister = array(); + protected $fileRegister = []; /** * @param Adapter $adapter A configured Adapter instance @@ -162,8 +162,8 @@ public function listKeys($prefix = '') return $this->adapter->listKeys($prefix); } - $dirs = array(); - $keys = array(); + $dirs = []; + $keys = []; foreach ($this->keys() as $key) { if (empty($prefix) || 0 === strpos($key, $prefix)) { @@ -175,10 +175,10 @@ public function listKeys($prefix = '') } } - return array( + return [ 'keys' => $keys, 'dirs' => $dirs, - ); + ]; } /** @@ -310,7 +310,7 @@ private function isFileInRegister($key) */ public function clearFileRegister() { - $this->fileRegister = array(); + $this->fileRegister = []; } /** diff --git a/src/Gaufrette/FilesystemInterface.php b/src/Gaufrette/FilesystemInterface.php index f4bb0f443..9a686ab8f 100644 --- a/src/Gaufrette/FilesystemInterface.php +++ b/src/Gaufrette/FilesystemInterface.php @@ -34,7 +34,6 @@ public function has($key); */ public function rename($sourceKey, $targetKey); - /** * Returns the file matching the specified key. * diff --git a/src/Gaufrette/FilesystemMap.php b/src/Gaufrette/FilesystemMap.php index b118f48e9..2a700dcaa 100644 --- a/src/Gaufrette/FilesystemMap.php +++ b/src/Gaufrette/FilesystemMap.php @@ -9,7 +9,7 @@ */ class FilesystemMap implements FilesystemMapInterface { - private $filesystems = array(); + private $filesystems = []; /** * Returns an array of all the registered filesystems where the key is the @@ -88,6 +88,6 @@ public function remove($name) */ public function clear() { - $this->filesystems = array(); + $this->filesystems = []; } } diff --git a/src/Gaufrette/Stream/InMemoryBuffer.php b/src/Gaufrette/Stream/InMemoryBuffer.php index 4fc279091..30ac9be62 100644 --- a/src/Gaufrette/Stream/InMemoryBuffer.php +++ b/src/Gaufrette/Stream/InMemoryBuffer.php @@ -89,7 +89,7 @@ public function write($data) } else { $before = substr($this->content, 0, $this->position); $after = $newNumBytes > $newPosition ? substr($this->content, $newPosition) : ''; - $this->content = $before.$data.$after; + $this->content = $before . $data . $after; } $this->position = $newPosition; @@ -111,12 +111,15 @@ public function seek($offset, $whence = SEEK_SET) switch ($whence) { case SEEK_SET: $this->position = $offset; + break; case SEEK_CUR: $this->position += $offset; + break; case SEEK_END: $this->position = $this->numBytes + $offset; + break; default: return false; @@ -159,7 +162,7 @@ public function stat() $isDirectory = $this->filesystem->isDirectory($this->key); $time = $this->filesystem->mtime($this->key); - $stats = array( + $stats = [ 'dev' => 1, 'ino' => 0, 'mode' => $isDirectory ? 16893 : 33204, @@ -173,7 +176,7 @@ public function stat() 'ctime' => $time, 'blksize' => -1, 'blocks' => -1, - ); + ]; return array_merge(array_values($stats), $stats); } diff --git a/src/Gaufrette/Stream/Local.php b/src/Gaufrette/Stream/Local.php index 6b1c49283..306119c5e 100644 --- a/src/Gaufrette/Stream/Local.php +++ b/src/Gaufrette/Stream/Local.php @@ -36,6 +36,7 @@ public function open(StreamMode $mode) if ($mode->allowsWrite() && !is_dir($baseDirPath)) { @mkdir($baseDirPath, $this->mkdirMode, true); } + try { $fileHandle = @fopen($this->path, $mode->getMode()); } catch (\Exception $e) { diff --git a/src/Gaufrette/StreamWrapper.php b/src/Gaufrette/StreamWrapper.php index b96076683..cc8a92ca0 100644 --- a/src/Gaufrette/StreamWrapper.php +++ b/src/Gaufrette/StreamWrapper.php @@ -243,25 +243,25 @@ public function stream_cast($castAs) protected function createStream($path) { $parts = array_merge( - array( + [ 'scheme' => null, 'host' => null, 'path' => null, 'query' => null, 'fragment' => null, - ), - parse_url($path) ?: array() + ], + parse_url($path) ?: [] ); $domain = $parts['host']; $key = substr($parts['path'], 1); if (null !== $parts['query']) { - $key .= '?'.$parts['query']; + $key .= '?' . $parts['query']; } if (null !== $parts['fragment']) { - $key .= '#'.$parts['fragment']; + $key .= '#' . $parts['fragment']; } if (empty($domain) || empty($key)) { diff --git a/src/Gaufrette/Util/Path.php b/src/Gaufrette/Util/Path.php index eb7e2a0e3..4dec5c50a 100644 --- a/src/Gaufrette/Util/Path.php +++ b/src/Gaufrette/Util/Path.php @@ -22,7 +22,7 @@ public static function normalize($path) $prefix = static::getAbsolutePrefix($path); $path = substr($path, strlen($prefix)); $parts = array_filter(explode('/', $path), 'strlen'); - $tokens = array(); + $tokens = []; foreach ($parts as $part) { switch ($part) { @@ -31,6 +31,7 @@ public static function normalize($path) case '..': if (0 !== count($tokens)) { array_pop($tokens); + continue 2; } elseif (!empty($prefix)) { continue 2; @@ -40,7 +41,7 @@ public static function normalize($path) } } - return $prefix.implode('/', $tokens); + return $prefix . implode('/', $tokens); } /** diff --git a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php index e641ce520..7115fef6f 100644 --- a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php +++ b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php @@ -9,7 +9,7 @@ class AwsS3Test extends FunctionalTestCase { /** @var int */ - static private $SDK_VERSION; + private static $SDK_VERSION; /** @var string */ private $bucket; @@ -64,6 +64,7 @@ public function tearDown() if (!$result->hasKey('Contents')) { $this->client->deleteBucket(['Bucket' => $this->bucket]); + return; } diff --git a/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php b/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php index 9ec350f72..e53e669e1 100644 --- a/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php +++ b/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php @@ -30,8 +30,8 @@ public function setUp() $connection = sprintf('BlobEndpoint=http://%1$s.blob.core.windows.net/;AccountName=%1$s;AccountKey=%2$s', $account, $key); - $this->container = uniqid($containerName); - $this->adapter = new AzureBlobStorage(new BlobProxyFactory($connection), $this->container, true); + $this->container = uniqid($containerName); + $this->adapter = new AzureBlobStorage(new BlobProxyFactory($connection), $this->container, true); $this->filesystem = new Filesystem($this->adapter); } diff --git a/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php b/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php index e9d589ad8..fc251927d 100644 --- a/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php +++ b/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php @@ -21,10 +21,10 @@ public function setUp() $schema = $this->connection->getSchemaManager()->createSchema(); $table = $schema->createTable('gaufrette'); - $table->addColumn('key', 'string', array('unique' => true)); + $table->addColumn('key', 'string', ['unique' => true]); $table->addColumn('content', 'blob'); $table->addColumn('mtime', 'integer'); - $table->addColumn('checksum', 'string', array('length' => 32)); + $table->addColumn('checksum', 'string', ['length' => 32]); // Generates the SQL from the defined schema and execute each line array_map([$this->connection, 'exec'], $schema->toSql($this->connection->getDatabasePlatform())); @@ -42,8 +42,8 @@ public function tearDown() } /** - * @test - */ + * @test + */ public function shouldListKeys() { $this->filesystem->write('foo/foobar/bar.txt', 'data'); @@ -68,25 +68,25 @@ public function shouldListKeys() $keys = $this->filesystem->listKeys('foo/foob'); $this->assertEquals( - array('foo/foobar/bar.txt'), + ['foo/foobar/bar.txt'], $keys['keys'], '', 0, 10, true); $keys = $this->filesystem->listKeys('foo/'); $this->assertEquals( - array('foo/foobar/bar.txt', 'foo/bar/buzz.txt'), + ['foo/foobar/bar.txt', 'foo/bar/buzz.txt'], $keys['keys'], '', 0, 10, true); $keys = $this->filesystem->listKeys('foo'); $this->assertEquals( - array('foo/foobar/bar.txt', 'foo/bar/buzz.txt', 'foobarbuz.txt', 'foo'), + ['foo/foobar/bar.txt', 'foo/bar/buzz.txt', 'foobarbuz.txt', 'foo'], $keys['keys'], '', 0, 10, true); $keys = $this->filesystem->listKeys('fooz'); $this->assertEquals( - array(), + [], $keys['keys'], '', 0, 10, true); } diff --git a/tests/Gaufrette/Functional/Adapter/DropboxTest.php b/tests/Gaufrette/Functional/Adapter/DropboxTest.php index ff0a224ec..6058ceb82 100644 --- a/tests/Gaufrette/Functional/Adapter/DropboxTest.php +++ b/tests/Gaufrette/Functional/Adapter/DropboxTest.php @@ -2,8 +2,6 @@ namespace Gaufrette\Functional\Adapter; -use Gaufrette\Adapter\Dropbox; - class DropboxTest extends FunctionalTestCase { } diff --git a/tests/Gaufrette/Functional/Adapter/FtpTest.php b/tests/Gaufrette/Functional/Adapter/FtpTest.php index 8b623753e..59f1da03b 100644 --- a/tests/Gaufrette/Functional/Adapter/FtpTest.php +++ b/tests/Gaufrette/Functional/Adapter/FtpTest.php @@ -9,11 +9,11 @@ class FtpTest extends FunctionalTestCase { public function setUp() { - $host = getenv('FTP_HOST'); - $port = getenv('FTP_PORT'); - $user = getenv('FTP_USER'); + $host = getenv('FTP_HOST'); + $port = getenv('FTP_PORT'); + $user = getenv('FTP_USER'); $password = getenv('FTP_PASSWORD'); - $baseDir = getenv('FTP_BASE_DIR'); + $baseDir = getenv('FTP_BASE_DIR'); if ($user === false || $password === false || $host === false || $baseDir === false) { $this->markTestSkipped('Either FTP_HOST, FTP_USER, FTP_PASSWORD and/or FTP_BASE_DIR env variables are not defined.'); diff --git a/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php b/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php index 1e53e18fb..ec3dc518e 100644 --- a/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php +++ b/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php @@ -16,7 +16,7 @@ public function getAdapterName() { if (!preg_match('/\\\\(\w+)Test$/', get_class($this), $matches)) { throw new \RuntimeException(sprintf( - 'Unable to guess filesystem name from class "%s", '. + 'Unable to guess filesystem name from class "%s", ' . 'please override the ->getAdapterName() method.', get_class($this) )); @@ -160,7 +160,7 @@ public function shouldDeleteFile() */ public function shouldFetchKeys() { - $this->assertEquals(array(), $this->filesystem->keys()); + $this->assertEquals([], $this->filesystem->keys()); $this->filesystem->write('foo', 'Some content'); $this->filesystem->write('bar', 'Some content'); @@ -169,7 +169,7 @@ public function shouldFetchKeys() $actualKeys = $this->filesystem->keys(); $this->assertCount(3, $actualKeys); - foreach (array('foo', 'bar', 'baz') as $key) { + foreach (['foo', 'bar', 'baz'] as $key) { $this->assertContains($key, $actualKeys); } } diff --git a/tests/Gaufrette/Functional/Adapter/GoogleCloudStorageTest.php b/tests/Gaufrette/Functional/Adapter/GoogleCloudStorageTest.php index 7f8d9d4f2..49cb1b039 100644 --- a/tests/Gaufrette/Functional/Adapter/GoogleCloudStorageTest.php +++ b/tests/Gaufrette/Functional/Adapter/GoogleCloudStorageTest.php @@ -38,7 +38,7 @@ public function shouldWriteAndReadWithDirectory() /** @var \Gaufrette\Adapter\GoogleCloudStorage $adapter */ $adapter = $this->filesystem->getAdapter(); $oldOptions = $adapter->getOptions(); - $adapter->setOptions(array('directory' => 'Gaufrette')); + $adapter->setOptions(['directory' => 'Gaufrette']); $this->assertEquals(12, $this->filesystem->write('foo', 'Some content')); $this->assertEquals(13, $this->filesystem->write('test/subdir/foo', 'Some content1', true)); @@ -60,28 +60,28 @@ public function shouldSetMetadataCorrectly() /** @var \Gaufrette\Adapter\GoogleCloudStorage $adapter */ $adapter = $this->filesystem->getAdapter(); - $adapter->setMetadata('metadata.txt', array( + $adapter->setMetadata('metadata.txt', [ 'CacheControl' => 'public, maxage=7200', 'ContentDisposition' => 'attachment; filename="test.txt"', 'ContentEncoding' => 'identity', 'ContentLanguage' => 'en', 'Colour' => 'Yellow', - )); + ]); $this->assertEquals(12, $this->filesystem->write('metadata.txt', 'Some content', true)); $reflectionObject = new \ReflectionObject($adapter); $reflectionMethod = $reflectionObject->getMethod('getObjectData'); $reflectionMethod->setAccessible(true); - $metadata = $reflectionMethod->invoke($adapter, array('metadata.txt')); + $metadata = $reflectionMethod->invoke($adapter, ['metadata.txt']); $this->assertEquals('public, maxage=7200', $metadata->cacheControl); $this->assertEquals('attachment; filename="test.txt"', $metadata->contentDisposition); $this->assertEquals('identity', $metadata->contentEncoding); $this->assertEquals('en', $metadata->contentLanguage); - $this->assertEquals(array( + $this->assertEquals([ 'Colour' => 'Yellow', - ), $metadata->metadata); + ], $metadata->metadata); $this->filesystem->delete('metadata.txt'); } diff --git a/tests/Gaufrette/Functional/Adapter/GridFSTest.php b/tests/Gaufrette/Functional/Adapter/GridFSTest.php index 5f15a2ec5..cf96c241e 100644 --- a/tests/Gaufrette/Functional/Adapter/GridFSTest.php +++ b/tests/Gaufrette/Functional/Adapter/GridFSTest.php @@ -52,25 +52,25 @@ public function shouldListKeys() $keys = $this->filesystem->listKeys('foo/foob'); $this->assertEquals( - array('foo/foobar/bar.txt'), + ['foo/foobar/bar.txt'], $keys['keys'], '', 0, 10, true); $keys = $this->filesystem->listKeys('foo/'); $this->assertEquals( - array('foo/foobar/bar.txt', 'foo/bar/buzz.txt'), + ['foo/foobar/bar.txt', 'foo/bar/buzz.txt'], $keys['keys'], '', 0, 10, true); $keys = $this->filesystem->listKeys('foo'); $this->assertEquals( - array('foo/foobar/bar.txt', 'foo/bar/buzz.txt', 'foobarbuz.txt', 'foo'), + ['foo/foobar/bar.txt', 'foo/bar/buzz.txt', 'foobarbuz.txt', 'foo'], $keys['keys'], '', 0, 10, true); $keys = $this->filesystem->listKeys('fooz'); $this->assertEquals( - array(), + [], $keys['keys'], '', 0, 10, true); } @@ -84,7 +84,7 @@ public function testMetadataRetrieveAfterWrite() //Create local copy of fileadapter $fileadpt = clone $this->filesystem->getAdapter(); - $this->filesystem->getAdapter()->setMetadata('metadatatest', array('testing' => true)); + $this->filesystem->getAdapter()->setMetadata('metadatatest', ['testing' => true]); $this->filesystem->write('metadatatest', 'test'); $this->assertEquals($this->filesystem->getAdapter()->getMetadata('metadatatest'), $fileadpt->getMetadata('metadatatest')); @@ -108,6 +108,6 @@ public function testRetrieveWithoutMetadata() { $this->filesystem->write('no-metadata.txt', 'content'); - $this->assertEquals(array(), $this->filesystem->getAdapter()->getMetadata('no-metadata.txt')); + $this->assertEquals([], $this->filesystem->getAdapter()->getMetadata('no-metadata.txt')); } } diff --git a/tests/Gaufrette/Functional/Adapter/MogileFSTest.php b/tests/Gaufrette/Functional/Adapter/MogileFSTest.php index 83fc7f48b..d00c4c00c 100644 --- a/tests/Gaufrette/Functional/Adapter/MogileFSTest.php +++ b/tests/Gaufrette/Functional/Adapter/MogileFSTest.php @@ -2,8 +2,6 @@ namespace Gaufrette\Functional\Adapter; -use Gaufrette\Adapter\MogileFS; - class MogileFSTest extends FunctionalTestCase { /** diff --git a/tests/Gaufrette/Functional/Adapter/PhpseclibSftpTest.php b/tests/Gaufrette/Functional/Adapter/PhpseclibSftpTest.php index 3a996da6d..0a12b67e1 100644 --- a/tests/Gaufrette/Functional/Adapter/PhpseclibSftpTest.php +++ b/tests/Gaufrette/Functional/Adapter/PhpseclibSftpTest.php @@ -16,11 +16,11 @@ class PhpseclibSftpTest extends FunctionalTestCase public function setUp() { - $host = getenv('SFTP_HOST'); - $port = getenv('SFTP_PORT') ?: 22; - $user = getenv('SFTP_USER'); + $host = getenv('SFTP_HOST'); + $port = getenv('SFTP_PORT') ?: 22; + $user = getenv('SFTP_USER'); $password = getenv('SFTP_PASSWORD'); - $baseDir = getenv('SFTP_BASE_DIR'); + $baseDir = getenv('SFTP_BASE_DIR'); if ($host === false || $user === false || $password === false || $baseDir === false) { $this->markTestSkipped('Either SFTP_HOST, SFTP_USER, SFTP_PASSWORD and/or SFTP_BASE_DIR env variables are not defined.'); diff --git a/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php b/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php index d16fd042b..7805a22c4 100644 --- a/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php +++ b/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php @@ -207,14 +207,14 @@ public function shouldCreateNewFile($mode) public static function modesProvider() { - return array( - array('w'), - array('a+'), - array('w+'), - array('ab+'), - array('wb'), - array('wb+') - ); + return [ + ['w'], + ['a+'], + ['w+'], + ['ab+'], + ['wb'], + ['wb+'], + ]; } protected function registerLocalFilesystemInStream() diff --git a/tests/Gaufrette/Functional/FileStream/InMemoryBufferTest.php b/tests/Gaufrette/Functional/FileStream/InMemoryBufferTest.php index 5f00fc36e..343f1acab 100644 --- a/tests/Gaufrette/Functional/FileStream/InMemoryBufferTest.php +++ b/tests/Gaufrette/Functional/FileStream/InMemoryBufferTest.php @@ -9,7 +9,7 @@ class InMemoryBufferTest extends FunctionalTestCase { public function setUp() { - $this->filesystem = new Filesystem(new InMemoryAdapter(array())); + $this->filesystem = new Filesystem(new InMemoryAdapter([])); $this->registerLocalFilesystemInStream(); } diff --git a/tests/Gaufrette/Functional/FileStream/LocalTest.php b/tests/Gaufrette/Functional/FileStream/LocalTest.php index c05c7fb0d..8ef0d4963 100644 --- a/tests/Gaufrette/Functional/FileStream/LocalTest.php +++ b/tests/Gaufrette/Functional/FileStream/LocalTest.php @@ -11,8 +11,8 @@ class LocalTest extends FunctionalTestCase public function setUp() { - $this->directory = __DIR__.DIRECTORY_SEPARATOR.'filesystem'; - @mkdir($this->directory.DIRECTORY_SEPARATOR.'subdir', 0777, true); + $this->directory = __DIR__ . DIRECTORY_SEPARATOR . 'filesystem'; + @mkdir($this->directory . DIRECTORY_SEPARATOR . 'subdir', 0777, true); umask(0002); $this->filesystem = new Filesystem(new LocalAdapter($this->directory, true, 0770)); From 0c31221aa3e5a3a612b2590dc942fcd15d82da6d Mon Sep 17 00:00:00 2001 From: peter279k Date: Tue, 22 Oct 2019 16:36:45 +0800 Subject: [PATCH 383/426] Test enhancement --- composer.json | 2 +- .../Gaufrette/Functional/Adapter/ApcTest.php | 4 +- .../Functional/Adapter/AwsS3Test.php | 57 ++++++++++++++----- .../Adapter/AzureBlobStorageTest.php | 4 +- .../AzureMultiContainerBlobStorageTest.php | 4 +- .../Functional/Adapter/CacheTest.php | 2 +- .../Functional/Adapter/DoctrineDbalTest.php | 4 +- .../Gaufrette/Functional/Adapter/FtpTest.php | 4 +- .../Functional/Adapter/FunctionalTestCase.php | 4 +- .../Functional/Adapter/GridFSTest.php | 8 +-- .../Functional/Adapter/LocalTest.php | 4 +- .../Functional/Adapter/MogileFSTest.php | 2 + .../Functional/Adapter/OpenCloudTest.php | 4 +- .../Functional/Adapter/PhpseclibSftpTest.php | 4 +- .../Functional/Adapter/SafeLocalTest.php | 4 +- .../Gaufrette/Functional/Adapter/SftpTest.php | 2 +- .../Gaufrette/Functional/Adapter/ZipTest.php | 4 +- .../FileStream/InMemoryBufferTest.php | 2 +- .../Functional/FileStream/LocalTest.php | 9 ++- 19 files changed, 82 insertions(+), 46 deletions(-) diff --git a/composer.json b/composer.json index d1d2488aa..7a6c8fe63 100644 --- a/composer.json +++ b/composer.json @@ -31,7 +31,7 @@ "doctrine/dbal": ">=2.3", "dropbox-php/dropbox-php": "*", "phpunit/phpunit": "~7.5", - "mikey179/vfsStream": "~1.2.0", + "mikey179/vfsstream": "~1.2.0", "league/flysystem": "~1.0", "mongodb/mongodb": "^1.1", "microsoft/azure-storage-blob": "^1.0", diff --git a/tests/Gaufrette/Functional/Adapter/ApcTest.php b/tests/Gaufrette/Functional/Adapter/ApcTest.php index 581b77001..b46888d8e 100644 --- a/tests/Gaufrette/Functional/Adapter/ApcTest.php +++ b/tests/Gaufrette/Functional/Adapter/ApcTest.php @@ -7,7 +7,7 @@ class ApcTest extends FunctionalTestCase { - public function setUp() + protected function setUp() { if (!extension_loaded('apc')) { return $this->markTestSkipped('The APC extension is not available.'); @@ -20,7 +20,7 @@ public function setUp() $this->filesystem = new Filesystem(new Apc('gaufrette-test.')); } - public function tearDown() + protected function tearDown() { parent::tearDown(); if (extension_loaded('apc')) { diff --git a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php index 7115fef6f..1a786f4a3 100644 --- a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php +++ b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php @@ -17,7 +17,7 @@ class AwsS3Test extends FunctionalTestCase /** @var S3Client */ private $client; - public function setUp() + protected function setUp() { $key = getenv('AWS_KEY'); $secret = getenv('AWS_SECRET'); @@ -54,7 +54,7 @@ public function setUp() $this->createFilesystem(['create' => true]); } - public function tearDown() + protected function tearDown() { if ($this->client === null || !$this->client->doesBucketExist($this->bucket)) { return; @@ -81,31 +81,44 @@ private function createFilesystem(array $adapterOptions = []) } /** + * @test * @expectedException \RuntimeException */ - public function testThrowsExceptionIfBucketMissingAndNotCreating() + public function shouldThrowExceptionIfBucketMissingAndNotCreating() { $this->createFilesystem(); $this->filesystem->read('foo'); } - public function testWritesObjects() + /** + * @test + */ + public function shouldWriteObjects() { $this->assertEquals(7, $this->filesystem->write('foo', 'testing')); } - public function testChecksForObjectExistence() + /** + * @test + */ + public function shouldCheckForObjectExistence() { $this->filesystem->write('foo', ''); $this->assertTrue($this->filesystem->has('foo')); } - public function testGetsObjectUrls() + /** + * @test + */ + public function shouldGetObjectUrls() { $this->assertNotEmpty($this->filesystem->getAdapter()->getUrl('foo')); } - public function testChecksForObjectExistenceWithDirectory() + /** + * @test + */ + public function shouldCheckForObjectExistenceWithDirectory() { $this->createFilesystem(['directory' => 'bar', 'create' => true]); $this->filesystem->write('foo', ''); @@ -113,20 +126,29 @@ public function testChecksForObjectExistenceWithDirectory() $this->assertTrue($this->filesystem->has('foo')); } - public function testGetsObjectUrlsWithDirectory() + /** + * @test + */ + public function shouldGetObjectUrlsWithDirectory() { $this->createFilesystem(['directory' => 'bar']); $this->assertNotEmpty($this->filesystem->getAdapter()->getUrl('foo')); } - public function testListKeysWithoutDirectory() + /** + * @test + */ + public function shouldListKeysWithoutDirectory() { $this->assertEquals([], $this->filesystem->listKeys()); $this->filesystem->write('test.txt', 'some content'); $this->assertEquals(['test.txt'], $this->filesystem->listKeys()); } - public function testListKeysWithDirectory() + /** + * @test + */ + public function shouldListKeysWithDirectory() { $this->createFilesystem(['create' => true, 'directory' => 'root/']); $this->filesystem->write('test.txt', 'some content'); @@ -134,20 +156,29 @@ public function testListKeysWithDirectory() $this->assertTrue($this->filesystem->has('test.txt')); } - public function testKeysWithoutDirectory() + /** + * @test + */ + public function shouldGetKeysWithoutDirectory() { $this->filesystem->write('test.txt', 'some content'); $this->assertEquals(['test.txt'], $this->filesystem->keys()); } - public function testKeysWithDirectory() + /** + * @test + */ + public function shouldGetKeysWithDirectory() { $this->createFilesystem(['create' => true, 'directory' => 'root/']); $this->filesystem->write('test.txt', 'some content'); $this->assertEquals(['test.txt'], $this->filesystem->keys()); } - public function testUploadWithGivenContentType() + /** + * @test + */ + public function shouldUploadWithGivenContentType() { /** @var AwsS3 $adapter */ $adapter = $this->filesystem->getAdapter(); diff --git a/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php b/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php index e53e669e1..e7e2026f3 100644 --- a/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php +++ b/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php @@ -18,7 +18,7 @@ class AzureBlobStorageTest extends FunctionalTestCase /** @var AzureBlobStorage */ private $adapter; - public function setUp() + protected function setUp() { $account = getenv('AZURE_ACCOUNT'); $key = getenv('AZURE_KEY'); @@ -35,7 +35,7 @@ public function setUp() $this->filesystem = new Filesystem($this->adapter); } - public function tearDown() + protected function tearDown() { if ($this->adapter === null) { return; diff --git a/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php index c73bfc910..9a3e2c176 100644 --- a/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php +++ b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php @@ -17,7 +17,7 @@ class AzureMultiContainerBlobStorageTest extends FunctionalTestCase private $containers = []; - public function setUp() + protected function setUp() { $this->markTestSkipped(__CLASS__ . ' is flaky.'); @@ -247,7 +247,7 @@ private function createUniqueContainerName($prefix) return $container; } - public function tearDown() + protected function tearDown() { foreach ($this->containers as $container) { $this->adapter->deleteContainer($container); diff --git a/tests/Gaufrette/Functional/Adapter/CacheTest.php b/tests/Gaufrette/Functional/Adapter/CacheTest.php index 8c3835b29..8138fbe70 100644 --- a/tests/Gaufrette/Functional/Adapter/CacheTest.php +++ b/tests/Gaufrette/Functional/Adapter/CacheTest.php @@ -8,7 +8,7 @@ class CacheTest extends FunctionalTestCase { - public function setUp() + protected function setUp() { $this->filesystem = new Filesystem(new Cache(new InMemory(), new InMemory())); } diff --git a/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php b/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php index fc251927d..67b66c924 100644 --- a/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php +++ b/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php @@ -11,7 +11,7 @@ class DoctrineDbalTest extends FunctionalTestCase /** @var \Doctrine\DBAL\Connection */ private $connection; - public function setUp() + protected function setUp() { $this->connection = DriverManager::getConnection([ 'driver' => 'pdo_sqlite', @@ -32,7 +32,7 @@ public function setUp() $this->filesystem = new Filesystem(new DoctrineDbal($this->connection, 'gaufrette')); } - public function tearDown() + protected function tearDown() { $schemaManager = $this->connection->getSchemaManager(); diff --git a/tests/Gaufrette/Functional/Adapter/FtpTest.php b/tests/Gaufrette/Functional/Adapter/FtpTest.php index 59f1da03b..9aec24ff6 100644 --- a/tests/Gaufrette/Functional/Adapter/FtpTest.php +++ b/tests/Gaufrette/Functional/Adapter/FtpTest.php @@ -7,7 +7,7 @@ class FtpTest extends FunctionalTestCase { - public function setUp() + protected function setUp() { $host = getenv('FTP_HOST'); $port = getenv('FTP_PORT'); @@ -23,7 +23,7 @@ public function setUp() $this->filesystem = new Filesystem($adapter); } - public function tearDown() + protected function tearDown() { if (null === $this->filesystem) { return; diff --git a/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php b/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php index ec3dc518e..3728607aa 100644 --- a/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php +++ b/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php @@ -25,7 +25,7 @@ public function getAdapterName() return $matches[1]; } - public function setUp() + protected function setUp() { $basename = $this->getAdapterName(); $filename = sprintf( @@ -48,7 +48,7 @@ public function setUp() $this->filesystem = new Filesystem($adapter); } - public function tearDown() + protected function tearDown() { if (null === $this->filesystem) { return; diff --git a/tests/Gaufrette/Functional/Adapter/GridFSTest.php b/tests/Gaufrette/Functional/Adapter/GridFSTest.php index cf96c241e..041dd2183 100644 --- a/tests/Gaufrette/Functional/Adapter/GridFSTest.php +++ b/tests/Gaufrette/Functional/Adapter/GridFSTest.php @@ -8,7 +8,7 @@ class GridFSTest extends FunctionalTestCase { - public function setUp() + protected function setUp() { $uri = getenv('MONGO_URI'); $dbname = getenv('MONGO_DBNAME'); @@ -79,7 +79,7 @@ public function shouldListKeys() * @test * Tests metadata written to GridFS can be retrieved after writing */ - public function testMetadataRetrieveAfterWrite() + public function shouldRetrieveMetadataAfterWrite() { //Create local copy of fileadapter $fileadpt = clone $this->filesystem->getAdapter(); @@ -94,7 +94,7 @@ public function testMetadataRetrieveAfterWrite() * @test * Test to see if filesize works */ - public function testSize() + public function shouldGetSize() { $this->filesystem->write('sizetest.txt', 'data'); $this->assertEquals(4, $this->filesystem->size('sizetest.txt')); @@ -104,7 +104,7 @@ public function testSize() * @test * Should retrieve empty metadata w/o errors */ - public function testRetrieveWithoutMetadata() + public function shouldRetrieveEmptyMetadata() { $this->filesystem->write('no-metadata.txt', 'content'); diff --git a/tests/Gaufrette/Functional/Adapter/LocalTest.php b/tests/Gaufrette/Functional/Adapter/LocalTest.php index 4958fdefb..991faa974 100644 --- a/tests/Gaufrette/Functional/Adapter/LocalTest.php +++ b/tests/Gaufrette/Functional/Adapter/LocalTest.php @@ -9,7 +9,7 @@ class LocalTest extends FunctionalTestCase { private $directory; - public function setUp() + protected function setUp() { $this->directory = sprintf('%s/filesystem', str_replace('\\', '/', __DIR__)); @@ -20,7 +20,7 @@ public function setUp() $this->filesystem = new Filesystem(new Local($this->directory)); } - public function tearDown() + protected function tearDown() { $adapter = $this->filesystem->getAdapter(); diff --git a/tests/Gaufrette/Functional/Adapter/MogileFSTest.php b/tests/Gaufrette/Functional/Adapter/MogileFSTest.php index d00c4c00c..a1b4ddcf9 100644 --- a/tests/Gaufrette/Functional/Adapter/MogileFSTest.php +++ b/tests/Gaufrette/Functional/Adapter/MogileFSTest.php @@ -5,6 +5,7 @@ class MogileFSTest extends FunctionalTestCase { /** + * @test * @group functional */ public function shouldGetMtime() @@ -13,6 +14,7 @@ public function shouldGetMtime() } /** + * @test * @group functional */ public function shouldGetMtimeNonExistingFile() diff --git a/tests/Gaufrette/Functional/Adapter/OpenCloudTest.php b/tests/Gaufrette/Functional/Adapter/OpenCloudTest.php index ae459e4b6..54fd2ddf3 100644 --- a/tests/Gaufrette/Functional/Adapter/OpenCloudTest.php +++ b/tests/Gaufrette/Functional/Adapter/OpenCloudTest.php @@ -14,7 +14,7 @@ class OpenCloudTest extends FunctionalTestCase /** @var string */ private $container; - public function setUp() + protected function setUp() { $username = getenv('RACKSPACE_USER') ?: ''; $apiKey = getenv('RACKSPACE_APIKEY') ?: ''; @@ -49,7 +49,7 @@ public function setUp() $this->filesystem = new Filesystem($adapter); } - public function tearDown() + protected function tearDown() { if ($this->filesystem === null) { return; diff --git a/tests/Gaufrette/Functional/Adapter/PhpseclibSftpTest.php b/tests/Gaufrette/Functional/Adapter/PhpseclibSftpTest.php index 0a12b67e1..af48dda58 100644 --- a/tests/Gaufrette/Functional/Adapter/PhpseclibSftpTest.php +++ b/tests/Gaufrette/Functional/Adapter/PhpseclibSftpTest.php @@ -14,7 +14,7 @@ class PhpseclibSftpTest extends FunctionalTestCase /** @var string */ private $baseDir; - public function setUp() + protected function setUp() { $host = getenv('SFTP_HOST'); $port = getenv('SFTP_PORT') ?: 22; @@ -34,7 +34,7 @@ public function setUp() $this->filesystem = new Filesystem(new PhpseclibSftp($this->sftp, $this->baseDir, true)); } - public function tearDown() + protected function tearDown() { if (!isset($this->sftp)) { return; diff --git a/tests/Gaufrette/Functional/Adapter/SafeLocalTest.php b/tests/Gaufrette/Functional/Adapter/SafeLocalTest.php index efe299e8e..5c1f198c2 100644 --- a/tests/Gaufrette/Functional/Adapter/SafeLocalTest.php +++ b/tests/Gaufrette/Functional/Adapter/SafeLocalTest.php @@ -7,7 +7,7 @@ class SafeLocalTest extends FunctionalTestCase { - public function setUp() + protected function setUp() { if (!file_exists($this->getDirectory())) { mkdir($this->getDirectory()); @@ -16,7 +16,7 @@ public function setUp() $this->filesystem = new Filesystem(new SafeLocal($this->getDirectory())); } - public function tearDown() + protected function tearDown() { foreach ($this->filesystem->keys() as $key) { $this->filesystem->delete($key); diff --git a/tests/Gaufrette/Functional/Adapter/SftpTest.php b/tests/Gaufrette/Functional/Adapter/SftpTest.php index 974e59029..95841083c 100644 --- a/tests/Gaufrette/Functional/Adapter/SftpTest.php +++ b/tests/Gaufrette/Functional/Adapter/SftpTest.php @@ -4,7 +4,7 @@ class SftpTest extends FunctionalTestCase { - public function setUp() + protected function setUp() { if (!extension_loaded('ssh2')) { $this->markTestSkipped('Extension ssh2 not loaded'); diff --git a/tests/Gaufrette/Functional/Adapter/ZipTest.php b/tests/Gaufrette/Functional/Adapter/ZipTest.php index cc59d4e72..dc131f52b 100644 --- a/tests/Gaufrette/Functional/Adapter/ZipTest.php +++ b/tests/Gaufrette/Functional/Adapter/ZipTest.php @@ -7,7 +7,7 @@ class ZipTest extends FunctionalTestCase { - public function setUp() + protected function setUp() { if (!extension_loaded('zip')) { return $this->markTestSkipped('The zip extension is not available.'); @@ -20,7 +20,7 @@ public function setUp() $this->filesystem = new Filesystem(new Zip(__DIR__ . '/test.zip')); } - public function tearDown() + protected function tearDown() { parent::tearDown(); diff --git a/tests/Gaufrette/Functional/FileStream/InMemoryBufferTest.php b/tests/Gaufrette/Functional/FileStream/InMemoryBufferTest.php index 343f1acab..3b143163b 100644 --- a/tests/Gaufrette/Functional/FileStream/InMemoryBufferTest.php +++ b/tests/Gaufrette/Functional/FileStream/InMemoryBufferTest.php @@ -7,7 +7,7 @@ class InMemoryBufferTest extends FunctionalTestCase { - public function setUp() + protected function setUp() { $this->filesystem = new Filesystem(new InMemoryAdapter([])); diff --git a/tests/Gaufrette/Functional/FileStream/LocalTest.php b/tests/Gaufrette/Functional/FileStream/LocalTest.php index 8ef0d4963..1f687bb3c 100644 --- a/tests/Gaufrette/Functional/FileStream/LocalTest.php +++ b/tests/Gaufrette/Functional/FileStream/LocalTest.php @@ -9,7 +9,7 @@ class LocalTest extends FunctionalTestCase { protected $directory; - public function setUp() + protected function setUp() { $this->directory = __DIR__ . DIRECTORY_SEPARATOR . 'filesystem'; @mkdir($this->directory . DIRECTORY_SEPARATOR . 'subdir', 0777, true); @@ -19,7 +19,10 @@ public function setUp() $this->registerLocalFilesystemInStream(); } - public function testDirectoryChmod() + /** + * @test + */ + public function shouldChmodDirectory() { if (strtolower(substr(PHP_OS, 0, 3)) === 'win') { $this->markTestSkipped('Chmod and umask are not available on Windows.'); @@ -32,7 +35,7 @@ public function testDirectoryChmod() $this->assertEquals('0770', substr(sprintf('%o', $perms), -4)); } - public function tearDown() + protected function tearDown() { $adapter = $this->filesystem->getAdapter(); From 3762238df1931af466421df8e820467cae22a979 Mon Sep 17 00:00:00 2001 From: Rafal Gembalik Date: Thu, 19 Dec 2019 14:51:44 +0100 Subject: [PATCH 384/426] Add MimeTypeProvider to AzureBlobStorage. Also, fix Azure test cases by providing https url. Signed-off-by: Rafal Gembalik --- src/Gaufrette/Adapter/AzureBlobStorage.php | 21 ++++++++++++++++++- .../Adapter/AzureBlobStorageTest.php | 15 ++++++++++++- .../AzureMultiContainerBlobStorageTest.php | 16 +++++++++++++- 3 files changed, 49 insertions(+), 3 deletions(-) diff --git a/src/Gaufrette/Adapter/AzureBlobStorage.php b/src/Gaufrette/Adapter/AzureBlobStorage.php index 1bd3969fd..256453b94 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage.php @@ -20,7 +20,7 @@ * @author Luciano Mammino * @author Paweł Czyżewski */ -class AzureBlobStorage implements Adapter, MetadataSupporter, SizeCalculator, ChecksumCalculator +class AzureBlobStorage implements Adapter, MetadataSupporter, SizeCalculator, ChecksumCalculator, MimeTypeProvider { /** * Error constants. @@ -332,6 +332,25 @@ public function size($key) } } + /** + * {@inheritdoc} + */ + public function mimeType($key) + { + $this->init(); + list($containerName, $key) = $this->tokenizeKey($key); + + try { + $properties = $this->blobProxy->getBlobProperties($containerName, $key); + + return $properties->getProperties()->getContentType(); + } catch (ServiceException $e) { + $this->failIfContainerNotFound($e, sprintf('read content mime type for key "%s"', $key), $containerName); + + return false; + } + } + /** * {@inheritdoc} */ diff --git a/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php b/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php index e7e2026f3..024880589 100644 --- a/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php +++ b/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php @@ -28,13 +28,26 @@ protected function setUp() $this->markTestSkipped('Either AZURE_ACCOUNT, AZURE_KEY and/or AZURE_CONTAINER env variables are not defined.'); } - $connection = sprintf('BlobEndpoint=http://%1$s.blob.core.windows.net/;AccountName=%1$s;AccountKey=%2$s', $account, $key); + $connection = sprintf('BlobEndpoint=https://%1$s.blob.core.windows.net/;AccountName=%1$s;AccountKey=%2$s', $account, $key); $this->container = uniqid($containerName); $this->adapter = new AzureBlobStorage(new BlobProxyFactory($connection), $this->container, true); $this->filesystem = new Filesystem($this->adapter); } + /** + * @test + * @group functional + */ + public function shouldGetContentType() + { + $path = '/somefile'; + $content = 'Some content'; + $this->filesystem->write($path, $content); + + $this->assertEquals("text/plain", $this->filesystem->mimeType($path)); + } + protected function tearDown() { if ($this->adapter === null) { diff --git a/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php index 9a3e2c176..87cd50f57 100644 --- a/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php +++ b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php @@ -27,7 +27,7 @@ protected function setUp() $this->markTestSkipped('Either AZURE_ACCOUNT and/or AZURE_KEY env variables are not defined.'); } - $connection = sprintf('BlobEndpoint=http://%1$s.blob.core.windows.net/;AccountName=%1$s;AccountKey=%2$s', $account, $key); + $connection = sprintf('BlobEndpoint=https://%1$s.blob.core.windows.net/;AccountName=%1$s;AccountKey=%2$s', $account, $key); $this->adapter = new AzureBlobStorage(new BlobProxyFactory($connection)); $this->filesystem = new Filesystem($this->adapter); @@ -122,6 +122,20 @@ public function shouldGetMd5Hash() $this->assertEquals(\md5($content), $this->filesystem->checksum($path)); } + /** + * @test + * @group functional + */ + public function shouldGetContentType() + { + $path = $this->createUniqueContainerName('container') . '/foo'; + + $content = 'Some content'; + $this->filesystem->write($path, $content); + + $this->assertEquals('text/plain', $this->filesystem->mimeType($path)); + } + /** * @test * @group functional From d7ce6c1935abd767804ee759e62d35f33c8b578a Mon Sep 17 00:00:00 2001 From: Rafal Gembalik Date: Thu, 19 Dec 2019 15:04:28 +0100 Subject: [PATCH 385/426] Fix string quotes Signed-off-by: Rafal Gembalik --- tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php b/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php index 024880589..06d55755a 100644 --- a/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php +++ b/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php @@ -45,7 +45,7 @@ public function shouldGetContentType() $content = 'Some content'; $this->filesystem->write($path, $content); - $this->assertEquals("text/plain", $this->filesystem->mimeType($path)); + $this->assertEquals('text/plain', $this->filesystem->mimeType($path)); } protected function tearDown() From 70bd8b4a11926247e3537b0dc5364cddef0ce35a Mon Sep 17 00:00:00 2001 From: Nicolas MURE Date: Thu, 26 Dec 2019 15:49:40 +0100 Subject: [PATCH 386/426] prepare v0.9.0 release --- CHANGELOG.md | 16 ++++++++++++++++ composer.json | 2 +- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d48edc63..ab80bbea1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,19 @@ +v0.9.0 +====== + +## New features + +- Add MimeTypeProvider to AzureBlobStorage (#630) + +## Fixes + +- Fix docker setup for dev env (#580) +- Define env vars in .env file only (#615) +- Add PHP Coding Standard check step on CI (#617) +- PHPUnit tests enhancement (#627) + +Thank you @nicolasmure, @peter279k and @rgembalik for your contributions! + v0.8.3 ====== diff --git a/composer.json b/composer.json index 7a6c8fe63..2f46e5df6 100644 --- a/composer.json +++ b/composer.json @@ -64,7 +64,7 @@ }, "extra": { "branch-alias": { - "dev-master": "0.9.x-dev" + "dev-master": "0.10.x-dev" } } } From 8672d9d03b97324ae4bfe402ebe021f8a749e73d Mon Sep 17 00:00:00 2001 From: Kyoichi Ozaki Date: Tue, 31 Dec 2019 21:33:25 +0900 Subject: [PATCH 387/426] fix trigger_error argument order --- src/Gaufrette/Adapter/AwsS3.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index 853025a63..2f68ec19d 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -68,8 +68,8 @@ public function __construct(S3Client $service, $bucket, array $options = [], $de public function getUrl($key, array $options = []) { @trigger_error( - E_USER_DEPRECATED, - 'Using AwsS3::getUrl() method was deprecated since v0.4. Please chek gaufrette/extras package if you want this feature' + 'Using AwsS3::getUrl() method was deprecated since v0.4. Please chek gaufrette/extras package if you want this feature', + E_USER_DEPRECATED ); return $this->service->getObjectUrl( From ea3932bb2623294e49918de14551ec1254e61ffe Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Mon, 5 Oct 2020 11:28:43 +0200 Subject: [PATCH 388/426] Adding some Github actions (#641) --- .github/workflows/.editorconfig | 2 + .github/workflows/bc.yml | 10 ++ .github/workflows/static.yml | 27 +++ .travis.yml | 2 +- phpstan-baseline.neon | 292 ++++++++++++++++++++++++++++++++ phpstan.neon.dist | 7 + psalm.baseline.xml | 106 ++++++++++++ psalm.xml | 16 ++ 8 files changed, 461 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/.editorconfig create mode 100644 .github/workflows/bc.yml create mode 100644 .github/workflows/static.yml create mode 100644 phpstan-baseline.neon create mode 100644 phpstan.neon.dist create mode 100644 psalm.baseline.xml create mode 100644 psalm.xml diff --git a/.github/workflows/.editorconfig b/.github/workflows/.editorconfig new file mode 100644 index 000000000..7bd3346f2 --- /dev/null +++ b/.github/workflows/.editorconfig @@ -0,0 +1,2 @@ +[*.yml] +indent_size = 2 diff --git a/.github/workflows/bc.yml b/.github/workflows/bc.yml new file mode 100644 index 000000000..2d288edfe --- /dev/null +++ b/.github/workflows/bc.yml @@ -0,0 +1,10 @@ +on: [push, pull_request] +name: Roave +jobs: + roave_bc_check: + name: BC Check + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - name: Roave BC Check + uses: docker://nyholm/roave-bc-check-ga diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml new file mode 100644 index 000000000..5ce1d7ec0 --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,27 @@ +on: [push, pull_request] +name: Static analysis + +jobs: + phpstan: + name: PHPStan + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: PHPStan + uses: docker://oskarstark/phpstan-ga + env: + REQUIRE_DEV: true + with: + args: analyze --no-progress + + psalm: + name: Psalm + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v2 + + - name: Psalm + uses: docker://vimeo/psalm-github-actions diff --git a/.travis.yml b/.travis.yml index 1562ca093..6e25d546b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -41,6 +41,6 @@ install: - composer update --prefer-dist --no-progress --no-suggest --ansi script: - - php vendor/bin/php-cs-fixer fix --diff --dry-run --show-progress=none --verbose + - php vendor/bin/php-cs-fixer fix --diff --diff-format udiff --dry-run --show-progress=none --verbose - php vendor/bin/phpspec run -fpretty --verbose - php vendor/bin/phpunit --verbose diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 000000000..4e5d5d0aa --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,292 @@ +parameters: + ignoreErrors: + - + message: "#^Strict comparison using \\=\\=\\= between null and string will always evaluate to false\\.$#" + count: 1 + path: src/Gaufrette/Adapter/AmazonS3.php + + - + message: "#^Strict comparison using \\=\\=\\= between null and APCIterator will always evaluate to false\\.$#" + count: 1 + path: src/Gaufrette/Adapter/Apc.php + + - + message: "#^Method Aws\\\\S3\\\\S3Client\\:\\:getObjectUrl\\(\\) invoked with 4 parameters, 2 required\\.$#" + count: 1 + path: src/Gaufrette/Adapter/AwsS3.php + + - + message: "#^Call to function is_resource\\(\\) with string will always evaluate to false\\.$#" + count: 2 + path: src/Gaufrette/Adapter/AwsS3.php + + - + message: "#^Method Gaufrette\\\\Adapter\\\\AwsS3\\:\\:write\\(\\) should return bool\\|int but returns string\\.$#" + count: 1 + path: src/Gaufrette/Adapter/AwsS3.php + + - + message: "#^Method Gaufrette\\\\Adapter\\\\AwsS3\\:\\:size\\(\\) should return int but returns false\\.$#" + count: 1 + path: src/Gaufrette/Adapter/AwsS3.php + + - + message: "#^Method Gaufrette\\\\Adapter\\\\AwsS3\\:\\:mimeType\\(\\) should return string but returns false\\.$#" + count: 1 + path: src/Gaufrette/Adapter/AwsS3.php + + - + message: "#^Parameter \\$options of method Gaufrette\\\\Adapter\\\\AzureBlobStorage\\:\\:deleteContainer\\(\\) has invalid typehint type MicrosoftAzure\\\\Storage\\\\Blob\\\\Models\\\\DeleteContainerOptions\\.$#" + count: 2 + path: src/Gaufrette/Adapter/AzureBlobStorage.php + + - + message: "#^Parameter \\#2 \\$options of method MicrosoftAzure\\\\Storage\\\\Blob\\\\Internal\\\\IBlob\\:\\:deleteContainer\\(\\) expects MicrosoftAzure\\\\Storage\\\\Blob\\\\Models\\\\BlobServiceOptions\\|null, MicrosoftAzure\\\\Storage\\\\Blob\\\\Models\\\\DeleteContainerOptions\\|null given\\.$#" + count: 1 + path: src/Gaufrette/Adapter/AzureBlobStorage.php + + - + message: "#^Parameter \\#4 \\$options of method MicrosoftAzure\\\\Storage\\\\Blob\\\\Internal\\\\IBlob\\:\\:createBlockBlob\\(\\) expects MicrosoftAzure\\\\Storage\\\\Blob\\\\Models\\\\CreateBlockBlobOptions\\|null, MicrosoftAzure\\\\Storage\\\\Blob\\\\Models\\\\CreateBlobOptions given\\.$#" + count: 1 + path: src/Gaufrette/Adapter/AzureBlobStorage.php + + - + message: "#^Call to function is_resource\\(\\) with string will always evaluate to false\\.$#" + count: 1 + path: src/Gaufrette/Adapter/AzureBlobStorage.php + + - + message: "#^Method Gaufrette\\\\Adapter\\\\AzureBlobStorage\\:\\:write\\(\\) should return bool\\|int but returns string\\.$#" + count: 1 + path: src/Gaufrette/Adapter/AzureBlobStorage.php + + - + message: "#^Parameter \\#2 \\$prefix of method Gaufrette\\\\Adapter\\\\AzureBlobStorage\\:\\:fetchBlobs\\(\\) expects null, string given\\.$#" + count: 1 + path: src/Gaufrette/Adapter/AzureBlobStorage.php + + - + message: "#^Method Gaufrette\\\\Adapter\\\\AzureBlobStorage\\:\\:size\\(\\) should return int but returns false\\.$#" + count: 1 + path: src/Gaufrette/Adapter/AzureBlobStorage.php + + - + message: "#^Method Gaufrette\\\\Adapter\\\\AzureBlobStorage\\:\\:mimeType\\(\\) should return string but returns false\\.$#" + count: 1 + path: src/Gaufrette/Adapter/AzureBlobStorage.php + + - + message: "#^Method Gaufrette\\\\Adapter\\\\AzureBlobStorage\\:\\:checksum\\(\\) should return string but returns false\\.$#" + count: 1 + path: src/Gaufrette/Adapter/AzureBlobStorage.php + + - + message: "#^Cannot call method getBody\\(\\) on string\\.$#" + count: 1 + path: src/Gaufrette/Adapter/AzureBlobStorage.php + + - + message: "#^Strict comparison using \\!\\=\\= between null and null will always evaluate to false\\.$#" + count: 1 + path: src/Gaufrette/Adapter/AzureBlobStorage.php + + - + message: "#^Method Gaufrette\\\\Adapter\\\\Cache\\:\\:getMetadata\\(\\) should return array but returns false\\.$#" + count: 1 + path: src/Gaufrette/Adapter/Cache.php + + - + message: "#^Property Gaufrette\\\\Adapter\\\\Flysystem\\:\\:\\$config has unknown class Gaufrette\\\\Adapter\\\\Config as its type\\.$#" + count: 1 + path: src/Gaufrette/Adapter/Flysystem.php + + - + message: "#^Parameter \\$config of method Gaufrette\\\\Adapter\\\\Flysystem\\:\\:__construct\\(\\) has invalid typehint type Gaufrette\\\\Adapter\\\\Config\\.$#" + count: 1 + path: src/Gaufrette/Adapter/Flysystem.php + + - + message: "#^Property Gaufrette\\\\Adapter\\\\Flysystem\\:\\:\\$config \\(Gaufrette\\\\Adapter\\\\Config\\) does not accept League\\\\Flysystem\\\\Config\\.$#" + count: 1 + path: src/Gaufrette/Adapter/Flysystem.php + + - + message: "#^Parameter \\#3 \\$config of method League\\\\Flysystem\\\\AdapterInterface\\:\\:write\\(\\) expects League\\\\Flysystem\\\\Config, Gaufrette\\\\Adapter\\\\Config given\\.$#" + count: 1 + path: src/Gaufrette/Adapter/Flysystem.php + + - + message: "#^PHPDoc tag @throws with type Gaufrette\\\\Adapter\\\\RuntimeException is not subtype of Throwable$#" + count: 3 + path: src/Gaufrette/Adapter/Ftp.php + + - + message: "#^Reflection error\\: Google_Task_Retryable not found\\.$#" + count: 2 + path: src/Gaufrette/Adapter/GoogleCloudStorage.php + + - + message: "#^Class MongoDB\\\\Driver\\\\Exception\\\\RuntimeException not found\\.$#" + count: 2 + path: src/Gaufrette/Adapter/GridFS.php + + - + message: "#^Negated boolean expression is always false\\.$#" + count: 1 + path: src/Gaufrette/Adapter/LazyOpenCloud.php + + - + message: "#^PHPDoc tag @param has invalid value \\(\\$path\\)\\: Unexpected token \"\\$path\", expected type at offset 74$#" + count: 1 + path: src/Gaufrette/Adapter/Local.php + + - + message: "#^PHPDoc tag @param has invalid value \\(string The directory's path to delete\\)\\: Unexpected token \"The\", expected variable at offset 25$#" + count: 1 + path: src/Gaufrette/Adapter/Local.php + + - + message: "#^PHPDoc tag @param has invalid value \\(domain MogileFS domain\\)\\: Unexpected token \"MogileFS\", expected variable at offset 25$#" + count: 1 + path: src/Gaufrette/Adapter/MogileFS.php + + - + message: "#^PHPDoc tag @param has invalid value \\(hosts Array of MogileFS trackers\\)\\: Unexpected token \"Array\", expected variable at offset 62$#" + count: 1 + path: src/Gaufrette/Adapter/MogileFS.php + + - + message: "#^Return typehint of method Gaufrette\\\\Adapter\\\\MogileFS\\:\\:connect\\(\\) has invalid type Gaufrette\\\\Adapter\\\\Socket\\.$#" + count: 1 + path: src/Gaufrette/Adapter/MogileFS.php + + - + message: "#^Parameter \\#2 \\$port of function fsockopen expects int, string given\\.$#" + count: 1 + path: src/Gaufrette/Adapter/MogileFS.php + + - + message: "#^PHPDoc tag @param has invalid value \\(args Array of arguments\\)\\: Unexpected token \"Array\", expected variable at offset 98$#" + count: 1 + path: src/Gaufrette/Adapter/MogileFS.php + + - + message: "#^PHPDoc tag @param has invalid value \\(cmd Command\\)\\: Unexpected token \"Command\", expected variable at offset 71$#" + count: 1 + path: src/Gaufrette/Adapter/MogileFS.php + + - + message: "#^PHPDoc tag @param has invalid value \\(key File key\\)\\: Unexpected token \"File\", expected variable at offset 87$#" + count: 1 + path: src/Gaufrette/Adapter/MogileFS.php + + - + message: "#^PHPDoc tag @param has invalid value \\(data Data to save\\)\\: Unexpected token \"Data\", expected variable at offset 108$#" + count: 1 + path: src/Gaufrette/Adapter/MogileFS.php + + - + message: "#^PHPDoc tag @param has invalid value \\(path Save path at server\\)\\: Unexpected token \"Save\", expected variable at offset 69$#" + count: 1 + path: src/Gaufrette/Adapter/MogileFS.php + + - + message: "#^If condition is always true\\.$#" + count: 1 + path: src/Gaufrette/Adapter/OpenCloud.php + + - + message: "#^Unreachable statement \\- code above always terminates\\.$#" + count: 2 + path: src/Gaufrette/Adapter/OpenCloud.php + + - + message: "#^Call to an undefined method OpenCloud\\\\Common\\\\Base\\:\\:getName\\(\\)\\.$#" + count: 1 + path: src/Gaufrette/Adapter/OpenCloud.php + + - + message: "#^Method Gaufrette\\\\Adapter\\\\OpenCloud\\:\\:mtime\\(\\) should return bool\\|int but returns string\\.$#" + count: 1 + path: src/Gaufrette/Adapter/OpenCloud.php + + - + message: "#^Method Gaufrette\\\\Adapter\\\\OpenCloud\\:\\:checksum\\(\\) should return string but returns false\\.$#" + count: 1 + path: src/Gaufrette/Adapter/OpenCloud.php + + - + message: "#^Constant NET_SFTP_TYPE_DIRECTORY not found\\.$#" + count: 1 + path: src/Gaufrette/Adapter/PhpseclibSftp.php + + - + message: "#^Parameter \\$sftp of method Gaufrette\\\\Adapter\\\\Sftp\\:\\:__construct\\(\\) has invalid typehint type Ssh\\\\Sftp\\.$#" + count: 2 + path: src/Gaufrette/Adapter/Sftp.php + + - + message: "#^Method Gaufrette\\\\Adapter\\\\Sftp\\:\\:checksum\\(\\) should return string but returns false\\.$#" + count: 1 + path: src/Gaufrette/Adapter/Sftp.php + + - + message: "#^PHPDoc tag @throws with type Gaufrette\\\\Adapter\\\\RuntimeException is not subtype of Throwable$#" + count: 1 + path: src/Gaufrette/Adapter/Sftp.php + + - + message: "#^PHPDoc tag @param has invalid value \\(\\$key\\)\\: Unexpected token \"\\$key\", expected type at offset 161$#" + count: 1 + path: src/Gaufrette/Adapter/Zip.php + + - + message: "#^If condition is always true\\.$#" + count: 1 + path: src/Gaufrette/Adapter/Zip.php + + - + message: "#^PHPDoc tag @param has invalid value \\(\\$key\\)\\: Unexpected token \"\\$key\", expected type at offset 82$#" + count: 1 + path: src/Gaufrette/FilesystemInterface.php + + - + message: "#^PHPDoc tag @param has invalid value \\(\\$key\\)\\: Unexpected token \"\\$key\", expected type at offset 68$#" + count: 1 + path: src/Gaufrette/FilesystemInterface.php + + - + message: "#^Method Gaufrette\\\\Stream\\\\InMemoryBuffer\\:\\:stat\\(\\) should return array but returns false\\.$#" + count: 1 + path: src/Gaufrette/Stream/InMemoryBuffer.php + + - + message: "#^Method Gaufrette\\\\Stream\\\\Local\\:\\:read\\(\\) should return string but returns false\\.$#" + count: 1 + path: src/Gaufrette/Stream/Local.php + + - + message: "#^Method Gaufrette\\\\Stream\\\\Local\\:\\:write\\(\\) should return int but returns false\\.$#" + count: 1 + path: src/Gaufrette/Stream/Local.php + + - + message: "#^Method Gaufrette\\\\Stream\\\\Local\\:\\:tell\\(\\) should return int but returns false\\.$#" + count: 1 + path: src/Gaufrette/Stream/Local.php + + - + message: "#^Method Gaufrette\\\\Stream\\\\Local\\:\\:stat\\(\\) should return array but returns false\\.$#" + count: 1 + path: src/Gaufrette/Stream/Local.php + + - + message: "#^Parameter \\#2 \\$callback of function array_filter expects callable\\(mixed, mixed\\)\\: bool, 'strlen' given\\.$#" + count: 1 + path: src/Gaufrette/Util/Path.php + + - + message: "#^Method Gaufrette\\\\Util\\\\Size\\:\\:fromResource\\(\\) should return string but returns int\\.$#" + count: 1 + path: src/Gaufrette/Util/Size.php + diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 000000000..bc0f2c3a4 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,7 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 5 + paths: + - src diff --git a/psalm.baseline.xml b/psalm.baseline.xml new file mode 100644 index 000000000..6c664e8ed --- /dev/null +++ b/psalm.baseline.xml @@ -0,0 +1,106 @@ + + + + + $key + $metadata + $new + + + + + $metadata + + + + + $cachedKeys + \APCIterator + + + \APCIterator + + + + + $metadata + + + + + DeleteContainerOptions + + + + + $key + $metadata + $new + + + + + \OAuthException + + + + + Config + Config|array|null + + + + + RuntimeException + RuntimeException + RuntimeException + + + + + FileNotFoundException + FileNotFoundException + + + $metadata + + + $cursor + $files + Regex + + + $this->bucket->find(['filename' => $regex], ['projection' => ['filename' => 1]]) + $this->bucket->find([], ['projection' => ['filename' => 1]]) + + + + + Socket + + + + + NET_SFTP_TYPE_DIRECTORY + + + + + $this->sftp + $this->sftp + $this->sftp + $this->sftp + $this->sftp + $this->sftp + $this->sftp + $this->sftp + $this->sftp + $this->sftp + $this->sftp + \Ssh\Sftp + + + RuntimeException + + + diff --git a/psalm.xml b/psalm.xml new file mode 100644 index 000000000..5289d8eb2 --- /dev/null +++ b/psalm.xml @@ -0,0 +1,16 @@ + + + + + + + + + From 6f113b677cf054f222ab66f0a80906ee04b28728 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Mon, 5 Oct 2020 14:21:23 +0200 Subject: [PATCH 389/426] Remove deprecated code (#642) * Remove deprecated code * Remove more lazy * Added back some CI rules * Updated changelog --- CHANGELOG.md | 18 + composer.json | 2 - phpstan.neon.dist | 1 + .../Adapter/AclAwareAmazonS3Spec.php | 240 ------ spec/Gaufrette/Adapter/AmazonS3Spec.php | 684 ------------------ spec/Gaufrette/Adapter/ApcSpec.php | 67 -- spec/Gaufrette/Adapter/CacheSpec.php | 158 ---- spec/Gaufrette/Adapter/DropboxSpec.php | 289 -------- spec/Gaufrette/Adapter/LazyOpenCloudSpec.php | 47 -- spec/Gaufrette/Adapter/MogileFSSpec.php | 24 - spec/Gaufrette/Adapter/SftpSpec.php | 127 ---- src/Gaufrette/Adapter/AclAwareAmazonS3.php | 195 ----- src/Gaufrette/Adapter/AmazonS3.php | 306 -------- src/Gaufrette/Adapter/Apc.php | 153 ---- src/Gaufrette/Adapter/AwsS3.php | 32 - src/Gaufrette/Adapter/Cache.php | 243 ------- src/Gaufrette/Adapter/Dropbox.php | 182 ----- src/Gaufrette/Adapter/LazyOpenCloud.php | 48 -- src/Gaufrette/Adapter/MogileFS.php | 377 ---------- src/Gaufrette/Adapter/Sftp.php | 231 ------ .../Functional/Adapter/AmazonS3Test.php | 7 - .../Gaufrette/Functional/Adapter/ApcTest.php | 30 - .../Functional/Adapter/CacheTest.php | 33 - .../Functional/Adapter/DropboxTest.php | 7 - .../Functional/Adapter/MogileFSTest.php | 24 - .../Gaufrette/Functional/Adapter/SftpTest.php | 15 - 26 files changed, 19 insertions(+), 3521 deletions(-) delete mode 100644 spec/Gaufrette/Adapter/AclAwareAmazonS3Spec.php delete mode 100644 spec/Gaufrette/Adapter/AmazonS3Spec.php delete mode 100644 spec/Gaufrette/Adapter/ApcSpec.php delete mode 100644 spec/Gaufrette/Adapter/CacheSpec.php delete mode 100644 spec/Gaufrette/Adapter/DropboxSpec.php delete mode 100644 spec/Gaufrette/Adapter/LazyOpenCloudSpec.php delete mode 100644 spec/Gaufrette/Adapter/MogileFSSpec.php delete mode 100644 spec/Gaufrette/Adapter/SftpSpec.php delete mode 100644 src/Gaufrette/Adapter/AclAwareAmazonS3.php delete mode 100644 src/Gaufrette/Adapter/AmazonS3.php delete mode 100644 src/Gaufrette/Adapter/Apc.php delete mode 100644 src/Gaufrette/Adapter/Cache.php delete mode 100644 src/Gaufrette/Adapter/Dropbox.php delete mode 100644 src/Gaufrette/Adapter/LazyOpenCloud.php delete mode 100644 src/Gaufrette/Adapter/MogileFS.php delete mode 100644 src/Gaufrette/Adapter/Sftp.php delete mode 100644 tests/Gaufrette/Functional/Adapter/AmazonS3Test.php delete mode 100644 tests/Gaufrette/Functional/Adapter/ApcTest.php delete mode 100644 tests/Gaufrette/Functional/Adapter/CacheTest.php delete mode 100644 tests/Gaufrette/Functional/Adapter/DropboxTest.php delete mode 100644 tests/Gaufrette/Functional/Adapter/MogileFSTest.php delete mode 100644 tests/Gaufrette/Functional/Adapter/SftpTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index ab80bbea1..9f5c940ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ + +v0.10.0 - UNRELEASED +==================== + +## Removed + +The following adapters were deprecated and have now been removed. + +- AclAwareAmazonS3 +- AmazonS3 +- Cache +- Dropbox +- LazyOpenCloud +- MogileFS +- Sftp + +We also removed the deprecated function `AwsS3::getUrl()`. + v0.9.0 ====== diff --git a/composer.json b/composer.json index 2f46e5df6..0d800af8a 100644 --- a/composer.json +++ b/composer.json @@ -23,13 +23,11 @@ }, "require-dev": { "aws/aws-sdk-php": "^2.4.12||~3", - "amazonwebservices/aws-sdk-for-php": "1.5.*", "rackspace/php-opencloud" : "^1.9.2", "google/apiclient": "~1.1.3", "phpspec/phpspec": "~5.1", "phpseclib/phpseclib": "^2.0", "doctrine/dbal": ">=2.3", - "dropbox-php/dropbox-php": "*", "phpunit/phpunit": "~7.5", "mikey179/vfsstream": "~1.2.0", "league/flysystem": "~1.0", diff --git a/phpstan.neon.dist b/phpstan.neon.dist index bc0f2c3a4..72b5c82e0 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -3,5 +3,6 @@ includes: parameters: level: 5 + reportUnmatchedIgnoredErrors: false paths: - src diff --git a/spec/Gaufrette/Adapter/AclAwareAmazonS3Spec.php b/spec/Gaufrette/Adapter/AclAwareAmazonS3Spec.php deleted file mode 100644 index 8bd124ea2..000000000 --- a/spec/Gaufrette/Adapter/AclAwareAmazonS3Spec.php +++ /dev/null @@ -1,240 +0,0 @@ -beConstructedWith($adapter, $service, 'bucketName'); - } - - function it_is_adapter() - { - $this->shouldHaveType('Gaufrette\Adapter'); - } - - function it_supports_metadata() - { - $this->shouldHaveType('Gaufrette\Adapter\MetadataSupporter'); - } - - /** - * @param \Gaufrette\Adapter $adapter - */ - function it_delegates_read(Adapter $adapter) - { - $adapter->read('filename')->willReturn('some content'); - $adapter->read('filename2')->willReturn('other content'); - - $this->read('filename')->shouldReturn('some content'); - $this->read('filename2')->shouldReturn('other content'); - } - - /** - * @param \Gaufrette\Adapter $adapter - * @param \AmazonS3 $service - */ - function it_delegates_rename_and_update_acl(Adapter $adapter, AmazonS3 $service) - { - $service - ->set_object_acl('bucketName', 'filename2', \AmazonS3::ACL_PRIVATE) - ->shouldBeCalled() - ->willReturn(new \CFResponse([], '', 200)) - ; - $adapter - ->rename('filename', 'filename2') - ->shouldBeCalled() - ->willReturn(true) - ; - $adapter - ->delete('filename') - ->shouldNotBeCalled() - ; - - $this->rename('filename', 'filename2')->shouldReturn(true); - } - - /** - * @param \Gaufrette\Adapter $adapter - * @param \AmazonS3 $service - */ - function it_does_not_rename_when_cannot_update_acl(Adapter $adapter, AmazonS3 $service) - { - $service - ->set_object_acl('bucketName', 'filename2', \AmazonS3::ACL_PRIVATE) - ->shouldBeCalled() - ->willReturn(new \CFResponse([], '', 500)); - $adapter - ->rename('filename', 'filename2') - ->shouldBeCalled() - ->willReturn(true); - $adapter - ->delete('filename2') - ->shouldBeCalled(); - - $this->rename('filename', 'filename2')->shouldReturn(false); - } - - /** - * @param \Gaufrette\Adapter $adapter - * @param \AmazonS3 $service - */ - function it_updates_acl_with_users_array_when_rename(Adapter $adapter, AmazonS3 $service) - { - $service - ->set_object_acl('bucketName', 'filename2', [['id' => 'someId', 'permission' => \AmazonS3::GRANT_READ]]) - ->shouldBeCalled() - ->willReturn(new \CFResponse([], '', 200)) - ; - $adapter - ->rename('filename', 'filename2') - ->willReturn(true) - ; - - $this->setUsers([['id' => 'someId', 'permission' => 'read']]); - $this->rename('filename', 'filename2')->shouldReturn(true); - } - - /** - * @param \Gaufrette\Adapter $adapter - * @param \AmazonS3 $service - */ - function it_delegates_write_and_update_acl(Adapter $adapter, AmazonS3 $service) - { - $service - ->set_object_acl('bucketName', 'filename', \AmazonS3::ACL_PRIVATE) - ->shouldBeCalled() - ->willReturn(new \CFResponse([], '', 200)) - ; - $adapter - ->write('filename', 'some content') - ->shouldBeCalled() - ->willReturn(12) - ; - $adapter - ->delete('filename') - ->shouldNotBeCalled() - ; - - $this->write('filename', 'some content')->shouldReturn(12); - } - - /** - * @param \Gaufrette\Adapter $adapter - * @param \AmazonS3 $service - */ - function it_does_not_write_when_cannot_update_acl(Adapter $adapter, AmazonS3 $service) - { - $service - ->set_object_acl('bucketName', 'filename', \AmazonS3::ACL_PRIVATE) - ->shouldBeCalled() - ->willReturn(new \CFResponse([], '', 500)) - ; - $adapter - ->write('filename', 'some content') - ->shouldBeCalled() - ->willReturn(12) - ; - $adapter - ->delete('filename') - ->shouldBeCalled() - ; - - $this->write('filename', 'some content')->shouldReturn(false); - } - - /** - * @param \Gaufrette\Adapter $adapter - * @param \AmazonS3 $service - */ - function it_updates_acl_with_users_array_when_write(Adapter $adapter, AmazonS3 $service) - { - $service - ->set_object_acl('bucketName', 'filename', [['id' => 'someId', 'permission' => \AmazonS3::GRANT_READ]]) - ->shouldBeCalled() - ->willReturn(new \CFResponse([], '', 200)) - ; - $adapter - ->write('filename', 'some content') - ->willReturn(12) - ; - - $this->setUsers([['id' => 'someId', 'permission' => 'read']]); - $this->write('filename', 'some content')->shouldReturn(12); - } - - /** - * @param \Gaufrette\Adapter $adapter - */ - function it_delegates_exists(Adapter $adapter) - { - $adapter->exists('filename')->willReturn(true); - $adapter->exists('filename2')->willReturn(false); - - $this->exists('filename')->shouldReturn(true); - $this->exists('filename2')->shouldReturn(false); - } - - /** - * @param \Gaufrette\Adapter $adapter - */ - function it_delegates_mtime(Adapter $adapter) - { - $adapter->mtime('filename')->willReturn(1234); - $adapter->mtime('filename2')->willReturn(2345); - - $this->mtime('filename')->shouldReturn(1234); - $this->mtime('filename2')->shouldReturn(2345); - } - - /** - * @param \Gaufrette\Adapter $adapter - */ - function it_delegates_directory_check(Adapter $adapter) - { - $adapter->isDirectory('filename')->willReturn(true); - $adapter->isDirectory('filename2')->willReturn(false); - - $this->isDirectory('filename')->shouldReturn(true); - $this->isDirectory('filename2')->shouldReturn(false); - } - - /** - * @param \Gaufrette\Adapter $adapter - */ - function it_delegates_keys(Adapter $adapter) - { - $adapter->keys()->willReturn(['filename', 'filename2']); - - $this->keys()->shouldReturn(['filename', 'filename2']); - } - - /** - * @param \spec\Gaufrette\Adapter\TestDelegateAdapter $extendedAdapter - * @param \AmazonS3 $service - */ - function it_delegates_metadata_handling(TestDelegateAdapter $extendedAdapter, AmazonS3 $service) - { - $this->beConstructedWith($extendedAdapter, $service, 'bucketName'); - - $extendedAdapter->setMetadata('filename', ['some'])->shouldBeCalled(); - $extendedAdapter->getMetadata('filename')->shouldBeCalled()->willReturn(['some2']); - - $this->setMetadata('filename', ['some']); - $this->getMetadata('filename')->shouldReturn(['some2']); - } -} - -interface TestDelegateAdapter extends \Gaufrette\Adapter, - \Gaufrette\Adapter\MetadataSupporter -{ -} diff --git a/spec/Gaufrette/Adapter/AmazonS3Spec.php b/spec/Gaufrette/Adapter/AmazonS3Spec.php deleted file mode 100644 index 494ad0521..000000000 --- a/spec/Gaufrette/Adapter/AmazonS3Spec.php +++ /dev/null @@ -1,684 +0,0 @@ -beConstructedWith($service, 'bucketName'); - } - - function it_is_adapter() - { - $this->shouldHaveType('Gaufrette\Adapter'); - } - - function it_supports_metadata() - { - $this->shouldHaveType('Gaufrette\Adapter\MetadataSupporter'); - } - - /** - * @param \AmazonS3 $service - */ - function it_reads_file(AmazonS3 $service) - { - $options = [ - 'range' => 12, - 'response' => [ - 'content-language' => 'pl-pl', - ], - ]; - - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true) - ; - $service - ->get_object( - 'bucketName', - 'filename', - $options - ) - ->shouldBeCalled() - ->willReturn(new \CFResponse('header', 'some content', 200)) - ; - - $this->setMetadata('filename', $options); - $this->read('filename')->shouldReturn('some content'); - } - - /** - * @param \AmazonS3 $service - */ - function it_returns_false_when_cannot_read(AmazonS3 $service) - { - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true) - ; - $service - ->get_object( - 'bucketName', - 'filename', - [] - ) - ->shouldBeCalled() - ->willReturn(new \CFResponse('header', 'some content', 500)) - ; - - $this->read('filename')->shouldReturn(false); - } - - /** - * @param \AmazonS3 $service - */ - function it_is_verbose_and_throws_exceptions_when_read(AmazonS3 $service) - { - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true) - ; - $service - ->get_object( - 'bucketName', - 'filename', - [] - ) - ->willThrow(new \RuntimeException('read')) - ; - - $this->shouldThrow(new \RuntimeException('read'))->duringRead('filename'); - } - - /** - * @param \AmazonS3 $service - */ - function it_rename_file(AmazonS3 $service) - { - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true) - ; - $service - ->copy_object( - [ - 'bucket' => 'bucketName', - 'filename' => 'filename1', - ], - [ - 'bucket' => 'bucketName', - 'filename' => 'filename2', - ], - ['acl' => \AmazonS3::ACL_OWNER_READ] - ) - ->shouldBeCalled() - ->willReturn(new \CFResponse('header', 'some content', 200)) - ; - $service - ->delete_object( - 'bucketName', - 'filename1', - Argument::any() - ) - ->shouldBeCalled() - ->willReturn(new \CFResponse([], 'some', 200)) - ; - - $this->setMetadata('filename1', ['acl' => \AmazonS3::ACL_OWNER_READ]); - $this->rename('filename1', 'filename2')->shouldReturn(true); - } - - /** - * @param \AmazonS3 $service - */ - function it_is_verbose_and_throws_exceptions_when_rename(AmazonS3 $service) - { - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true) - ; - $service - ->copy_object(Argument::cetera()) - ->willThrow(new \RuntimeException('rename')) - ; - - $this->shouldThrow(new \RuntimeException('rename'))->duringRename('filename', 'filename1'); - } - - /** - * @param \AmazonS3 $service - */ - function it_returns_false_when_cannot_rename(AmazonS3 $service) - { - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true) - ; - $service - ->copy_object( - [ - 'bucket' => 'bucketName', - 'filename' => 'filename1', - ], - [ - 'bucket' => 'bucketName', - 'filename' => 'filename2', - ], - [] - ) - ->shouldBeCalled() - ->willReturn(new \CFResponse('header', 'some content', 500)) - ; - - $this->rename('filename1', 'filename2')->shouldReturn(false); - } - - /** - * @param \AmazonS3 $service - */ - function it_should_write_file(AmazonS3 $service) - { - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true) - ; - $service - ->create_object( - 'bucketName', - 'filename', - [ - 'acl' => \AmazonS3::ACL_PRIVATE, - 'body' => 'some content', - ] - ) - ->shouldBeCalled() - ->willReturn(new \CFResponse(['x-aws-requestheaders' => ['Content-Length' => 12]], 'some content', 200)) - ; - - $this->setMetadata('filename', ['acl' => \AmazonS3::ACL_PRIVATE, 'body' => 'other content']); - $this->write('filename', 'some content')->shouldReturn(12); - } - - /** - * @param \AmazonS3 $service - */ - function it_returns_false_when_cannot_write(AmazonS3 $service) - { - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true) - ; - $service - ->create_object( - 'bucketName', - 'filename', - [ - 'acl' => \AmazonS3::ACL_PUBLIC, - 'body' => 'some content', - ] - ) - ->shouldBeCalled() - ->willReturn(new \CFResponse(['x-aws-requestheaders' => ['Content-Length' => 12]], 'some content', 500)) - ; - - $this->write('filename', 'some content')->shouldReturn(false); - } - - /** - * @param \AmazonS3 $service - */ - function it_is_verbose_and_throws_exceptions_when_write(AmazonS3 $service) - { - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true) - ; - $service - ->create_object(Argument::cetera()) - ->willThrow(new \RuntimeException('write')) - ; - - $this->shouldThrow(new \RuntimeException('write'))->duringWrite('filename', 'some content'); - } - - /** - * @param \AmazonS3 $service - */ - function it_should_check_if_file_exists(AmazonS3 $service) - { - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true) - ; - - $service->if_object_exists('bucketName', 'filename')->willReturn(true); - $this->exists('filename')->shouldReturn(true); - - $service->if_object_exists('bucketName', 'filename')->willReturn(false); - $this->exists('filename')->shouldReturn(false); - } - - /** - * @param \AmazonS3 $service - */ - function it_is_verbose_and_throws_exceptions_when_file_exists(AmazonS3 $service) - { - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true) - ; - $service - ->if_object_exists('bucketName', 'filename') - ->willThrow(new \RuntimeException('exists')) - ; - - $this->shouldThrow(new \RuntimeException('exists'))->duringExists('filename'); - } - - /** - * @param \AmazonS3 $service - */ - function it_should_get_file_mtime(AmazonS3 $service) - { - $metadata = ['acl' => \AmazonS3::ACL_PUBLIC]; - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true) - ; - - $service - ->get_object_metadata( - 'bucketName', - 'filename', - $metadata - ) - ->shouldBeCalled() - ->willReturn(['Headers' => ['last-modified' => '2012-01-01 23:10:10']]) - ; - - $this->setMetadata('filename', $metadata); - $this->mtime('filename')->shouldReturn(strtotime('2012-01-01 23:10:10')); - } - - /** - * @param \AmazonS3 $service - */ - function it_returns_false_when_cannot_fetch_mtime(AmazonS3 $service) - { - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true) - ; - - $service - ->get_object_metadata( - 'bucketName', - 'filename', - [] - ) - ->shouldBeCalled() - ->willReturn(['Headers' => []]) - ; - - $this->mtime('filename')->shouldReturn(false); - } - - /** - * @param \AmazonS3 $service - */ - function it_is_verbose_and_throws_exceptions_when_fetch_mtime(AmazonS3 $service) - { - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true) - ; - $service - ->get_object_metadata('bucketName', 'filename', Argument::any()) - ->willThrow(new \RuntimeException('mtime')) - ; - - $this->shouldThrow(new \RuntimeException('mtime'))->duringMtime('filename'); - } - - /** - * @param \AmazonS3 $service - */ - function it_should_delete_file(AmazonS3 $service) - { - $metadata = ['acl' => \AmazonS3::ACL_PRIVATE]; - - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true) - ; - $service - ->delete_object( - 'bucketName', - 'filename', - $metadata - ) - ->willReturn(new \CFResponse([], 'some', 200)) - ; - - $this->setMetadata('filename', $metadata); - $this->delete('filename')->shouldReturn(true); - } - - /** - * @param \AmazonS3 $service - */ - function it_is_verbose_and_throws_exceptions_when_fetch_delete(AmazonS3 $service) - { - $service - ->if_bucket_exists('bucketName') - ->willReturn(true) - ; - $service - ->delete_object( - 'bucketName', - 'filename', - Argument::any() - ) - ->willThrow(new \RuntimeException('delete')) - ; - - $this->shouldThrow(new \RuntimeException('delete'))->duringDelete('filename'); - } - - /** - * @param \AmazonS3 $service - */ - function it_returns_false_when_cannot_delete(AmazonS3 $service) - { - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true) - ; - $service - ->delete_object( - 'bucketName', - 'filename', - [] - ) - ->willReturn(new \CFResponse([], 'some', 500)) - ; - - $this->delete('filename')->shouldReturn(false); - } - - /** - * @param \AmazonS3 $service - */ - function it_should_get_keys(AmazonS3 $service) - { - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true) - ; - $service - ->get_object_list('bucketName') - ->shouldBeCalled() - ->willReturn(['filename2', 'aaa/filename', 'filename1']) - ; - - $this->keys()->shouldReturn(['aaa', 'aaa/filename', 'filename1', 'filename2']); - } - - /** - * @param \AmazonS3 $service - */ - function it_is_verbose_and_throws_exceptions_when_fetch_keys(AmazonS3 $service) - { - $service - ->if_bucket_exists('bucketName') - ->willReturn(true) - ; - $service - ->get_object_list('bucketName') - ->willThrow(new \RuntimeException('keys')) - ; - - $this->shouldThrow(new \RuntimeException('keys'))->duringKeys(); - } - - /** - * @param \AmazonS3 $service - */ - function it_should_handle_dirs(AmazonS3 $service) - { - $service - ->if_bucket_exists('bucketName') - ->willReturn(true) - ; - $service - ->if_object_exists('bucketName', 'filename') - ->shouldNotBeCalled() - ; - $service - ->if_object_exists('bucketName', 'filename/') - ->shouldBeCalled() - ->willReturn(false) - ; - $service - ->if_object_exists('bucketName', 'dirname/') - ->willReturn(true) - ; - - $this->isDirectory('filename')->shouldReturn(false); - $this->isDirectory('dirname')->shouldReturn(true); - } - - /** - * @param \AmazonS3 $service - */ - function it_should_fail_when_bucket_does_not_exist(AmazonS3 $service) - { - $service - ->if_bucket_exists('bucketName') - ->willReturn(false) - ; - $this - ->shouldThrow(new \RuntimeException('The configured bucket "bucketName" does not exist.')) - ->duringRead('filename') - ; - $this - ->shouldThrow(new \RuntimeException('The configured bucket "bucketName" does not exist.')) - ->duringWrite('filename', 'content') - ; - $this - ->shouldThrow(new \RuntimeException('The configured bucket "bucketName" does not exist.')) - ->duringDelete('filename') - ; - $this - ->shouldThrow(new \RuntimeException('The configured bucket "bucketName" does not exist.')) - ->duringExists('filename') - ; - $this - ->shouldThrow(new \RuntimeException('The configured bucket "bucketName" does not exist.')) - ->duringMtime('filename') - ; - $this - ->shouldThrow(new \RuntimeException('The configured bucket "bucketName" does not exist.')) - ->duringRename('filename', 'filename2') - ; - $this - ->shouldThrow(new \RuntimeException('The configured bucket "bucketName" does not exist.')) - ->duringKeys() - ; - } - - /** - * @param \AmazonS3 $service - */ - function it_creates_bucket_if_create_mode_is_enabled(AmazonS3 $service) - { - $service->set_region(Argument::any())->shouldBeCalled(); - $service - ->if_bucket_exists('bucketName') - ->willReturn(false) - ; - $service->hostname = \AmazonS3::REGION_US_E1; - $service - ->create_bucket('bucketName', \AmazonS3::REGION_US_E1) - ->shouldBeCalled() - ->willReturn(new \CFResponse([], 'created', 201)) - ; - $service - ->if_object_exists('bucketName', 'filename') - ->willReturn(false) - ; - - $this->beConstructedWith($service, 'bucketName', ['create' => true]); - $this->exists('filename'); - } - - /** - * @param \AmazonS3 $service - */ - function it_fails_when_cannot_create_bucket(AmazonS3 $service) - { - $service - ->if_bucket_exists('bucketName') - ->willReturn(false) - ; - $service - ->create_bucket('bucketName', Argument::any()) - ->shouldBeCalled() - ->willReturn(new \CFResponse([], 'created', 500)) - ; - - $this->beConstructedWith($service, 'bucketName', ['create' => true]); - $this - ->shouldThrow(new \RuntimeException('Failed to create the configured bucket "bucketName".')) - ->duringExists('filename') - ; - } - - /** - * @param \AmazonS3 $service - */ - function it_allows_to_configure_reqion(AmazonS3 $service) - { - $service - ->if_bucket_exists('bucketName') - ->willReturn(true) - ; - $service - ->set_region(\AmazonS3::REGION_EU_W1) - ->shouldBeCalled() - ; - $service - ->if_object_exists('bucketName', 'filename') - ->willReturn(true) - ; - - $this->beConstructedWith($service, 'bucketName', ['region' => \AmazonS3::REGION_EU_W1]); - $this->exists('filename'); - } - - /** - * @param \AmazonS3 $service - */ - function it_allows_to_configure_region_for_bucket(AmazonS3 $service) - { - $service->set_region(Argument::any())->shouldBeCalled(); - $service - ->if_bucket_exists('bucketName') - ->willReturn(false) - ; - $service - ->create_bucket('bucketName', \AmazonS3::REGION_EU_W1) - ->shouldBeCalled() - ->willReturn(new \CFResponse([], 'created', 201)) - ; - $service - ->if_object_exists('bucketName', 'filename') - ->willReturn(false) - ; - - $this->beConstructedWith($service, 'bucketName', ['create' => true, 'region' => \AmazonS3::REGION_EU_W1]); - $this->exists('filename'); - } - - /** - * @param \AmazonS3 $service - */ - function it_allows_to_configure_acl(AmazonS3 $service) - { - $this->setAcl('123abc'); - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true) - ; - $service - ->create_object( - 'bucketName', - 'filename', - [ - 'acl' => '123abc', - 'body' => 'some content', - ] - ) - ->shouldBeCalled() - ->willReturn(new \CFResponse(['x-aws-requestheaders' => ['Content-Length' => 12]], 'some content', 200)) - ; - - $this->write('filename', 'some content')->shouldReturn(12); - $this->getAcl()->shouldBe('123abc'); - } - - /** - * @param \AmazonS3 $service - */ - function its_file_metadata_acl_are_more_important_than_global_acl_config(AmazonS3 $service) - { - $this->setAcl('123abc'); - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true) - ; - $service - ->create_object( - 'bucketName', - 'filename', - [ - 'acl' => 'more important acl', - 'body' => 'some content', - ] - ) - ->shouldBeCalled() - ->willReturn(new \CFResponse(['x-aws-requestheaders' => ['Content-Length' => 12]], 'some content', 200)) - ; - - $this->setMetadata('filename', ['acl' => 'more important acl']); - $this->write('filename', 'some content')->shouldReturn(12); - } -} diff --git a/spec/Gaufrette/Adapter/ApcSpec.php b/spec/Gaufrette/Adapter/ApcSpec.php deleted file mode 100644 index 1d01f6670..000000000 --- a/spec/Gaufrette/Adapter/ApcSpec.php +++ /dev/null @@ -1,67 +0,0 @@ -beConstructedWith('prefix-apc-test/'); - } - - function letGo() - { - global $extensionLoaded; - $extensionLoaded = null; - } - - function it_is_adapter() - { - $this->shouldHaveType('Gaufrette\Adapter'); - } - - function it_reads_file() - { - $this->read('filename')->shouldReturn('prefix-apc-test/filename content'); - $this->read('filename2')->shouldReturn('prefix-apc-test/filename2 content'); - } - - function it_writes_file() - { - $this->write('filename', 'some content')->shouldReturn(12); - $this->write('invalid', 'some content')->shouldReturn(false); - } - - function it_deletes_file() - { - $this->delete('filename')->shouldReturn(true); - $this->delete('invalid')->shouldReturn(false); - } - - function it_rename_file() - { - $this->rename('filename', 'aaa/filename2')->shouldReturn(true); - $this->rename('filename', 'invalid')->shouldReturn(false); - $this->rename('invalid', 'somename')->shouldReturn(false); - } - - function it_checks_if_file_exists() - { - $this->exists('filename')->shouldReturn(true); - $this->exists('invalid')->shouldReturn(false); - } - - function it_does_not_handles_directory() - { - $this->isDirectory('filename')->shouldReturn(false); - $this->isDirectory('invalid')->shouldReturn(false); - } -} diff --git a/spec/Gaufrette/Adapter/CacheSpec.php b/spec/Gaufrette/Adapter/CacheSpec.php deleted file mode 100644 index 7a739377e..000000000 --- a/spec/Gaufrette/Adapter/CacheSpec.php +++ /dev/null @@ -1,158 +0,0 @@ -beConstructedWith($source, $cache); - } - - function it_is_adapter() - { - $this->shouldHaveType('Gaufrette\Adapter'); - } - - function it_supports_metadata() - { - $this->shouldHaveType('Gaufrette\Adapter\MetadataSupporter'); - } - - /** - * @param \spec\Gaufrette\Adapter\CacheTestExtendedAdapter $extendedSource - * @param \spec\Gaufrette\Adapter\CacheTestExtendedAdapter $extendedCache - */ - function it_handles_metadata_when_cached_adapters_supports_metadata( - CacheTestExtendedAdapter $extendedSource, - CacheTestExtendedAdapter $extendedCache - ) { - $extendedSource->setMetadata('filename', ['metadata'])->shouldBeCalled(); - $extendedCache->setMetadata('filename', ['metadata'])->shouldBeCalled(); - $extendedSource->getMetadata('filename')->shouldBeCalled()->willReturn(['someMetadata']); - $this->beConstructedWith($extendedSource, $extendedCache); - - $this->setMetadata('filename', ['metadata']); - $this->getMetadata('filename')->shouldReturn(['someMetadata']); - } - - /** - * @param \Gaufrette\Adapter $source - */ - function it_delegates_is_directory_check_to_source(Adapter $source) - { - $source->isDirectory('filename')->shouldBeCalled()->willReturn(true); - - $this->isDirectory('filename')->shouldReturn(true); - } - - /** - * @param \Gaufrette\Adapter $source - * @param \Gaufrette\Adapter $cache - */ - function it_reads_from_cache_adapter(Adapter $source, Adapter $cache) - { - $source->read('filename')->shouldNotBeCalled(); - $cache->read('filename')->shouldBeCalled()->willReturn('some content'); - $source->mtime('filename')->willReturn(strtotime('2010-10-11')); - $cache->mtime('filename')->willReturn(strtotime('2010-10-12')); - $cache->exists('filename')->willReturn(true); - - $this->read('filename')->shouldReturn('some content'); - } - - /** - * @param \Gaufrette\Adapter $source - * @param \Gaufrette\Adapter $cache - */ - function it_update_cache_adapter_when_source_file_is_modified(Adapter $source, Adapter $cache) - { - $source->read('filename')->shouldBeCalled()->willReturn('some other content'); - $cache->read('filename')->shouldNotBeCalled(); - $cache->write('filename', 'some other content')->shouldBeCalled(); - $source->mtime('filename')->willReturn(strtotime('2010-10-11')); - $cache->mtime('filename')->willReturn(strtotime('2010-10-10')); - $cache->exists('filename')->willReturn(true); - - $this->read('filename')->shouldReturn('some other content'); - } - - /** - * @param \Gaufrette\Adapter $source - * @param \Gaufrette\Adapter $cache - */ - function it_rename_file_in_source_and_cache(Adapter $source, Adapter $cache) - { - $source->rename('filename', 'filename2')->shouldBeCalled()->willReturn(true); - $cache->rename('filename', 'filename2')->shouldBeCalled()->willReturn(true); - - $this->rename('filename', 'filename2')->shouldReturn(true); - } - - /** - * @param \Gaufrette\Adapter $source - * @param \Gaufrette\Adapter $cache - */ - function it_writes_file_to_source_and_cache(Adapter $source, Adapter $cache) - { - $source->write('filename', 'some content')->shouldBeCalled()->willReturn(12); - $cache->write('filename', 'some content')->shouldBeCalled()->willReturn(12); - - $this->write('filename', 'some content')->shouldReturn(12); - } - - /** - * @param \Gaufrette\Adapter $source - * @param \Gaufrette\Adapter $cache - */ - function it_deletes_file_from_source_and_cache(Adapter $source, Adapter $cache) - { - $source->delete('filename')->shouldBeCalled()->willReturn(true); - $cache->delete('filename')->shouldBeCalled()->willReturn(true); - - $this->delete('filename')->shouldReturn(true); - } - - /** - * @param \Gaufrette\Adapter $source - */ - function it_check_if_exists_in_source(Adapter $source) - { - $source->exists('filename')->shouldBeCalled()->willReturn(true); - - $this->exists('filename')->shouldReturn(true); - } - - /** - * @param \Gaufrette\Adapter $source - */ - function it_get_mtime_from_source(Adapter $source) - { - $source->mtime('filename')->shouldBeCalled()->willReturn(1234); - - $this->mtime('filename')->shouldReturn(1234); - } - - /** - * @param \Gaufrette\Adapter $source - */ - function it_get_keys_from_source(Adapter $source) - { - $source->keys()->willReturn(['filename2', 'filename1', 'filename']); - - $this->keys()->shouldReturn(['filename', 'filename1', 'filename2']); - } -} - -interface CacheTestExtendedAdapter extends \Gaufrette\Adapter, - \Gaufrette\Adapter\ChecksumCalculator, - \Gaufrette\Adapter\MetadataSupporter -{ -} diff --git a/spec/Gaufrette/Adapter/DropboxSpec.php b/spec/Gaufrette/Adapter/DropboxSpec.php deleted file mode 100644 index 76f355d09..000000000 --- a/spec/Gaufrette/Adapter/DropboxSpec.php +++ /dev/null @@ -1,289 +0,0 @@ -beConstructedWith($dropbox); - } - - function it_is_adapter() - { - $this->shouldHaveType('Gaufrette\Adapter'); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_reads_file(Dropbox_API $dropbox) - { - $dropbox->getFile('filename')->willReturn('some content'); - - $this->read('filename')->shouldReturn('some content'); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_does_not_mask_exception_from_client_during_read(Dropbox_API $dropbox) - { - $dropbox->getFile('filename')->willThrow(new \RuntimeException('read')); - - $this->shouldThrow(new \RuntimeException('read'))->duringRead('filename'); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_does_not_read_file(Dropbox_API $dropbox) - { - $dropbox - ->getFile('filename') - ->willThrow(new \Dropbox_Exception_NotFound()); - - $this->read('filename')->shouldReturn(false); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_checks_if_file_exists(Dropbox_API $dropbox) - { - $dropbox - ->getMetaData('filename', false) - ->willReturn([ - 'size' => '225.4KB', - 'rev' => '35e97029684fe', - 'thumb_exists' => false, - 'bytes' => 230783, - 'modified' => 'Tue, 19 Jul 2011 21:55:38 +0000', - 'client_mtime' => 'Mon, 18 Jul 2011 18:04:35 +0000', - 'path' => '/filename', - 'is_dir' => false, - 'icon' => 'page_white_acrobat', - 'root' => 'dropbox', - 'mime_type' => 'application/pdf', - 'revision' => 220823, - ]); - - $this->exists('filename')->shouldReturn(true); - - $dropbox - ->getMetaData('filename', false) - ->willThrow(new \Dropbox_Exception_NotFound); - - $this->exists('filename')->shouldReturn(false); - - $dropbox - ->getMetaData('filename', false) - ->willReturn(['is_deleted' => true]); - - $this->exists('filename')->shouldReturn(false); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_does_not_mask_exception_from_client_during_check_if_exists(Dropbox_API $dropbox) - { - $dropbox - ->getMetaData('filename', false) - ->willThrow(new \RuntimeException('exists')); - - $this->shouldThrow(new \RuntimeException('exists'))->duringExists('filename'); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_gets_keys(Dropbox_API $dropbox) - { - $dropbox - ->getMetaData('/', true) - ->willReturn([ - 'contents' => [ - ['path' => '/filename'], - ['path' => '/aaa/filename'], - ], - ]); - - $this->keys()->shouldReturn(['aaa', 'aaa/filename', 'filename']); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_does_not_mask_exception_from_client_during_getting_keys(Dropbox_API $dropbox) - { - $dropbox - ->getMetaData('/', true) - ->willThrow(new \RuntimeException('keys')) - ; - - $this->shouldThrow(new \RuntimeException('keys'))->duringKeys(); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_checks_if_given_key_is_directory(Dropbox_API $dropbox) - { - $dropbox - ->getMetaData('filename', false) - ->willReturn([ - 'is_dir' => true, - ]) - ; - - $this->isDirectory('filename')->shouldReturn(true); - - $dropbox - ->getMetaData('filename', false) - ->willReturn([ - 'is_dir' => false, - ]); - - $this->isDirectory('filename')->shouldReturn(false); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_writes_file(Dropbox_API $dropbox) - { - $dropbox - ->putFile('filename', Argument::any()) - ->shouldBeCalled() - ; - - $this->write('filename', 'some content')->shouldReturn(12); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_does_not_mask_exception_from_client_during_write(Dropbox_API $dropbox) - { - $dropbox - ->putFile('filename', Argument::any()) - ->willThrow(new \RuntimeException('write')) - ; - - $this->shouldThrow(new \RuntimeException('write'))->duringWrite('filename', 'some content'); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_deletes_file(Dropbox_API $dropbox) - { - $dropbox - ->delete('filename') - ->shouldBeCalled() - ; - - $this->delete('filename')->shouldReturn(true); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_does_not_delete_file(Dropbox_API $dropbox) - { - $dropbox - ->delete('filename') - ->willThrow(new \Dropbox_Exception_NotFound()) - ; - - $this->delete('filename')->shouldReturn(false); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_renames_file(Dropbox_API $dropbox) - { - $dropbox - ->move('filename', 'filename2') - ->shouldBeCalled() - ; - - $this->rename('filename', 'filename2')->shouldReturn(true); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_does_not_rename_file(Dropbox_API $dropbox) - { - $dropbox - ->move('filename', 'filename2') - ->willThrow(new \Dropbox_Exception_NotFound()) - ; - - $this->rename('filename', 'filename2')->shouldReturn(false); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_fetches_mtime(Dropbox_API $dropbox) - { - $dropbox - ->getMetaData('filename', false) - ->willReturn([ - 'modified' => 'Tue, 19 Jul 2011 21:55:38 +0000', - ]) - ; - - $this->mtime('filename')->shouldReturn(1311112538); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_does_not_fetch_mtime_when_file_not_found(Dropbox_API $dropbox) - { - $dropbox - ->getMetaData('filename', false) - ->willThrow(new \Dropbox_Exception_NotFound()) - ; - - $this->mtime('filename')->shouldReturn(false); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_does_not_check_if_key_is_dir_when_file_not_found(Dropbox_API $dropbox) - { - $dropbox - ->getMetaData('filename', false) - ->willThrow(new \Dropbox_Exception_NotFound()) - ; - - $this->isDirectory('filename')->shouldReturn(false); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_fails_checking_if_key_is_dir_when_dropbox_throws_exception(Dropbox_API $dropbox) - { - $dropbox - ->getMetaData('filename', false) - ->willThrow(new \RuntimeException('some exception')) - ; - - $this->shouldThrow(new \RuntimeException('some exception'))->duringIsDirectory('filename'); - } -} diff --git a/spec/Gaufrette/Adapter/LazyOpenCloudSpec.php b/spec/Gaufrette/Adapter/LazyOpenCloudSpec.php deleted file mode 100644 index a31e43e0c..000000000 --- a/spec/Gaufrette/Adapter/LazyOpenCloudSpec.php +++ /dev/null @@ -1,47 +0,0 @@ - - */ -class LazyOpenCloudSpec extends ObjectBehavior -{ - /** - * @param \Gaufrette\Adapter\OpenStackCloudFiles\ObjectStoreFactoryInterface $objectStoreFactory - */ - function let(ObjectStoreFactoryInterface $objectStoreFactory) - { - $this->beConstructedWith($objectStoreFactory, 'test-container-name'); - } - - function it_is_adapter() - { - $this->shouldHaveType('Gaufrette\Adapter'); - } - - /** - * @param \Gaufrette\Adapter\OpenStackCloudFiles\ObjectStoreFactoryInterface $objectStoreFactory - * @param \OpenCloud\ObjectStore\Service $objectStore - * @param \OpenCloud\ObjectStore\Resource\Container $container - */ - function it_initializes_object_store( - ObjectStoreFactoryInterface $objectStoreFactory, - Service $objectStore, - Container $container - ) { - $objectStoreFactory->getObjectStore()->shouldBeCalled()->willReturn($objectStore); - $objectStore->getContainer('test-container-name')->shouldBeCalled()->willReturn($container); - $container->getObject('test-file-name')->willThrow(new ObjectNotFoundException()); - - $this->read('test-file-name'); - } -} diff --git a/spec/Gaufrette/Adapter/MogileFSSpec.php b/spec/Gaufrette/Adapter/MogileFSSpec.php deleted file mode 100644 index cf6471984..000000000 --- a/spec/Gaufrette/Adapter/MogileFSSpec.php +++ /dev/null @@ -1,24 +0,0 @@ -beConstructedWith('http://domain.com', ['localhost']); - } - - function it_is_adapter() - { - $this->shouldHaveType('Gaufrette\Adapter'); - } - - function it_does_not_handle_mtime() - { - $this->mtime('filename')->shouldReturn(false); - $this->mtime('filename2')->shouldReturn(false); - } -} diff --git a/spec/Gaufrette/Adapter/SftpSpec.php b/spec/Gaufrette/Adapter/SftpSpec.php deleted file mode 100644 index cf115ddec..000000000 --- a/spec/Gaufrette/Adapter/SftpSpec.php +++ /dev/null @@ -1,127 +0,0 @@ -beConstructedWith($sftp, '/home/l3l0'); - } - - function it_is_adapter() - { - $this->shouldHaveType('Gaufrette\Adapter'); - } - - function it_is_checksum_calculator() - { - $this->shouldHaveType('Gaufrette\Adapter\ChecksumCalculator'); - } - - /** - * @param \Ssh\Sftp $sftp - */ - function it_fetches_keys($sftp) - { - $sftp - ->getUrl('/home/l3l0') - ->willReturn('ssh+ssl://localhost/home/l3l0') - ; - $sftp - ->listDirectory('/home/l3l0', true) - ->willReturn(['files' => ['/home/l3l0/filename', '/home/l3l0/filename1', '/home/l3l0/aaa/filename']]) - ; - - $this->keys()->shouldReturn(['aaa', 'aaa/filename', 'filename', 'filename1']); - } - - /** - * @param \Ssh\Sftp $sftp - */ - function it_reads_file($sftp) - { - $sftp - ->getUrl('/home/l3l0') - ->willReturn('ssh+ssl://localhost/home/l3l0') - ; - $sftp - ->read('/home/l3l0/filename') - ->shouldBeCalled() - ->willReturn('some content') - ; - - $this->read('filename')->shouldReturn('some content'); - } - - /** - * @param \Ssh\Sftp $sftp - */ - function it_writes_file($sftp) - { - $sftp - ->getUrl('/home/l3l0') - ->willReturn('ssh+ssl://localhost/home/l3l0') - ; - $sftp - ->write('/home/l3l0/filename', 'some content') - ->shouldBeCalled() - ->willReturn(12) - ; - - $this->write('filename', 'some content')->shouldReturn(12); - } - - /** - * @param \Ssh\Sftp $sftp - */ - function it_renames_file($sftp) - { - $sftp - ->getUrl('/home/l3l0') - ->willReturn('ssh+ssl://localhost/home/l3l0') - ; - $sftp - ->rename('/home/l3l0/filename', '/home/l3l0/filename1') - ->shouldBeCalled() - ->willReturn(true) - ; - - $this->rename('filename', 'filename1')->shouldReturn(true); - } - - /** - * @param \Ssh\Sftp $sftp - */ - function it_checks_if_file_exists($sftp) - { - $sftp - ->getUrl('/home/l3l0') - ->willReturn('ssh+ssl://localhost/home/l3l0') - ; - $sftp - ->getUrl('/home/l3l0/filename') - ->shouldBeCalled() - ->willReturn('ssh+ssl://localhost/home/l3l0/filename') - ; - $sftp - ->getUrl('/home/l3l0/filename1') - ->shouldBeCalled() - ->willReturn('ssh+ssl://localhost/home/l3l0/filename1') - ; - - $this->exists('filename')->shouldReturn(true); - $this->exists('filename1')->shouldReturn(false); - } -} diff --git a/src/Gaufrette/Adapter/AclAwareAmazonS3.php b/src/Gaufrette/Adapter/AclAwareAmazonS3.php deleted file mode 100644 index 6986276d8..000000000 --- a/src/Gaufrette/Adapter/AclAwareAmazonS3.php +++ /dev/null @@ -1,195 +0,0 @@ - - * - * @deprecated The AclAwareAmazonS3 adapter is deprecated since version 0.4 and will be removed in 1.0. Use the AwsS3 adapter instead. - */ -class AclAwareAmazonS3 implements Adapter, MetadataSupporter -{ - protected $delegate; - protected $s3; - protected $bucketName; - protected $aclConstant = AmazonClient::ACL_PRIVATE; - protected $users = []; - - public function __construct(Adapter $delegate, AmazonClient $s3, $bucketName) - { - $this->delegate = $delegate; - $this->s3 = $s3; - $this->bucketName = $bucketName; - } - - public function setAclConstant($constant) - { - if (!defined($constant = 'AmazonS3::ACL_' . strtoupper($constant))) { - throw new \InvalidArgumentException(sprintf('The ACL constant "%s" does not exist on AmazonS3.', $constant)); - } - - $this->aclConstant = constant($constant); - } - - public function setUsers(array $users) - { - $this->users = []; - - foreach ($users as $user) { - if (!isset($user['permission'])) { - throw new \InvalidArgumentException(sprintf('setUsers() expects an array where each item contains a "permission" key, but got %s.', json_encode($user))); - } - - if (!defined($constant = 'AmazonS3::GRANT_' . strtoupper($user['permission']))) { - throw new \InvalidArgumentException('The permission must be the suffix for one of the AmazonS3::GRANT_ constants.'); - } - $user['permission'] = constant($constant); - - if (isset($user['group'])) { - if (!defined($constant = 'AmazonS3::USERS_' . strtoupper($user['group']))) { - throw new \InvalidArgumentException('The group must be the suffix for one of the AmazonS3::USERS_ constants.'); - } - $user['id'] = constant($constant); - unset($user['group']); - } elseif (!isset($user['id'])) { - throw new \InvalidArgumentException(sprintf('Either "group", or "id" must be set for each user, but got %s.', json_encode($user))); - } - - $this->users[] = $user; - } - } - - /** - * {@inheritdoc} - */ - public function read($key) - { - return $this->delegate->read($key); - } - - /** - * {@inheritdoc} - */ - public function rename($key, $new) - { - $rs = $this->delegate->rename($key, $new); - - try { - $this->updateAcl($new); - - return $rs; - } catch (\Exception $ex) { - $this->delete($new); - - return false; - } - } - - /** - * {@inheritdoc} - */ - public function write($key, $content) - { - $rs = $this->delegate->write($key, $content); - - try { - $this->updateAcl($key); - - return $rs; - } catch (\Exception $ex) { - $this->delete($key); - - return false; - } - } - - /** - * {@inheritdoc} - */ - public function exists($key) - { - return $this->delegate->exists($key); - } - - /** - * {@inheritdoc} - */ - public function mtime($key) - { - return $this->delegate->mtime($key); - } - - /** - * {@inheritdoc} - */ - public function keys() - { - return $this->delegate->keys(); - } - - /** - * {@inheritdoc} - */ - public function delete($key) - { - return $this->delegate->delete($key); - } - - /** - * {@inheritdoc} - */ - public function setMetadata($key, $metadata) - { - if ($this->delegate instanceof MetadataSupporter) { - $this->delegate->setMetadata($key, $metadata); - } - } - - /** - * {@inheritdoc} - */ - public function getMetadata($key) - { - if ($this->delegate instanceof MetadataSupporter) { - return $this->delegate->getMetadata($key); - } - - return []; - } - - /** - * {@inheritdoc} - */ - public function isDirectory($key) - { - return $this->delegate->isDirectory($key); - } - - protected function getAcl() - { - if (empty($this->users)) { - return $this->aclConstant; - } - - return $this->users; - } - - private function updateAcl($key) - { - $response = $this->s3->set_object_acl($this->bucketName, $key, $this->getAcl()); - if (200 != $response->status) { - throw new \RuntimeException('S3-ACL change failed: ' . print_r($response, true)); - } - } -} diff --git a/src/Gaufrette/Adapter/AmazonS3.php b/src/Gaufrette/Adapter/AmazonS3.php deleted file mode 100644 index 470cd1cd4..000000000 --- a/src/Gaufrette/Adapter/AmazonS3.php +++ /dev/null @@ -1,306 +0,0 @@ - - * @author Leszek Prabucki - * - * @deprecated The AmazonS3 adapter is deprecated since version 0.4 and will be removed in 1.0. Use the AwsS3 adapter instead. - */ -class AmazonS3 implements Adapter, MetadataSupporter -{ - protected $service; - protected $bucket; - protected $ensureBucket = false; - protected $metadata; - protected $options; - - public function __construct(AmazonClient $service, $bucket, $options = []) - { - $this->service = $service; - $this->bucket = $bucket; - $this->options = array_replace_recursive( - ['directory' => '', 'create' => false, 'region' => $service->hostname, 'acl' => AmazonClient::ACL_PUBLIC], - $options - ); - } - - /** - * Set the acl used when writing files. - * - * @param string $acl - */ - public function setAcl($acl) - { - $this->options['acl'] = $acl; - } - - /** - * Get the acl used when writing files. - * - * @return string - */ - public function getAcl() - { - return $this->options['acl']; - } - - /** - * Set the base directory the user will have access to. - * - * @param string $directory - */ - public function setDirectory($directory) - { - $this->options['directory'] = $directory; - } - - /** - * Get the directory the user has access to. - * - * @return string - */ - public function getDirectory() - { - return $this->options['directory']; - } - - /** - * {@inheritdoc} - */ - public function setMetadata($key, $metadata) - { - $path = $this->computePath($key); - - $this->metadata[$path] = $metadata; - } - - /** - * {@inheritdoc} - */ - public function getMetadata($key) - { - $path = $this->computePath($key); - - return isset($this->metadata[$path]) ? $this->metadata[$path] : []; - } - - /** - * {@inheritdoc} - */ - public function read($key) - { - $this->ensureBucketExists(); - - $response = $this->service->get_object( - $this->bucket, - $this->computePath($key), - $this->getMetadata($key) - ); - - if (!$response->isOK()) { - return false; - } - - return $response->body; - } - - /** - * {@inheritdoc} - */ - public function rename($sourceKey, $targetKey) - { - $this->ensureBucketExists(); - - $response = $this->service->copy_object( - [ // source - 'bucket' => $this->bucket, - 'filename' => $this->computePath($sourceKey), - ], - [ // target - 'bucket' => $this->bucket, - 'filename' => $this->computePath($targetKey), - ], - $this->getMetadata($sourceKey) - ); - - return $response->isOK() && $this->delete($sourceKey); - } - - /** - * {@inheritdoc} - */ - public function write($key, $content) - { - $this->ensureBucketExists(); - - $opt = array_replace_recursive( - ['acl' => $this->options['acl']], - $this->getMetadata($key), - ['body' => $content] - ); - - $response = $this->service->create_object( - $this->bucket, - $this->computePath($key), - $opt - ); - - if (!$response->isOK()) { - return false; - }; - - return intval($response->header['x-aws-requestheaders']['Content-Length']); - } - - /** - * {@inheritdoc} - */ - public function exists($key) - { - $this->ensureBucketExists(); - - return $this->service->if_object_exists( - $this->bucket, - $this->computePath($key) - ); - } - - /** - * {@inheritdoc} - */ - public function mtime($key) - { - $this->ensureBucketExists(); - - $response = $this->service->get_object_metadata( - $this->bucket, - $this->computePath($key), - $this->getMetadata($key) - ); - - return isset($response['Headers']['last-modified']) ? strtotime($response['Headers']['last-modified']) : false; - } - - /** - * {@inheritdoc} - */ - public function keys() - { - $this->ensureBucketExists(); - - $list = $this->service->get_object_list($this->bucket); - - $keys = []; - foreach ($list as $file) { - if ('.' !== $dirname = \Gaufrette\Util\Path::dirname($file)) { - $keys[] = $dirname; - } - $keys[] = $file; - } - sort($keys); - - return $keys; - } - - /** - * {@inheritdoc} - */ - public function delete($key) - { - $this->ensureBucketExists(); - - $response = $this->service->delete_object( - $this->bucket, - $this->computePath($key), - $this->getMetadata($key) - ); - - return $response->isOK(); - } - - /** - * {@inheritdoc} - */ - public function isDirectory($key) - { - if ($this->exists($key . '/')) { - return true; - } - - return false; - } - - /** - * Ensures the specified bucket exists. If the bucket does not exists - * and the create parameter is set to true, it will try to create the - * bucket. - * - * @throws \RuntimeException if the bucket does not exists or could not be - * created - */ - private function ensureBucketExists() - { - if ($this->ensureBucket) { - return; - } - - if (isset($this->options['region'])) { - $this->service->set_region($this->options['region']); - } - - if ($this->service->if_bucket_exists($this->bucket)) { - $this->ensureBucket = true; - - return; - } - - if (!$this->options['create']) { - throw new \RuntimeException(sprintf( - 'The configured bucket "%s" does not exist.', - $this->bucket - )); - } - - $response = $this->service->create_bucket( - $this->bucket, - $this->options['region'] - ); - - if (!$response->isOK()) { - throw new \RuntimeException(sprintf( - 'Failed to create the configured bucket "%s".', - $this->bucket - )); - } - - $this->ensureBucket = true; - } - - /** - * Computes the path for the specified key taking the bucket in account. - * - * @param string $key The key for which to compute the path - * - * @return string - */ - private function computePath($key) - { - $directory = $this->getDirectory(); - if (null === $directory || '' === $directory) { - return $key; - } - - return sprintf('%s/%s', $directory, $key); - } -} diff --git a/src/Gaufrette/Adapter/Apc.php b/src/Gaufrette/Adapter/Apc.php deleted file mode 100644 index b2367c0e4..000000000 --- a/src/Gaufrette/Adapter/Apc.php +++ /dev/null @@ -1,153 +0,0 @@ - - * @author Antoine Hérault - * @author Leszek Prabucki - * - * @deprecated The Apc adapter is deprecated since version 0.4 and will be removed in 1.0. - */ -class Apc implements Adapter -{ - protected $prefix; - protected $ttl; - - /** - * @throws \RuntimeException - * - * @param string $prefix to avoid conflicts between filesystems - * @param int $ttl time to live, default is 0 - */ - public function __construct($prefix, $ttl = 0) - { - if (!extension_loaded('apc')) { - throw new \RuntimeException('Unable to use Gaufrette\Adapter\Apc as the APC extension is not available.'); - } - - $this->prefix = $prefix; - $this->ttl = $ttl; - } - - /** - * {@inheritdoc} - */ - public function read($key) - { - return apc_fetch($this->computePath($key)); - } - - /** - * {@inheritdoc} - */ - public function write($key, $content, array $metadata = null) - { - $result = apc_store($this->computePath($key), $content, $this->ttl); - - if (!$result) { - return false; - } - - return Util\Size::fromContent($content); - } - - /** - * {@inheritdoc} - */ - public function exists($key) - { - return apc_exists($this->computePath($key)); - } - - /** - * {@inheritdoc} - */ - public function keys() - { - $cachedKeys = $this->getCachedKeysIterator(); - - if (null === $cachedKeys) { - return []; - } - - $keys = []; - foreach ($cachedKeys as $key => $value) { - $pattern = sprintf('/^%s/', preg_quote($this->prefix, '/')); - $keys[] = preg_replace($pattern, '', $key); - } - sort($keys); - - return $keys; - } - - /** - * {@inheritdoc} - */ - public function mtime($key) - { - $cachedKeys = iterator_to_array($this->getCachedKeysIterator($key, APC_ITER_MTIME)); - - return $cachedKeys[$this->computePath($key)]['mtime']; - } - - /** - * {@inheritdoc} - */ - public function delete($key) - { - return apc_delete($this->computePath($key)); - } - - /** - * {@inheritdoc} - */ - public function rename($sourceKey, $targetKey) - { - // TODO: this probably allows for race conditions... - $written = $this->write($targetKey, $this->read($sourceKey)); - $deleted = $this->delete($sourceKey); - - return $written && $deleted; - } - - /** - * {@inheritdoc} - */ - public function isDirectory($key) - { - return false; - } - - /** - * Computes the path for the given key. - * - * @param string $key - * - * @return string - */ - public function computePath($key) - { - return $this->prefix . $key; - } - - /** - * @param string $key - by default '' - * @param int $format - by default APC_ITER_NONE - * - * @return \APCIterator - */ - protected function getCachedKeysIterator($key = '', $format = APC_ITER_NONE) - { - $pattern = sprintf('/^%s/', preg_quote($this->prefix . $key, '/')); - - return new \APCIterator('user', $pattern, $format); - } -} diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index 2f68ec19d..cc478314d 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -48,38 +48,6 @@ public function __construct(S3Client $service, $bucket, array $options = [], $de $this->detectContentType = $detectContentType; } - /** - * Gets the publicly accessible URL of an Amazon S3 object. - * - * @param string $key Object key - * @param array $options Associative array of options used to buld the URL - * - expires: The time at which the URL should expire - * represented as a UNIX timestamp - * - Any options available in the Amazon S3 GetObject - * operation may be specified. - * - * @return string - * - * @deprecated 1.0 Resolving object path into URLs is out of the scope of this repository since v0.4. gaufrette/extras - * provides a Filesystem decorator with a regular resolve() method. You should use it instead. - * - * @see https://github.com/Gaufrette/extras - */ - public function getUrl($key, array $options = []) - { - @trigger_error( - 'Using AwsS3::getUrl() method was deprecated since v0.4. Please chek gaufrette/extras package if you want this feature', - E_USER_DEPRECATED - ); - - return $this->service->getObjectUrl( - $this->bucket, - $this->computePath($key), - $options['expires'] ?? null, - $options - ); - } - /** * {@inheritdoc} */ diff --git a/src/Gaufrette/Adapter/Cache.php b/src/Gaufrette/Adapter/Cache.php deleted file mode 100644 index 8b2d37e95..000000000 --- a/src/Gaufrette/Adapter/Cache.php +++ /dev/null @@ -1,243 +0,0 @@ - - * - * @deprecated The Cache adapter is deprecated since version 0.4 and will be removed in 1.0. - */ -class Cache implements Adapter, MetadataSupporter -{ - /** - * @var Adapter - */ - protected $source; - - /** - * @var Adapter - */ - protected $cache; - - /** - * @var int - */ - protected $ttl; - - /** - * @var Adapter - */ - protected $serializeCache; - - /** - * @param Adapter $source The source adapter that must be cached - * @param Adapter $cache The adapter used to cache the source - * @param int $ttl Time to live of a cached file - * @param Adapter $serializeCache The adapter used to cache serializations - */ - public function __construct(Adapter $source, Adapter $cache, $ttl = 0, Adapter $serializeCache = null) - { - $this->source = $source; - $this->cache = $cache; - $this->ttl = $ttl; - - if (!$serializeCache) { - $serializeCache = new InMemoryAdapter(); - } - $this->serializeCache = $serializeCache; - } - - /** - * Returns the time to live of the cache. - * - * @return int $ttl - */ - public function getTtl() - { - return $this->ttl; - } - - /** - * Defines the time to live of the cache. - * - * @param int $ttl - */ - public function setTtl($ttl) - { - $this->ttl = $ttl; - } - - /** - * {@inheritdoc} - */ - public function read($key) - { - if ($this->needsReload($key)) { - $contents = $this->source->read($key); - $this->cache->write($key, $contents); - } else { - $contents = $this->cache->read($key); - } - - return $contents; - } - - /** - * {@inheritdoc} - */ - public function rename($key, $new) - { - return $this->source->rename($key, $new) && $this->cache->rename($key, $new); - } - - /** - * {@inheritdoc} - */ - public function write($key, $content, array $metadata = null) - { - $bytesSource = $this->source->write($key, $content); - - if (false === $bytesSource) { - return false; - } - - $bytesCache = $this->cache->write($key, $content); - - if ($bytesSource !== $bytesCache) { - return false; - } - - return $bytesSource; - } - - /** - * {@inheritdoc} - */ - public function exists($key) - { - if ($this->needsReload($key)) { - return $this->source->exists($key); - } - - return $this->cache->exists($key); - } - - /** - * {@inheritdoc} - */ - public function mtime($key) - { - return $this->source->mtime($key); - } - - /** - * {@inheritdoc} - */ - public function keys() - { - $cacheFile = 'keys.cache'; - if ($this->needsRebuild($cacheFile)) { - $keys = $this->source->keys(); - sort($keys); - $this->serializeCache->write($cacheFile, serialize($keys)); - } else { - $keys = unserialize($this->serializeCache->read($cacheFile)); - } - - return $keys; - } - - /** - * {@inheritdoc} - */ - public function delete($key) - { - return $this->source->delete($key) && $this->cache->delete($key); - } - - /** - * {@inheritdoc} - */ - public function isDirectory($key) - { - return $this->source->isDirectory($key); - } - - /** - * {@inheritdoc} - */ - public function setMetadata($key, $metadata) - { - if ($this->source instanceof MetadataSupporter) { - $this->source->setMetadata($key, $metadata); - } - - if ($this->cache instanceof MetadataSupporter) { - $this->cache->setMetadata($key, $metadata); - } - } - - /** - * {@inheritdoc} - */ - public function getMetadata($key) - { - if ($this->source instanceof MetadataSupporter) { - return $this->source->getMetadata($key); - } - - return false; - } - - /** - * Indicates whether the cache for the specified key needs to be reloaded. - * - * @param string $key - */ - public function needsReload($key) - { - $needsReload = true; - - if ($this->cache->exists($key)) { - try { - $dateCache = $this->cache->mtime($key); - $needsReload = false; - - if (time() - $this->ttl >= $dateCache) { - $dateSource = $this->source->mtime($key); - $needsReload = $dateCache < $dateSource; - } - } catch (\RuntimeException $e) { - } - } - - return $needsReload; - } - - /** - * Indicates whether the serialized cache file needs to be rebuild. - * - * @param string $cacheFile - */ - public function needsRebuild($cacheFile) - { - $needsRebuild = true; - - if ($this->serializeCache->exists($cacheFile)) { - try { - $needsRebuild = time() - $this->ttl >= $this->serializeCache->mtime($cacheFile); - } catch (\RuntimeException $e) { - } - } - - return $needsRebuild; - } -} diff --git a/src/Gaufrette/Adapter/Dropbox.php b/src/Gaufrette/Adapter/Dropbox.php deleted file mode 100644 index a39b7a220..000000000 --- a/src/Gaufrette/Adapter/Dropbox.php +++ /dev/null @@ -1,182 +0,0 @@ - - * @author Antoine Hérault - * @author Leszek Prabucki - * - * @deprecated The Dropbox adapter is deprecated since version 0.4 and will be removed in 1.0. - */ -class Dropbox implements Adapter -{ - protected $client; - - /** - * @param \Dropbox_API $client The Dropbox API client - */ - public function __construct(DropboxApi $client) - { - $this->client = $client; - } - - /** - * {@inheritdoc} - * - * @throws \Dropbox_Exception_Forbidden - * @throws \Dropbox_Exception_OverQuota - * @throws \OAuthException - */ - public function read($key) - { - try { - return $this->client->getFile($key); - } catch (DropboxNotFoundException $e) { - return false; - } - } - - /** - * {@inheritdoc} - */ - public function isDirectory($key) - { - try { - $metadata = $this->getDropboxMetadata($key); - } catch (Exception\FileNotFound $e) { - return false; - } - - return (boolean) isset($metadata['is_dir']) ? $metadata['is_dir'] : false; - } - - /** - * {@inheritdoc} - * - * @throws \Dropbox_Exception - */ - public function write($key, $content) - { - $resource = tmpfile(); - fwrite($resource, $content); - fseek($resource, 0); - - try { - $this->client->putFile($key, $resource); - } catch (\Exception $e) { - fclose($resource); - - throw $e; - } - - fclose($resource); - - return Util\Size::fromContent($content); - } - - /** - * {@inheritdoc} - */ - public function delete($key) - { - try { - $this->client->delete($key); - } catch (DropboxNotFoundException $e) { - return false; - } - - return true; - } - - /** - * {@inheritdoc} - */ - public function rename($sourceKey, $targetKey) - { - try { - $this->client->move($sourceKey, $targetKey); - } catch (DropboxNotFoundException $e) { - return false; - } - - return true; - } - - /** - * {@inheritdoc} - */ - public function mtime($key) - { - try { - $metadata = $this->getDropboxMetadata($key); - } catch (Exception\FileNotFound $e) { - return false; - } - - return strtotime($metadata['modified']); - } - - /** - * {@inheritdoc} - */ - public function keys() - { - $metadata = $this->client->getMetaData('/', true); - if (!isset($metadata['contents'])) { - return []; - } - - $keys = []; - foreach ($metadata['contents'] as $value) { - $file = ltrim($value['path'], '/'); - $keys[] = $file; - if ('.' !== $dirname = \Gaufrette\Util\Path::dirname($file)) { - $keys[] = $dirname; - } - } - sort($keys); - - return $keys; - } - - /** - * {@inheritdoc} - */ - public function exists($key) - { - try { - $this->getDropboxMetadata($key); - - return true; - } catch (Exception\FileNotFound $e) { - return false; - } - } - - private function getDropboxMetadata($key) - { - try { - $metadata = $this->client->getMetaData($key, false); - } catch (DropboxNotFoundException $e) { - throw new Exception\FileNotFound($key, 0, $e); - } - - // TODO find a way to exclude deleted files - if (isset($metadata['is_deleted']) && $metadata['is_deleted']) { - throw new Exception\FileNotFound($key); - } - - return $metadata; - } -} diff --git a/src/Gaufrette/Adapter/LazyOpenCloud.php b/src/Gaufrette/Adapter/LazyOpenCloud.php deleted file mode 100644 index f5fec63db..000000000 --- a/src/Gaufrette/Adapter/LazyOpenCloud.php +++ /dev/null @@ -1,48 +0,0 @@ - - * - * @deprecated The LazyOpenCloud adapter is deprecated since version 0.4 and will be removed in 1.0. Use the OpenCloud adapter instead. - */ -class LazyOpenCloud extends OpenCloud -{ - /** - * @var ObjectStoreFactoryInterface - */ - protected $objectStoreFactory; - - /** - * @param ObjectStoreFactoryInterface $objectStoreFactory - * @param string $containerName - * @param bool $createContainer - */ - public function __construct(ObjectStoreFactoryInterface $objectStoreFactory, $containerName, $createContainer = false) - { - $this->objectStoreFactory = $objectStoreFactory; - $this->containerName = $containerName; - $this->createContainer = $createContainer; - } - - /** - * Override parent to lazy-load object store. - * - * {@inheritdoc} - */ - protected function getContainer() - { - if (!$this->objectStore) { - $this->objectStore = $this->objectStoreFactory->getObjectStore(); - } - - return parent::getContainer(); - } -} diff --git a/src/Gaufrette/Adapter/MogileFS.php b/src/Gaufrette/Adapter/MogileFS.php deleted file mode 100644 index 5b35c5947..000000000 --- a/src/Gaufrette/Adapter/MogileFS.php +++ /dev/null @@ -1,377 +0,0 @@ - - * @author Antoine Hérault - * - * @deprecated The MogileFS adapter is deprecated since version 0.4 and will be removed in 1.0. - * - * Bases partly on Wikimedia MogileFS client code by Jens Frank and Domas Mituzas, 2007. - * See more: http://svn.wikimedia.org/viewvc/mediawiki/trunk/extensions/MogileClient/ - */ -class MogileFS implements Adapter -{ - const ERR_OTHER = 0; - const ERR_UNKNOWN_KEY = 1; - const ERR_EMPTY_FILE = 2; - const ERR_NONE_MATCH = 3; - const ERR_KEY_EXISTS = 4; - - protected $domain; - protected $hosts; - protected $socket; - - /** - * @param domain MogileFS domain - * @param hosts Array of MogileFS trackers - */ - public function __construct($domain, array $hosts) - { - if (strlen($domain) < 1 || count($hosts) < 1) { - throw new \InvalidArgumentException('Invalid parameters. Given domain is too short or you not given any host.'); - } - - $this->domain = $domain; - $this->hosts = $hosts; - } - - /** - * {@inheritdoc} - */ - public function read($key) - { - $paths = $this->getPaths($key); - - $data = ''; - - if ($paths) { - shuffle($paths); - foreach ($paths as $path) { - $fh = fopen($path, 'r'); - - if (!$fh) { - continue; - } - - while (!feof($fh)) { - $data .= fread($fh, 8192); - } - - fclose($fh); - - break; - } - } - - return $data ?: false; - } - - /** - * {@inheritdoc} - */ - public function write($key, $content, array $metadata = null) - { - $closeres = false; - - if (mb_strlen($content) > 0) { - $res = $this->doRequest('CREATE_OPEN', ['key' => $key, 'class' => $metadata['mogile_class']]); - - if ($res && preg_match('/^http:\/\/([a-z0-9.-]*):([0-9]*)\/(.*)$/', $res['path'], $matches)) { - $host = $matches[1]; - $port = $matches[2]; - $path = $matches[3]; - - $status = $this->putFile($res['path'], $content); - - if ($status) { - $params = ['key' => $key, 'class' => $metadata['mogile_class'], 'devid' => $res['devid'], - 'fid' => $res['fid'], 'path' => urldecode($res['path']), ]; - $closeres = $this->doRequest('CREATE_CLOSE', $params); - } - } - } - - if (!is_array($closeres)) { - return false; - } - - return Util\Size::fromContent($content); - } - - /** - * {@inheritdoc} - */ - public function delete($key) - { - $this->doRequest('DELETE', ['key' => $key]); - - return true; - } - - /** - * {@inheritdoc} - */ - public function rename($sourceKey, $targetKey) - { - $this->doRequest('RENAME', [ - 'from_key' => $sourceKey, - 'to_key' => $targetKey, - ]); - - return true; - } - - /** - * {@inheritdoc} - */ - public function exists($key) - { - try { - $this->getPaths($key); - } catch (\RuntimeException $e) { - return false; - } - - return true; - } - - /** - * {@inheritdoc} - */ - public function keys() - { - try { - $result = $this->doRequest('LIST_KEYS'); - } catch (\RuntimeException $e) { - if (self::ERR_NONE_MATCH === $e->getCode()) { - return []; - } - - throw $e; - } - - unset($result['key_count'], $result['next_after']); - - return array_values($result); - } - - /** - * {@inheritdoc} - */ - public function mtime($key) - { - return false; - } - - /** - * {@inheritdoc} - */ - public function isDirectory($key) - { - return false; - } - - /** - * Get available domains and classes from tracker. - * - * @return mixed Array on success, false on failure - */ - public function getDomains() - { - $res = $this->doRequest('GET_DOMAINS'); - if (!$res) { - return false; - } - - $domains = []; - - for ($i = 1; $i <= $res['domains']; ++$i) { - $dom = 'domain' . $i; - $classes = []; - - // Associate classes to current domain (class name => mindevcount) - for ($j = 1; $j <= $res[$dom . 'classes']; ++$j) { - $classes[$res[$dom . 'class' . $j . 'name']] = $res[$dom . 'class' . $j . 'mindevcount']; - } - - $domains[] = ['name' => $res[$dom], 'classes' => $classes]; - } - - return $domains; - } - - /** - * Tries to connect MogileFS tracker. - * - * @return Socket - */ - private function connect() - { - if ($this->socket) { - return $this->socket; - } - - shuffle($this->hosts); - - foreach ($this->hosts as $host) { - list($ip, $port) = explode(':', $host); - $this->socket = @fsockopen($ip, $port, $err_no, $err_str, 1); - - if ($this->socket) { - break; - } - } - - if (!$this->socket) { - throw new \RuntimeException('Unable to connect to the tracker.'); - } - - return $this->socket; - } - - /** - * Close connection to MogileFS tracker. - * - * @return bool - */ - private function close() - { - if ($this->socket) { - return fclose($this->socket); - } - - return true; - } - - /** - * Makes request to MogileFS tracker. - * - * @param cmd Command - * @param args Array of arguments - * - * @return mixed Array on success, false on failure - */ - private function doRequest($cmd, $args = []) - { - clearstatcache(); - $args['domain'] = $this->domain; - $params = http_build_query($args); - - $this->connect(); - - fwrite($this->socket, "{$cmd} {$params}\n"); - $line = fgets($this->socket); - - $words = explode(' ', $line); - - if ($words[0] == 'OK') { - parse_str(trim($words[1]), $result); - } else { - $errorName = empty($words[1]) ? null : $words[1]; - - switch ($errorName) { - case 'unknown_key': - $errorCode = static::ERR_UNKNOWN_KEY; - - break; - case 'empty_file': - $errorCode = static::ERR_EMPTY_FILE; - - break; - case 'none_match': - $errorCode = static::ERR_NONE_MATCH; - - break; - case 'key_exists': - $errorCode = static::ERR_KEY_EXISTS; - - break; - default: - $errorCode = static::ERR_OTHER; - } - - throw new \RuntimeException( - sprintf('Error response: %s', $line), - $errorCode - ); - } - - return $result; - } - - /** - * Get file location at server from MogileFS tracker. - * - * @param key File key - * - * @return mixed Array on success, false on failure - */ - private function getPaths($key) - { - $res = $this->doRequest('GET_PATHS', ['key' => $key]); - unset($res['paths']); - - return $res; - } - - /** - * Sends file to MogileFS tracker. - * - * @param path Save path at server - * @param data Data to save - * - * @return bool - */ - private function putFile($path, $data) - { - $info = false; - $url = parse_url($path); - - $fp = fsockopen($url['host'], $url['port'], $errno, $errstr, 5); - if (!$fp) { - return false; - } - - $buffer = ''; - $b = "\r\n"; - - stream_set_blocking($fp, true); - stream_set_timeout($fp, 30, 200000); - - $out = 'PUT ' . $url['path'] . ' HTTP/1.1' . $b; - $out .= 'Host: ' . $url['host'] . $b; - $out .= 'Content-Length: ' . Util\Size::fromContent($data) . $b . $b; - $out .= $data; - $out .= $b . $b; - fwrite($fp, $out); - fflush($fp); - - stream_set_blocking($fp, true); - stream_set_timeout($fp, 30, 200000); - - while (!feof($fp) && !$info['timed_out']) { - $info = stream_get_meta_data($fp); - $buffer .= fgets($fp, 128); - } - fclose($fp); - - return true; - } - - /** - * Closes the underlying connection. - */ - public function __destruct() - { - $this->close(); - } -} diff --git a/src/Gaufrette/Adapter/Sftp.php b/src/Gaufrette/Adapter/Sftp.php deleted file mode 100644 index 96af99185..000000000 --- a/src/Gaufrette/Adapter/Sftp.php +++ /dev/null @@ -1,231 +0,0 @@ -sftp = $sftp; - $this->directory = $directory; - $this->create = $create; - } - - /** - * {@inheritdoc} - */ - public function read($key) - { - $this->initialize(); - - $content = $this->sftp->read($this->computePath($key)); - - return $content; - } - - /** - * {@inheritdoc} - */ - public function rename($sourceKey, $targetKey) - { - $sourcePath = $this->computePath($sourceKey); - $targetPath = $this->computePath($targetKey); - - $this->ensureDirectoryExists(\Gaufrette\Util\Path::dirname($targetPath), true); - - return $this->sftp->rename($sourcePath, $targetPath); - } - - /** - * {@inheritdoc} - */ - public function write($key, $content) - { - $this->initialize(); - - $path = $this->computePath($key); - $this->ensureDirectoryExists(\Gaufrette\Util\Path::dirname($path), true); - $numBytes = $this->sftp->write($path, $content); - - return $numBytes; - } - - /** - * {@inheritdoc} - */ - public function exists($key) - { - $this->initialize(); - - $url = $this->sftp->getUrl($this->computePath($key)); - clearstatcache(); - - return file_exists($url); - } - - /** - * {@inheritdoc} - */ - public function isDirectory($key) - { - $this->initialize(); - - $url = $this->sftp->getUrl($this->computePath($key)); - - return is_dir($url); - } - - /** - * {@inheritdoc} - */ - public function keys() - { - $this->initialize(); - $results = $this->sftp->listDirectory($this->directory, true); - $files = array_map([$this, 'computeKey'], $results['files']); - - $dirs = []; - foreach ($files as $file) { - if ('.' !== $dirname = \Gaufrette\Util\Path::dirname($file)) { - $dirs[] = $dirname; - } - } - - $keys = array_merge($files, $dirs); - sort($keys); - - return $keys; - } - - /** - * {@inheritdoc} - */ - public function mtime($key) - { - $this->initialize(); - - return filemtime($this->sftp->getUrl($this->computePath($key))); - } - - /** - * {@inheritdoc} - */ - public function checksum($key) - { - $this->initialize(); - - if ($this->exists($key)) { - return md5_file($this->sftp->getUrl($this->computePath($key))); - } - - return false; - } - - /** - * {@inheritdoc} - */ - public function delete($key) - { - $this->initialize(); - - return unlink($this->sftp->getUrl($this->computePath($key))); - } - - /** - * Computes the key from the specified path. - * - * @param string $path - * - * @return string - */ - public function computeKey($path) - { - if (0 !== strpos($path, $this->directory)) { - throw new \OutOfBoundsException(sprintf('The path \'%s\' is out of the filesystem.', $path)); - } - - return ltrim(substr($path, strlen($this->directory)), '/'); - } - - /** - * Computes the path for the specified key. - * - * @param string $key - * - * @return string - */ - protected function computePath($key) - { - return $this->directory . '/' . ltrim($key, '/'); - } - - /** - * Performs the adapter's initialization. - * - * It will ensure the root directory exists - */ - protected function initialize() - { - if ($this->initialized) { - return; - } - - $this->ensureDirectoryExists($this->directory, $this->create); - $this->initialized = true; - } - - /** - * Ensures the specified directory exists. - * - * @param string $directory The directory that we ensure the existence - * @param bool $create Whether to create it if it does not exist - * - * @throws RuntimeException if the specified directory does not exist and - * could not be created - */ - protected function ensureDirectoryExists($directory, $create = false) - { - $url = $this->sftp->getUrl($directory); - - $resource = @opendir($url); - if (false === $resource && (!$create || !$this->createDirectory($directory))) { - throw new \RuntimeException(sprintf('The directory \'%s\' does not exist and could not be created.', $directory)); - } - - // make sure we don't leak the resource - if (is_resource($resource)) { - closedir($resource); - } - } - - /** - * Creates the specified directory and its parents. - * - * @param string $directory The directory to create - * - * @return bool TRUE on success, or FALSE on failure - */ - protected function createDirectory($directory) - { - return $this->sftp->mkdir($directory, 0777, true); - } -} diff --git a/tests/Gaufrette/Functional/Adapter/AmazonS3Test.php b/tests/Gaufrette/Functional/Adapter/AmazonS3Test.php deleted file mode 100644 index 4c0a18d0f..000000000 --- a/tests/Gaufrette/Functional/Adapter/AmazonS3Test.php +++ /dev/null @@ -1,7 +0,0 @@ -markTestSkipped('The APC extension is not available.'); - } elseif (!filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) || !filter_var(ini_get('apc.enable_cli'), FILTER_VALIDATE_BOOLEAN)) { - return $this->markTestSkipped('The APC extension is available, but not enabled.'); - } - - apc_clear_cache('user'); - - $this->filesystem = new Filesystem(new Apc('gaufrette-test.')); - } - - protected function tearDown() - { - parent::tearDown(); - if (extension_loaded('apc')) { - apc_clear_cache('user'); - } - } -} diff --git a/tests/Gaufrette/Functional/Adapter/CacheTest.php b/tests/Gaufrette/Functional/Adapter/CacheTest.php deleted file mode 100644 index 8138fbe70..000000000 --- a/tests/Gaufrette/Functional/Adapter/CacheTest.php +++ /dev/null @@ -1,33 +0,0 @@ -filesystem = new Filesystem(new Cache(new InMemory(), new InMemory())); - } - - /** - * @test - */ - public function shouldNeedReloadAfterSourceChanged() - { - $source = new InMemory(); - $cache = new InMemory(); - $cachedFs = new Cache($source, $cache); - - // The source has been updated after the cache has been created. - $mtime = time(); - $source->setFile('foo', 'baz', $mtime - 10); - $cache->setFile('foo', 'bar', $mtime - 20); - - $this->assertTrue($cachedFs->needsReload('foo')); - $this->assertEquals('baz', $cachedFs->read('foo')); - } -} diff --git a/tests/Gaufrette/Functional/Adapter/DropboxTest.php b/tests/Gaufrette/Functional/Adapter/DropboxTest.php deleted file mode 100644 index 6058ceb82..000000000 --- a/tests/Gaufrette/Functional/Adapter/DropboxTest.php +++ /dev/null @@ -1,7 +0,0 @@ -markTestSkipped('Not supported by the adapter.'); - } - - /** - * @test - * @group functional - */ - public function shouldGetMtimeNonExistingFile() - { - $this->markTestSkipped('Not supported by the adapter.'); - } -} diff --git a/tests/Gaufrette/Functional/Adapter/SftpTest.php b/tests/Gaufrette/Functional/Adapter/SftpTest.php deleted file mode 100644 index 95841083c..000000000 --- a/tests/Gaufrette/Functional/Adapter/SftpTest.php +++ /dev/null @@ -1,15 +0,0 @@ -markTestSkipped('Extension ssh2 not loaded'); - } - - return parent::setUp(); - } -} From 8bbfa95afa5516744b12147c6dae309072f67241 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Mon, 5 Oct 2020 15:36:57 +0200 Subject: [PATCH 390/426] Test on PHP 7.4 (#640) --- .github/workflows/ci.yml | 76 +++++++++++++ .github/workflows/static.yml | 64 ++++++++++- .travis.yml | 10 +- Makefile | 22 ++++ composer.json | 30 +---- phpspec.yml | 2 - phpstan-baseline.neon | 103 ++++++------------ phpstan.neon.dist | 3 + src/Gaufrette/Adapter/AwsS3.php | 3 + src/Gaufrette/Adapter/AzureBlobStorage.php | 3 + .../AzureBlobStorage/BlobProxyFactory.php | 3 + src/Gaufrette/Adapter/DoctrineDbal.php | 4 + src/Gaufrette/Adapter/Flysystem.php | 4 + src/Gaufrette/Adapter/GoogleCloudStorage.php | 4 + src/Gaufrette/Adapter/GridFS.php | 3 + src/Gaufrette/Adapter/OpenCloud.php | 3 + src/Gaufrette/Adapter/PhpseclibSftp.php | 3 + src/Gaufrette/Adapter/Zip.php | 5 +- .../Functional/Adapter/DoctrineDbalTest.php | 8 ++ 19 files changed, 240 insertions(+), 113 deletions(-) create mode 100644 .github/workflows/ci.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..1ba9d59d9 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,76 @@ +name: Tests +on: [push, pull_request] + +jobs: + + build: + name: Build + runs-on: ubuntu-latest + strategy: + max-parallel: 10 + matrix: + php: ['7.1', '7.2', '7.3', '7.4'] + + steps: + - name: Set up PHP + uses: shivammathur/setup-php@2.1.0 + with: + php-version: ${{ matrix.php }} + coverage: none + tools: flex + + - name: Checkout code + uses: actions/checkout@v2 + + - name: Download dependencies + run: | + composer update --no-interaction --prefer-dist --optimize-autoloader --prefer-stable + + - name: Remove PHPSpec adapter tests + run: make remove-phpspec + + - name: PHPSpec + run: php vendor/bin/phpspec run -fpretty --verbose + + - name: PHPUnit + run: php vendor/bin/phpunit + + adapter: + name: Adapter + runs-on: ubuntu-latest + strategy: + max-parallel: 10 + matrix: + run: + - { php: '7.3', packages: 'aws/aws-sdk-php:^2.4.12', phpspec: 'spec/Gaufrette/Adapter/AwsS3Spec.php' } + - { php: '7.4', packages: 'aws/aws-sdk-php:^3.158', phpspec: 'spec/Gaufrette/Adapter/AwsS3Spec.php' } + - { php: '7.4', packages: 'rackspace/php-opencloud:^1.9.2', phpspec: 'spec/Gaufrette/Adapter/OpenCloudSpec.php' } + - { php: '7.4', packages: 'google/apiclient:^1.1.3', phpspec: 'spec/Gaufrette/Adapter/GoogleCloudStorageSpec.php' } + - { php: '7.4', packages: 'doctrine/dbal:^2.3', phpspec: 'spec/Gaufrette/Adapter/DoctrineDbalSpec.php' } + - { php: '7.4', packages: 'league/flysystem:^1.0', phpspec: 'spec/Gaufrette/Adapter/FlysystemSpec.php' } + - { php: '7.4', packages: 'microsoft/azure-storage-blob:^1.0', phpspec: 'spec/Gaufrette/Adapter/AzureBlobStore' } + - { php: '7.4', packages: 'mongodb/mongodb:^1.1', phpspec: 'spec/Gaufrette/Adapter/GridFSSpec.php' } + - { php: '7.4', packages: 'phpseclib/phpseclib:^2.0', phpspec: 'spec/Gaufrette/Adapter/PhpseclibSftpSpec.php' } + + steps: + - name: Set up PHP + uses: shivammathur/setup-php@2.1.0 + with: + php-version: ${{ matrix.run.php }} + coverage: none + tools: flex + + - name: Checkout code + uses: actions/checkout@v2 + + - name: Download dependencies + run: | + composer req ${{ matrix.run.packages }} --no-update + composer update --no-interaction --prefer-dist --optimize-autoloader --prefer-stable + + - name: PHPSpec + if: ${{ matrix.run.phpspec }} + run: php vendor/bin/phpspec run -fpretty --verbose ${{ matrix.run.phpspec }} + + - name: PHPUnit + run: php vendor/bin/phpunit diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 5ce1d7ec0..817027fe6 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -5,23 +5,77 @@ jobs: phpstan: name: PHPStan runs-on: ubuntu-latest + steps: + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + extensions: mongo, mbstring, fileinfo + tools: composer:v2 + - name: Checkout code uses: actions/checkout@v2 + - name: Download dependencies + run: | + make require-all + composer require --dev --no-update phpstan/phpstan:0.12.48 + composer update --no-interaction --prefer-dist + - name: PHPStan - uses: docker://oskarstark/phpstan-ga - env: - REQUIRE_DEV: true + run: | + ./vendor/bin/phpstan --version + ./vendor/bin/phpstan analyse + + + php-cs-fixer: + name: PHP-CS-Fixer + runs-on: ubuntu-latest + + steps: + - name: Set up PHP + uses: shivammathur/setup-php@v2 with: - args: analyze --no-progress + php-version: '7.4' + extensions: mongo, mbstring, fileinfo + tools: composer:v2 + + - name: Checkout code + uses: actions/checkout@v2 + + - name: Download dependencies + run: | + make require-all + composer require --dev --no-update pedrotroller/php-cs-custom-fixer:2.23.0 + composer update --no-interaction --prefer-dist + + - name: PHP-CS-Fixer + run: | + ./vendor/bin/php-cs-fixer --version + ./vendor/bin/php-cs-fixer fix --diff --diff-format udiff --dry-run --show-progress=none psalm: name: Psalm runs-on: ubuntu-latest steps: + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '7.4' + extensions: mongo, mbstring, fileinfo + tools: composer:v2 + - name: Checkout code uses: actions/checkout@v2 + - name: Download dependencies + run: | + make require-all + composer require --dev --no-update psalm/phar:3.16 + composer update --no-interaction --prefer-dist + - name: Psalm - uses: docker://vimeo/psalm-github-actions + run: | + ./vendor/bin/psalm.phar --version + ./vendor/bin/psalm.phar --output-format=github --no-progress --show-info=false --stats diff --git a/.travis.yml b/.travis.yml index 6e25d546b..4acb25b6f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,9 +11,7 @@ services: - docker env: - - MONGO_URI="mongodb://127.0.0.1:27017" - MONGO_DBNAME="gridfs_test" - SFTP_HOST=localhost + - SFTP_HOST=localhost SFTP_PORT=2222 SFTP_USER=gaufrette SFTP_PASSWORD=gaufrette @@ -29,18 +27,18 @@ matrix: - php: '7.1' - php: '7.2' - php: '7.3' + - php: '7.4' before_install: - - echo "extension = mongodb.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; - phpenv config-rm xdebug.ini || echo "xdebug not available" - docker run -p ${SFTP_PORT}:22 -d atmoz/sftp:alpine ${SFTP_USER}:${SFTP_PASSWORD}:::${SFTP_BASE_DIR} - docker run -d --name ftpd_server -p ${FTP_PORT}:21 -p 30000-30009:30000-30009 -e "PUBLICHOST=${FTP_HOST}" stilliard/pure-ftpd - docker exec -it ftpd_server sh -c "(echo ${FTP_PASSWORD}; echo ${FTP_PASSWORD}) | pure-pw useradd ${FTP_USER} -f /etc/pure-ftpd/passwd/pureftpd.passwd -m -u ftpuser -d /home/ftpusers/${FTP_USER}" install: + - composer require phpseclib/phpseclib:^2.0 --no-update - composer update --prefer-dist --no-progress --no-suggest --ansi script: - - php vendor/bin/php-cs-fixer fix --diff --diff-format udiff --dry-run --show-progress=none --verbose - - php vendor/bin/phpspec run -fpretty --verbose + - php vendor/bin/phpspec run -fpretty spec/Gaufrette/Adapter/PhpseclibSftpSpec.php - php vendor/bin/phpunit --verbose diff --git a/Makefile b/Makefile index 7b7a00852..2e6eeb870 100644 --- a/Makefile +++ b/Makefile @@ -31,3 +31,25 @@ php-cs-compare: .PHONY: php-cs-fix php-cs-fix: docker/run-task php${PHP_VERSION} vendor/bin/php-cs-fixer fix + +remove-phpspec: + rm spec/Gaufrette/Adapter/AwsS3Spec.php + rm spec/Gaufrette/Adapter/OpenCloudSpec.php + rm spec/Gaufrette/Adapter/GoogleCloudStorageSpec.php + rm spec/Gaufrette/Adapter/DoctrineDbalSpec.php + rm spec/Gaufrette/Adapter/FlysystemSpec.php + rm -r spec/Gaufrette/Adapter/AzureBlobStorage + rm spec/Gaufrette/Adapter/GridFSSpec.php + rm spec/Gaufrette/Adapter/PhpseclibSftpSpec.php + +require-all: + composer require --no-update \ + aws/aws-sdk-php:^3.158 \ + rackspace/php-opencloud:^1.9.2 \ + google/apiclient:^1.1.3 \ + doctrine/dbal:^2.3 \ + league/flysystem:^1.0 \ + microsoft/azure-storage-blob:^1.0 \ + phpseclib/phpseclib:^2.0 \ + mongodb/mongodb:^1.1 \ + diff --git a/composer.json b/composer.json index 0d800af8a..dcf8e05eb 100644 --- a/composer.json +++ b/composer.json @@ -22,36 +22,12 @@ "microsoft/windowsazure": "<0.4.3" }, "require-dev": { - "aws/aws-sdk-php": "^2.4.12||~3", - "rackspace/php-opencloud" : "^1.9.2", - "google/apiclient": "~1.1.3", - "phpspec/phpspec": "~5.1", - "phpseclib/phpseclib": "^2.0", - "doctrine/dbal": ">=2.3", + "phpspec/phpspec": "^5.1 || ^6.2", "phpunit/phpunit": "~7.5", - "mikey179/vfsstream": "~1.2.0", - "league/flysystem": "~1.0", - "mongodb/mongodb": "^1.1", - "microsoft/azure-storage-blob": "^1.0", - "akeneo/phpspec-skip-example-extension": "^4.0", - "pedrotroller/php-cs-custom-fixer": "^2.17" + "mikey179/vfsstream": "v1.x-dev as 1.7.0" }, "suggest": { - "knplabs/knp-gaufrette-bundle": "to use with Symfony2", - "gaufrette/azure-blob-storage-adapter": "to use AzureBlobStorage adapter", - "gaufrette/flysystem-adapter": "to use Flysystem adapter", - "gaufrette/doctrine-dbal-adapter": "to use DBAL adapter", - "gaufrette/phpseclib-sftp-adapter": "to use PhpseclibSftp adapter", - "gaufrette/ftp-adapter": "to use Ftp adapter", - "gaufrette/aws-s3-adapter": "to use AwsS3 adapter (supports SDK v2 and v3)", - "gaufrette/in-memory-adapter": "to use InMemory adapter", - "gaufrette/local-adapter": "to use Local adapter", - "gaufrette/opencloud-adapter": "to use Opencloud adapter", - "gaufrette/zip-adapter": "to use Zip adapter", - "gaufrette/gridfs-adapter": "to use GridFS adapter", - "google/apiclient": "to use GoogleCloudStorage adapter", - "ext-curl": "*", - "ext-mbstring": "*", + "knplabs/knp-gaufrette-bundle": "to use with Symfony", "ext-fileinfo": "This extension is used to automatically detect the content-type of a file in the AwsS3, OpenCloud, AzureBlogStorage and GoogleCloudStorage adapters" }, "autoload": { diff --git a/phpspec.yml b/phpspec.yml index 38cc7d8e3..e69de29bb 100644 --- a/phpspec.yml +++ b/phpspec.yml @@ -1,2 +0,0 @@ -extensions: - Akeneo\SkipExampleExtension: ~ diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 4e5d5d0aa..2c7c957ee 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -1,20 +1,5 @@ parameters: ignoreErrors: - - - message: "#^Strict comparison using \\=\\=\\= between null and string will always evaluate to false\\.$#" - count: 1 - path: src/Gaufrette/Adapter/AmazonS3.php - - - - message: "#^Strict comparison using \\=\\=\\= between null and APCIterator will always evaluate to false\\.$#" - count: 1 - path: src/Gaufrette/Adapter/Apc.php - - - - message: "#^Method Aws\\\\S3\\\\S3Client\\:\\:getObjectUrl\\(\\) invoked with 4 parameters, 2 required\\.$#" - count: 1 - path: src/Gaufrette/Adapter/AwsS3.php - - message: "#^Call to function is_resource\\(\\) with string will always evaluate to false\\.$#" count: 2 @@ -90,11 +75,6 @@ parameters: count: 1 path: src/Gaufrette/Adapter/AzureBlobStorage.php - - - message: "#^Method Gaufrette\\\\Adapter\\\\Cache\\:\\:getMetadata\\(\\) should return array but returns false\\.$#" - count: 1 - path: src/Gaufrette/Adapter/Cache.php - - message: "#^Property Gaufrette\\\\Adapter\\\\Flysystem\\:\\:\\$config has unknown class Gaufrette\\\\Adapter\\\\Config as its type\\.$#" count: 1 @@ -121,74 +101,74 @@ parameters: path: src/Gaufrette/Adapter/Ftp.php - - message: "#^Reflection error\\: Google_Task_Retryable not found\\.$#" - count: 2 + message: "#^Call to function is_resource\\(\\) with string will always evaluate to false\\.$#" + count: 1 path: src/Gaufrette/Adapter/GoogleCloudStorage.php - - message: "#^Class MongoDB\\\\Driver\\\\Exception\\\\RuntimeException not found\\.$#" - count: 2 + message: "#^Property Gaufrette\\\\Adapter\\\\GridFS\\:\\:\\$bucket has unknown class MongoDB\\\\GridFS\\\\Bucket as its type\\.$#" + count: 1 path: src/Gaufrette/Adapter/GridFS.php - - message: "#^Negated boolean expression is always false\\.$#" - count: 1 - path: src/Gaufrette/Adapter/LazyOpenCloud.php + message: "#^Parameter \\$bucket of method Gaufrette\\\\Adapter\\\\GridFS\\:\\:__construct\\(\\) has invalid typehint type MongoDB\\\\GridFS\\\\Bucket\\.$#" + count: 2 + path: src/Gaufrette/Adapter/GridFS.php - - message: "#^PHPDoc tag @param has invalid value \\(\\$path\\)\\: Unexpected token \"\\$path\", expected type at offset 74$#" + message: "#^Call to method openDownloadStreamByName\\(\\) on an unknown class MongoDB\\\\GridFS\\\\Bucket\\.$#" count: 1 - path: src/Gaufrette/Adapter/Local.php + path: src/Gaufrette/Adapter/GridFS.php - - message: "#^PHPDoc tag @param has invalid value \\(string The directory's path to delete\\)\\: Unexpected token \"The\", expected variable at offset 25$#" - count: 1 - path: src/Gaufrette/Adapter/Local.php + message: "#^Caught class MongoDB\\\\GridFS\\\\Exception\\\\FileNotFoundException not found\\.$#" + count: 2 + path: src/Gaufrette/Adapter/GridFS.php - - message: "#^PHPDoc tag @param has invalid value \\(domain MogileFS domain\\)\\: Unexpected token \"MogileFS\", expected variable at offset 25$#" - count: 1 - path: src/Gaufrette/Adapter/MogileFS.php + message: "#^Call to method openUploadStream\\(\\) on an unknown class MongoDB\\\\GridFS\\\\Bucket\\.$#" + count: 2 + path: src/Gaufrette/Adapter/GridFS.php - - message: "#^PHPDoc tag @param has invalid value \\(hosts Array of MogileFS trackers\\)\\: Unexpected token \"Array\", expected variable at offset 62$#" + message: "#^Unreachable statement \\- code above always terminates\\.$#" count: 1 - path: src/Gaufrette/Adapter/MogileFS.php + path: src/Gaufrette/Adapter/GridFS.php - - message: "#^Return typehint of method Gaufrette\\\\Adapter\\\\MogileFS\\:\\:connect\\(\\) has invalid type Gaufrette\\\\Adapter\\\\Socket\\.$#" + message: "#^Call to method downloadToStreamByName\\(\\) on an unknown class MongoDB\\\\GridFS\\\\Bucket\\.$#" count: 1 - path: src/Gaufrette/Adapter/MogileFS.php + path: src/Gaufrette/Adapter/GridFS.php - - message: "#^Parameter \\#2 \\$port of function fsockopen expects int, string given\\.$#" - count: 1 - path: src/Gaufrette/Adapter/MogileFS.php + message: "#^Call to method findOne\\(\\) on an unknown class MongoDB\\\\GridFS\\\\Bucket\\.$#" + count: 6 + path: src/Gaufrette/Adapter/GridFS.php - - message: "#^PHPDoc tag @param has invalid value \\(args Array of arguments\\)\\: Unexpected token \"Array\", expected variable at offset 98$#" - count: 1 - path: src/Gaufrette/Adapter/MogileFS.php + message: "#^Call to method find\\(\\) on an unknown class MongoDB\\\\GridFS\\\\Bucket\\.$#" + count: 2 + path: src/Gaufrette/Adapter/GridFS.php - - message: "#^PHPDoc tag @param has invalid value \\(cmd Command\\)\\: Unexpected token \"Command\", expected variable at offset 71$#" + message: "#^Call to method delete\\(\\) on an unknown class MongoDB\\\\GridFS\\\\Bucket\\.$#" count: 1 - path: src/Gaufrette/Adapter/MogileFS.php + path: src/Gaufrette/Adapter/GridFS.php - - message: "#^PHPDoc tag @param has invalid value \\(key File key\\)\\: Unexpected token \"File\", expected variable at offset 87$#" - count: 1 - path: src/Gaufrette/Adapter/MogileFS.php + message: "#^Method Gaufrette\\\\Adapter\\\\GridFS\\:\\:size\\(\\) should return int but returns false\\.$#" + count: 2 + path: src/Gaufrette/Adapter/GridFS.php - - message: "#^PHPDoc tag @param has invalid value \\(data Data to save\\)\\: Unexpected token \"Data\", expected variable at offset 108$#" + message: "#^PHPDoc tag @param has invalid value \\(\\$path\\)\\: Unexpected token \"\\$path\", expected type at offset 74$#" count: 1 - path: src/Gaufrette/Adapter/MogileFS.php + path: src/Gaufrette/Adapter/Local.php - - message: "#^PHPDoc tag @param has invalid value \\(path Save path at server\\)\\: Unexpected token \"Save\", expected variable at offset 69$#" + message: "#^PHPDoc tag @param has invalid value \\(string The directory's path to delete\\)\\: Unexpected token \"The\", expected variable at offset 25$#" count: 1 - path: src/Gaufrette/Adapter/MogileFS.php + path: src/Gaufrette/Adapter/Local.php - message: "#^If condition is always true\\.$#" @@ -220,21 +200,6 @@ parameters: count: 1 path: src/Gaufrette/Adapter/PhpseclibSftp.php - - - message: "#^Parameter \\$sftp of method Gaufrette\\\\Adapter\\\\Sftp\\:\\:__construct\\(\\) has invalid typehint type Ssh\\\\Sftp\\.$#" - count: 2 - path: src/Gaufrette/Adapter/Sftp.php - - - - message: "#^Method Gaufrette\\\\Adapter\\\\Sftp\\:\\:checksum\\(\\) should return string but returns false\\.$#" - count: 1 - path: src/Gaufrette/Adapter/Sftp.php - - - - message: "#^PHPDoc tag @throws with type Gaufrette\\\\Adapter\\\\RuntimeException is not subtype of Throwable$#" - count: 1 - path: src/Gaufrette/Adapter/Sftp.php - - message: "#^PHPDoc tag @param has invalid value \\(\\$key\\)\\: Unexpected token \"\\$key\", expected type at offset 161$#" count: 1 diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 72b5c82e0..0dfc69082 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -6,3 +6,6 @@ parameters: reportUnmatchedIgnoredErrors: false paths: - src + + scanFiles: + - vendor/google/apiclient/src/Google/Task/Retryable.php diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index cc478314d..9cccd7720 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -34,6 +34,9 @@ class AwsS3 implements Adapter, MetadataSupporter, ListKeysAware, SizeCalculator */ public function __construct(S3Client $service, $bucket, array $options = [], $detectContentType = false) { + if (!class_exists(S3Client::class)) { + throw new \LogicException('You need to install package "aws/aws-sdk-php" to use this adapter'); + } $this->service = $service; $this->bucket = $bucket; $this->options = array_replace( diff --git a/src/Gaufrette/Adapter/AzureBlobStorage.php b/src/Gaufrette/Adapter/AzureBlobStorage.php index 256453b94..c6cc9d002 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage.php @@ -68,6 +68,9 @@ class AzureBlobStorage implements Adapter, MetadataSupporter, SizeCalculator, Ch */ public function __construct(BlobProxyFactoryInterface $blobProxyFactory, $containerName = null, $create = false, $detectContentType = true) { + if (!class_exists(Blob::class)) { + throw new \LogicException('You need to install package "microsoft/azure-storage-blob" to use this adapter'); + } $this->blobProxyFactory = $blobProxyFactory; $this->containerName = $containerName; $this->detectContentType = $detectContentType; diff --git a/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php b/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php index d675180aa..06c950584 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php @@ -22,6 +22,9 @@ class BlobProxyFactory implements BlobProxyFactoryInterface */ public function __construct($connectionString) { + if (!class_exists(ServicesBuilder::class)) { + throw new \LogicException('You need to install package "microsoft/azure-storage-blob" to use this adapter'); + } $this->connectionString = $connectionString; } diff --git a/src/Gaufrette/Adapter/DoctrineDbal.php b/src/Gaufrette/Adapter/DoctrineDbal.php index 51d9a8094..d8cae0f91 100644 --- a/src/Gaufrette/Adapter/DoctrineDbal.php +++ b/src/Gaufrette/Adapter/DoctrineDbal.php @@ -31,6 +31,10 @@ class DoctrineDbal implements Adapter, ChecksumCalculator, ListKeysAware */ public function __construct(Connection $connection, $table, array $columns = []) { + if (!class_exists(Connection::class)) { + throw new \LogicException('You need to install package "doctrine/dbal" to use this adapter'); + } + $this->connection = $connection; $this->table = $table; $this->columns = array_replace($this->columns, $columns); diff --git a/src/Gaufrette/Adapter/Flysystem.php b/src/Gaufrette/Adapter/Flysystem.php index 9e1842e7f..aab29e883 100644 --- a/src/Gaufrette/Adapter/Flysystem.php +++ b/src/Gaufrette/Adapter/Flysystem.php @@ -25,6 +25,10 @@ class Flysystem implements Adapter, ListKeysAware */ public function __construct(AdapterInterface $adapter, $config = null) { + if (!interface_exists(AdapterInterface::class)) { + throw new \LogicException('You need to install package "league/flysystem" to use this adapter'); + } + $this->adapter = $adapter; $this->config = Util::ensureConfig($config); } diff --git a/src/Gaufrette/Adapter/GoogleCloudStorage.php b/src/Gaufrette/Adapter/GoogleCloudStorage.php index 24c834003..cc9d71606 100644 --- a/src/Gaufrette/Adapter/GoogleCloudStorage.php +++ b/src/Gaufrette/Adapter/GoogleCloudStorage.php @@ -32,6 +32,10 @@ public function __construct( array $options = [], $detectContentType = false ) { + if (!class_exists(\Google_Service_Storage::class)) { + throw new \LogicException('You need to install package "google/apiclient" to use this adapter'); + } + $this->service = $service; $this->bucket = $bucket; $this->options = array_replace( diff --git a/src/Gaufrette/Adapter/GridFS.php b/src/Gaufrette/Adapter/GridFS.php index 49d9b93fb..de9bee6d0 100644 --- a/src/Gaufrette/Adapter/GridFS.php +++ b/src/Gaufrette/Adapter/GridFS.php @@ -27,6 +27,9 @@ class GridFS implements Adapter, ChecksumCalculator, MetadataSupporter, ListKeys */ public function __construct(Bucket $bucket) { + if (!class_exists(Bucket::class)) { + throw new \LogicException('You need to install package "mongodb/mongodb" to use this adapter'); + } $this->bucket = $bucket; } diff --git a/src/Gaufrette/Adapter/OpenCloud.php b/src/Gaufrette/Adapter/OpenCloud.php index 1a739c0c0..165ed93e2 100644 --- a/src/Gaufrette/Adapter/OpenCloud.php +++ b/src/Gaufrette/Adapter/OpenCloud.php @@ -46,6 +46,9 @@ class OpenCloud implements Adapter, ChecksumCalculator */ public function __construct(Service $objectStore, $containerName, $createContainer = false) { + if (!class_exists(Service::class)) { + throw new \LogicException('You need to install package "rackspace/php-opencloud" to use this adapter'); + } $this->objectStore = $objectStore; $this->containerName = $containerName; $this->createContainer = $createContainer; diff --git a/src/Gaufrette/Adapter/PhpseclibSftp.php b/src/Gaufrette/Adapter/PhpseclibSftp.php index e8b460f15..2e71d625c 100644 --- a/src/Gaufrette/Adapter/PhpseclibSftp.php +++ b/src/Gaufrette/Adapter/PhpseclibSftp.php @@ -22,6 +22,9 @@ class PhpseclibSftp implements Adapter, FileFactory, ListKeysAware */ public function __construct(SecLibSFTP $sftp, $directory = null, $create = false) { + if (!class_exists(SecLibSFTP::class)) { + throw new \LogicException('You need to install package "phpseclib/phpseclib" to use this adapter'); + } $this->sftp = $sftp; $this->directory = $directory; $this->create = $create; diff --git a/src/Gaufrette/Adapter/Zip.php b/src/Gaufrette/Adapter/Zip.php index 0f68a8a8d..ec7af7ecd 100644 --- a/src/Gaufrette/Adapter/Zip.php +++ b/src/Gaufrette/Adapter/Zip.php @@ -27,10 +27,7 @@ class Zip implements Adapter public function __construct($zipFile) { if (!extension_loaded('zip')) { - throw new \RuntimeException(sprintf( - 'Unable to use %s as the ZIP extension is not available.', - __CLASS__ - )); + throw new \RuntimeException(sprintf('Unable to use %s as the ZIP extension is not available.', __CLASS__)); } $this->zipFile = $zipFile; diff --git a/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php b/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php index 67b66c924..890a3969c 100644 --- a/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php +++ b/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php @@ -11,6 +11,14 @@ class DoctrineDbalTest extends FunctionalTestCase /** @var \Doctrine\DBAL\Connection */ private $connection; + public static function setUpBeforeClass() + { + if (!class_exists(\DriverManager::class)) { + self::markTestSkipped('Package doctrine/dbal is not installed'); + } + parent::setUpBeforeClass(); + } + protected function setUp() { $this->connection = DriverManager::getConnection([ From a786092feaa54d29b549e91d841998777ab16e3b Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Mon, 5 Oct 2020 20:18:50 +0200 Subject: [PATCH 391/426] Better testing on protected branches (#643) * Better testing * Add back mongodb * minors * minor * Dont use symfony/event-dispatcher:5 * Bugfixes * Fixed test on appveyor * Skip GridFs::shouldGetMtime() on windows --- .travis.yml | 8 +++++--- Makefile | 1 + appveyor.yml | 2 +- src/Gaufrette/Adapter/AzureBlobStorage.php | 3 --- .../AzureBlobStorage/BlobProxyFactory.php | 2 +- .../Gaufrette/Functional/Adapter/AwsS3Test.php | 17 ----------------- .../Functional/Adapter/DoctrineDbalTest.php | 2 +- .../Gaufrette/Functional/Adapter/GridFSTest.php | 13 +++++++++++++ 8 files changed, 22 insertions(+), 26 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4acb25b6f..1e3959e7f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,7 +11,9 @@ services: - docker env: - - SFTP_HOST=localhost + - MONGO_URI="mongodb://127.0.0.1:27017" + MONGO_DBNAME="gridfs_test" + SFTP_HOST=localhost SFTP_PORT=2222 SFTP_USER=gaufrette SFTP_PASSWORD=gaufrette @@ -30,15 +32,15 @@ matrix: - php: '7.4' before_install: + - echo "extension = mongodb.so" >> ~/.phpenv/versions/$(phpenv version-name)/etc/php.ini; - phpenv config-rm xdebug.ini || echo "xdebug not available" - docker run -p ${SFTP_PORT}:22 -d atmoz/sftp:alpine ${SFTP_USER}:${SFTP_PASSWORD}:::${SFTP_BASE_DIR} - docker run -d --name ftpd_server -p ${FTP_PORT}:21 -p 30000-30009:30000-30009 -e "PUBLICHOST=${FTP_HOST}" stilliard/pure-ftpd - docker exec -it ftpd_server sh -c "(echo ${FTP_PASSWORD}; echo ${FTP_PASSWORD}) | pure-pw useradd ${FTP_USER} -f /etc/pure-ftpd/passwd/pureftpd.passwd -m -u ftpuser -d /home/ftpusers/${FTP_USER}" install: - - composer require phpseclib/phpseclib:^2.0 --no-update + - make require-all - composer update --prefer-dist --no-progress --no-suggest --ansi script: - - php vendor/bin/phpspec run -fpretty spec/Gaufrette/Adapter/PhpseclibSftpSpec.php - php vendor/bin/phpunit --verbose diff --git a/Makefile b/Makefile index 2e6eeb870..ed9de5036 100644 --- a/Makefile +++ b/Makefile @@ -52,4 +52,5 @@ require-all: microsoft/azure-storage-blob:^1.0 \ phpseclib/phpseclib:^2.0 \ mongodb/mongodb:^1.1 \ + symfony/event-dispatcher:^4.4 diff --git a/appveyor.yml b/appveyor.yml index efc4bb3b0..12c6081d8 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -61,9 +61,9 @@ install: - IF %PHP%==1 echo openssl.cafile=c:\php\cacert.pem >> php.ini - cd C:\projects\gaufrette - curl -fsSL -o composer.phar https://getcomposer.org/composer.phar + - php composer.phar require --no-update aws/aws-sdk-php:^3.158 rackspace/php-opencloud:^1.9.2 google/apiclient:^1.1.3 doctrine/dbal:^2.3 league/flysystem:^1.0 microsoft/azure-storage-blob:^1.0 phpseclib/phpseclib:^2.0 mongodb/mongodb:^1.1 symfony/event-dispatcher:^4.4 - php composer.phar update --prefer-dist --no-interaction --no-progress --no-suggest --ansi test_script: - cd C:\projects\gaufrette - - vendor\bin\phpspec run -fpretty --verbose - vendor\bin\phpunit --verbose -c phpunit.xml.dist diff --git a/src/Gaufrette/Adapter/AzureBlobStorage.php b/src/Gaufrette/Adapter/AzureBlobStorage.php index c6cc9d002..256453b94 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage.php @@ -68,9 +68,6 @@ class AzureBlobStorage implements Adapter, MetadataSupporter, SizeCalculator, Ch */ public function __construct(BlobProxyFactoryInterface $blobProxyFactory, $containerName = null, $create = false, $detectContentType = true) { - if (!class_exists(Blob::class)) { - throw new \LogicException('You need to install package "microsoft/azure-storage-blob" to use this adapter'); - } $this->blobProxyFactory = $blobProxyFactory; $this->containerName = $containerName; $this->detectContentType = $detectContentType; diff --git a/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php b/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php index 06c950584..441efae4e 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php @@ -22,7 +22,7 @@ class BlobProxyFactory implements BlobProxyFactoryInterface */ public function __construct($connectionString) { - if (!class_exists(ServicesBuilder::class)) { + if (!class_exists(ServicesBuilder::class) && !class_exists(BlobRestProxy::class)) { throw new \LogicException('You need to install package "microsoft/azure-storage-blob" to use this adapter'); } $this->connectionString = $connectionString; diff --git a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php index 1a786f4a3..fdf36aea5 100644 --- a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php +++ b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php @@ -107,14 +107,6 @@ public function shouldCheckForObjectExistence() $this->assertTrue($this->filesystem->has('foo')); } - /** - * @test - */ - public function shouldGetObjectUrls() - { - $this->assertNotEmpty($this->filesystem->getAdapter()->getUrl('foo')); - } - /** * @test */ @@ -126,15 +118,6 @@ public function shouldCheckForObjectExistenceWithDirectory() $this->assertTrue($this->filesystem->has('foo')); } - /** - * @test - */ - public function shouldGetObjectUrlsWithDirectory() - { - $this->createFilesystem(['directory' => 'bar']); - $this->assertNotEmpty($this->filesystem->getAdapter()->getUrl('foo')); - } - /** * @test */ diff --git a/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php b/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php index 890a3969c..462cfad46 100644 --- a/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php +++ b/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php @@ -13,7 +13,7 @@ class DoctrineDbalTest extends FunctionalTestCase public static function setUpBeforeClass() { - if (!class_exists(\DriverManager::class)) { + if (!class_exists(DriverManager::class)) { self::markTestSkipped('Package doctrine/dbal is not installed'); } parent::setUpBeforeClass(); diff --git a/tests/Gaufrette/Functional/Adapter/GridFSTest.php b/tests/Gaufrette/Functional/Adapter/GridFSTest.php index 041dd2183..82468356c 100644 --- a/tests/Gaufrette/Functional/Adapter/GridFSTest.php +++ b/tests/Gaufrette/Functional/Adapter/GridFSTest.php @@ -110,4 +110,17 @@ public function shouldRetrieveEmptyMetadata() $this->assertEquals([], $this->filesystem->getAdapter()->getMetadata('no-metadata.txt')); } + + /** + * @test + * @group functional + */ + public function shouldGetMtime() + { + if (strtolower(substr(PHP_OS, 0, 3)) === 'win') { + $this->markTestSkipped('Not working on Windows.'); + } else { + parent::shouldGetMtime(); + } + } } From dfcc2e656c538231b01c2ec0cdcb2e5744a88516 Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Mon, 5 Oct 2020 21:14:33 +0200 Subject: [PATCH 392/426] Adding support for AsyncAws (#639) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Adding support for AsyncAws * Added PHPSpec test * cs * Update doc/adapters/async-aws-s3.md Co-authored-by: Jérémy Derussé * Fixes * cs * cs * Minors * cs * fixes * minor * fix * Update make files Co-authored-by: Jérémy Derussé --- .github/workflows/ci.yml | 1 + .travis.yml | 2 +- Makefile | 7 +- README.md | 1 + appveyor.yml | 2 +- doc/adapters/async-aws-s3.md | 31 ++ spec/Gaufrette/Adapter/AsyncAwsS3Spec.php | 43 +++ src/Gaufrette/Adapter/AsyncAwsS3.php | 339 ++++++++++++++++++ .../Functional/Adapter/AsyncAwsS3Test.php | 145 ++++++++ 9 files changed, 568 insertions(+), 3 deletions(-) create mode 100644 doc/adapters/async-aws-s3.md create mode 100644 spec/Gaufrette/Adapter/AsyncAwsS3Spec.php create mode 100644 src/Gaufrette/Adapter/AsyncAwsS3.php create mode 100644 tests/Gaufrette/Functional/Adapter/AsyncAwsS3Test.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1ba9d59d9..920dd53f2 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,6 +42,7 @@ jobs: max-parallel: 10 matrix: run: + - { php: '7.3', packages: 'async-aws/simple-s3:^0.1.1', phpspec: 'spec/Gaufrette/Adapter/AsyncAwsS3Spec.php' } - { php: '7.3', packages: 'aws/aws-sdk-php:^2.4.12', phpspec: 'spec/Gaufrette/Adapter/AwsS3Spec.php' } - { php: '7.4', packages: 'aws/aws-sdk-php:^3.158', phpspec: 'spec/Gaufrette/Adapter/AwsS3Spec.php' } - { php: '7.4', packages: 'rackspace/php-opencloud:^1.9.2', phpspec: 'spec/Gaufrette/Adapter/OpenCloudSpec.php' } diff --git a/.travis.yml b/.travis.yml index 1e3959e7f..70deaa79e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -39,7 +39,7 @@ before_install: - docker exec -it ftpd_server sh -c "(echo ${FTP_PASSWORD}; echo ${FTP_PASSWORD}) | pure-pw useradd ${FTP_USER} -f /etc/pure-ftpd/passwd/pureftpd.passwd -m -u ftpuser -d /home/ftpusers/${FTP_USER}" install: - - make require-all + - make require-all-legacy - composer update --prefer-dist --no-progress --no-suggest --ansi script: diff --git a/Makefile b/Makefile index ed9de5036..cea370e76 100644 --- a/Makefile +++ b/Makefile @@ -33,6 +33,7 @@ php-cs-fix: docker/run-task php${PHP_VERSION} vendor/bin/php-cs-fixer fix remove-phpspec: + rm spec/Gaufrette/Adapter/AsyncAwsS3Spec.php rm spec/Gaufrette/Adapter/AwsS3Spec.php rm spec/Gaufrette/Adapter/OpenCloudSpec.php rm spec/Gaufrette/Adapter/GoogleCloudStorageSpec.php @@ -42,7 +43,7 @@ remove-phpspec: rm spec/Gaufrette/Adapter/GridFSSpec.php rm spec/Gaufrette/Adapter/PhpseclibSftpSpec.php -require-all: +require-all-legacy: composer require --no-update \ aws/aws-sdk-php:^3.158 \ rackspace/php-opencloud:^1.9.2 \ @@ -54,3 +55,7 @@ require-all: mongodb/mongodb:^1.1 \ symfony/event-dispatcher:^4.4 + +require-all: require-all-legacy + composer require --no-update async-aws/simple-s3:^0.1.1 + diff --git a/README.md b/README.md index a16fc677a..856615d6f 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,7 @@ your issue or pull request in a timely manner, ping us: | Adapter | Referent | |--------------------|-----------------------------| +| AsyncAws S3 | @Nyholm | | AwsS3 | @NiR- | | AzureBlobStorage | @NiR- | | DoctrineDbal | @pedrotroller, @NicolasNSSM | diff --git a/appveyor.yml b/appveyor.yml index 12c6081d8..422a4597f 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -61,7 +61,7 @@ install: - IF %PHP%==1 echo openssl.cafile=c:\php\cacert.pem >> php.ini - cd C:\projects\gaufrette - curl -fsSL -o composer.phar https://getcomposer.org/composer.phar - - php composer.phar require --no-update aws/aws-sdk-php:^3.158 rackspace/php-opencloud:^1.9.2 google/apiclient:^1.1.3 doctrine/dbal:^2.3 league/flysystem:^1.0 microsoft/azure-storage-blob:^1.0 phpseclib/phpseclib:^2.0 mongodb/mongodb:^1.1 symfony/event-dispatcher:^4.4 + - php composer.phar require --no-update async-aws/simple-s3:^0.1.1 aws/aws-sdk-php:^3.158 rackspace/php-opencloud:^1.9.2 google/apiclient:^1.1.3 doctrine/dbal:^2.3 league/flysystem:^1.0 microsoft/azure-storage-blob:^1.0 phpseclib/phpseclib:^2.0 mongodb/mongodb:^1.1 symfony/event-dispatcher:^4.4 - php composer.phar update --prefer-dist --no-interaction --no-progress --no-suggest --ansi test_script: diff --git a/doc/adapters/async-aws-s3.md b/doc/adapters/async-aws-s3.md new file mode 100644 index 000000000..0bdcdcd87 --- /dev/null +++ b/doc/adapters/async-aws-s3.md @@ -0,0 +1,31 @@ +--- +currentMenu: async-aws-s3 +--- + +# AsyncAws S3 + +First, you will need to install the simple S3 client: +```bash +composer require async-aws/simple-s3 +``` + +In order to use this adapter you'll need an access key and a secret key. + +## Example + +```php + 'your_key_here', + 'accessKeySecret' => 'your_secret', + 'region' => 'eu-west-1', +]); + +$adapter = new AwsS3Adapter($s3client, 'your-bucket-name'); +$filesystem = new Filesystem($adapter); +``` diff --git a/spec/Gaufrette/Adapter/AsyncAwsS3Spec.php b/spec/Gaufrette/Adapter/AsyncAwsS3Spec.php new file mode 100644 index 000000000..6c0ae7408 --- /dev/null +++ b/spec/Gaufrette/Adapter/AsyncAwsS3Spec.php @@ -0,0 +1,43 @@ +beConstructedWith($service, 'bucketName'); + } + + function it_is_initializable() + { + $this->shouldHaveType('Gaufrette\Adapter\AsyncAwsS3'); + } + + function it_is_adapter() + { + $this->shouldHaveType('Gaufrette\Adapter'); + } + + function it_supports_metadata() + { + $this->shouldHaveType('Gaufrette\Adapter\MetadataSupporter'); + } + + function it_supports_sizecalculator() + { + $this->shouldHaveType('Gaufrette\Adapter\SizeCalculator'); + } + + function it_provides_mime_type() + { + $this->shouldHaveType(MimeTypeProvider::class); + } +} diff --git a/src/Gaufrette/Adapter/AsyncAwsS3.php b/src/Gaufrette/Adapter/AsyncAwsS3.php new file mode 100644 index 000000000..f03a9fc31 --- /dev/null +++ b/src/Gaufrette/Adapter/AsyncAwsS3.php @@ -0,0 +1,339 @@ + + * @author Tobias Nyholm + */ +class AsyncAwsS3 implements Adapter, MetadataSupporter, ListKeysAware, SizeCalculator, MimeTypeProvider +{ + /** @var SimpleS3Client */ + protected $service; + /** @var string */ + protected $bucket; + /** @var array */ + protected $options; + /** @var bool */ + protected $bucketExists; + /** @var array */ + protected $metadata = []; + /** @var bool */ + protected $detectContentType; + + /** + * @param SimpleS3Client $service + * @param string $bucket + * @param array $options + * @param bool $detectContentType + */ + public function __construct(SimpleS3Client $service, $bucket, array $options = [], $detectContentType = false) + { + if (!class_exists(SimpleS3Client::class)) { + throw new \LogicException('You need to install package "async-aws/simple-s3" to use this adapter'); + } + $this->service = $service; + $this->bucket = $bucket; + $this->options = array_replace( + [ + 'create' => false, + 'directory' => '', + 'acl' => 'private', + ], + $options + ); + + $this->detectContentType = $detectContentType; + } + + /** + * {@inheritdoc} + */ + public function setMetadata($key, $content) + { + // BC with AmazonS3 adapter + if (isset($content['contentType'])) { + $content['ContentType'] = $content['contentType']; + unset($content['contentType']); + } + + $this->metadata[$key] = $content; + } + + /** + * {@inheritdoc} + */ + public function getMetadata($key) + { + return isset($this->metadata[$key]) ? $this->metadata[$key] : []; + } + + /** + * {@inheritdoc} + */ + public function read($key) + { + $this->ensureBucketExists(); + $options = $this->getOptions($key); + + try { + // Get remote object + $object = $this->service->getObject($options); + // If there's no metadata array set up for this object, set it up + if (!array_key_exists($key, $this->metadata) || !is_array($this->metadata[$key])) { + $this->metadata[$key] = []; + } + // Make remote ContentType metadata available locally + $this->metadata[$key]['ContentType'] = $object->getContentType(); + + return $object->getBody()->getContentAsString(); + } catch (\Exception $e) { + return false; + } + } + + /** + * {@inheritdoc} + */ + public function rename($sourceKey, $targetKey) + { + $this->ensureBucketExists(); + $options = $this->getOptions( + $targetKey, + ['CopySource' => $this->bucket . '/' . $this->computePath($sourceKey)] + ); + + try { + $this->service->copyObject(array_merge($options, $this->getMetadata($targetKey))); + + return $this->delete($sourceKey); + } catch (\Exception $e) { + return false; + } + } + + /** + * {@inheritdoc} + * @param string|resource $content + */ + public function write($key, $content) + { + $this->ensureBucketExists(); + $options = $this->getOptions($key); + unset($options['Bucket'], $options['Key']); + + /* + * If the ContentType was not already set in the metadata, then we autodetect + * it to prevent everything being served up as binary/octet-stream. + */ + if (!isset($options['ContentType']) && $this->detectContentType) { + $options['ContentType'] = $this->guessContentType($content); + } + + try { + $this->service->upload($this->bucket, $this->computePath($key), $content, $options); + + if (is_resource($content)) { + return (int) Util\Size::fromResource($content); + } + + return Util\Size::fromContent($content); + } catch (\Exception $e) { + return false; + } + } + + /** + * {@inheritdoc} + */ + public function exists($key) + { + return $this->service->has($this->bucket, $this->computePath($key)); + } + + /** + * {@inheritdoc} + */ + public function mtime($key) + { + try { + $result = $this->service->headObject($this->getOptions($key)); + + return $result->getLastModified()->getTimestamp(); + } catch (\Exception $e) { + return false; + } + } + + /** + * {@inheritdoc} + */ + public function size($key) + { + $result = $this->service->headObject($this->getOptions($key)); + + return (int) $result->getContentLength(); + } + + public function mimeType($key) + { + $result = $this->service->headObject($this->getOptions($key)); + + return $result->getContentType(); + } + + /** + * {@inheritdoc} + */ + public function keys() + { + return $this->listKeys(); + } + + /** + * {@inheritdoc} + */ + public function listKeys($prefix = '') + { + $this->ensureBucketExists(); + + $options = ['Bucket' => $this->bucket]; + if ((string) $prefix != '') { + $options['Prefix'] = $this->computePath($prefix); + } elseif (!empty($this->options['directory'])) { + $options['Prefix'] = $this->options['directory']; + } + + $keys = []; + $result = $this->service->listObjectsV2($options); + foreach ($result->getContents() as $file) { + $keys[] = $this->computeKey($file->getKey()); + } + + return $keys; + } + + /** + * {@inheritdoc} + */ + public function delete($key) + { + try { + $this->service->deleteObject($this->getOptions($key)); + + return true; + } catch (\Exception $e) { + return false; + } + } + + /** + * {@inheritdoc} + */ + public function isDirectory($key) + { + $result = $this->service->listObjectsV2([ + 'Bucket' => $this->bucket, + 'Prefix' => rtrim($this->computePath($key), '/') . '/', + 'MaxKeys' => 1, + ]); + + foreach ($result->getContents(true) as $file) { + return true; + } + + return false; + } + + /** + * Ensures the specified bucket exists. If the bucket does not exists + * and the create option is set to true, it will try to create the + * bucket. The bucket is created using the same region as the supplied + * client object. + * + * @throws \RuntimeException if the bucket does not exists or could not be + * created + */ + protected function ensureBucketExists() + { + if ($this->bucketExists) { + return true; + } + + if ($this->bucketExists = $this->service->bucketExists(['Bucket' => $this->bucket])->isSuccess()) { + return true; + } + + if (!$this->options['create']) { + throw new \RuntimeException(sprintf( + 'The configured bucket "%s" does not exist.', + $this->bucket + )); + } + + $this->service->createBucket([ + 'Bucket' => $this->bucket, + ]); + $this->bucketExists = true; + + return true; + } + + protected function getOptions($key, array $options = []) + { + $options['ACL'] = $this->options['acl']; + $options['Bucket'] = $this->bucket; + $options['Key'] = $this->computePath($key); + + /* + * Merge global options for adapter, which are set in the constructor, with metadata. + * Metadata will override global options. + */ + $options = array_merge($this->options, $options, $this->getMetadata($key)); + + return $options; + } + + protected function computePath($key) + { + if (empty($this->options['directory'])) { + return $key; + } + + return sprintf('%s/%s', $this->options['directory'], $key); + } + + /** + * Computes the key from the specified path. + * + * @param string $path + * + * return string + */ + protected function computeKey($path) + { + return ltrim(substr($path, strlen($this->options['directory'])), '/'); + } + + /** + * @param string|resource $content + * + * @return string + */ + private function guessContentType($content) + { + $fileInfo = new \finfo(FILEINFO_MIME_TYPE); + + if (is_resource($content)) { + return $fileInfo->file(stream_get_meta_data($content)['uri']); + } + + return $fileInfo->buffer($content); + } +} diff --git a/tests/Gaufrette/Functional/Adapter/AsyncAwsS3Test.php b/tests/Gaufrette/Functional/Adapter/AsyncAwsS3Test.php new file mode 100644 index 000000000..97928c257 --- /dev/null +++ b/tests/Gaufrette/Functional/Adapter/AsyncAwsS3Test.php @@ -0,0 +1,145 @@ +markTestSkipped('Either AWS_KEY and/or AWS_SECRET env variables are not defined.'); + } + + $this->bucket = uniqid(getenv('AWS_BUCKET')); + $this->client = new SimpleS3Client([ + 'region' => 'eu-west-1', + 'accessKeyId' => $key, + 'accessKeySecret' => $secret, + ]); + + $this->createFilesystem(['create' => true]); + } + + protected function tearDown() + { + if ($this->client === null) { + return; + } + + try { + $this->client->deleteBucket(['Bucket' => $this->bucket]); + } catch (\Throwable $e) { + } + } + + private function createFilesystem(array $adapterOptions = []) + { + $this->filesystem = new Filesystem(new AsyncAwsS3($this->client, $this->bucket, $adapterOptions)); + } + + /** + * @test + * @expectedException \RuntimeException + */ + public function shouldThrowExceptionIfBucketMissingAndNotCreating() + { + $this->createFilesystem(); + $this->filesystem->read('foo'); + } + + /** + * @test + */ + public function shouldWriteObjects() + { + $this->assertEquals(7, $this->filesystem->write('foo', 'testing')); + } + + /** + * @test + */ + public function shouldCheckForObjectExistence() + { + $this->filesystem->write('foo', ''); + $this->assertTrue($this->filesystem->has('foo')); + } + + /** + * @test + */ + public function shouldCheckForObjectExistenceWithDirectory() + { + $this->createFilesystem(['directory' => 'bar', 'create' => true]); + $this->filesystem->write('foo', ''); + + $this->assertTrue($this->filesystem->has('foo')); + } + + /** + * @test + */ + public function shouldListKeysWithoutDirectory() + { + $this->assertEquals([], $this->filesystem->listKeys()); + $this->filesystem->write('test.txt', 'some content'); + $this->assertEquals(['test.txt'], $this->filesystem->listKeys()); + } + + /** + * @test + */ + public function shouldListKeysWithDirectory() + { + $this->createFilesystem(['create' => true, 'directory' => 'root/']); + $this->filesystem->write('test.txt', 'some content'); + $this->assertEquals(['test.txt'], $this->filesystem->listKeys()); + $this->assertTrue($this->filesystem->has('test.txt')); + } + + /** + * @test + */ + public function shouldGetKeysWithoutDirectory() + { + $this->filesystem->write('test.txt', 'some content'); + $this->assertEquals(['test.txt'], $this->filesystem->keys()); + } + + /** + * @test + */ + public function shouldGetKeysWithDirectory() + { + $this->createFilesystem(['create' => true, 'directory' => 'root/']); + $this->filesystem->write('test.txt', 'some content'); + $this->assertEquals(['test.txt'], $this->filesystem->keys()); + } + + /** + * @test + */ + public function shouldUploadWithGivenContentType() + { + /** @var AwsS3 $adapter */ + $adapter = $this->filesystem->getAdapter(); + + $adapter->setMetadata('foo', ['ContentType' => 'text/html']); + $this->filesystem->write('foo', ''); + + $this->assertEquals('text/html', $this->filesystem->mimeType('foo')); + } +} From ef5ec9d72c06d21febfa09b36d5c3d8e3af9cf8b Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Mon, 5 Oct 2020 21:26:39 +0200 Subject: [PATCH 393/426] Prepare 0.10.0 (#645) --- CHANGELOG.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f5c940ad..27d1c9da7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ -v0.10.0 - UNRELEASED -==================== +v0.10.0 +======= + +## Added + +- Support for AsyncAws ## Removed From 80694cf0e5059641bdf9791d251f4c519895e6ed Mon Sep 17 00:00:00 2001 From: Tobias Nyholm Date: Fri, 9 Oct 2020 18:50:30 +0200 Subject: [PATCH 394/426] minor (#646) --- tests/Gaufrette/Functional/Adapter/AsyncAwsS3Test.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/Gaufrette/Functional/Adapter/AsyncAwsS3Test.php b/tests/Gaufrette/Functional/Adapter/AsyncAwsS3Test.php index 97928c257..f07095c8a 100644 --- a/tests/Gaufrette/Functional/Adapter/AsyncAwsS3Test.php +++ b/tests/Gaufrette/Functional/Adapter/AsyncAwsS3Test.php @@ -17,6 +17,9 @@ class AsyncAwsS3Test extends FunctionalTestCase protected function setUp() { + if (!class_exists(SimpleS3Client::class)) { + $this->markTestSkipped('You need to install async-aws/simple-s3 to run this test.'); + } $key = getenv('AWS_KEY'); $secret = getenv('AWS_SECRET'); From a7284ee7cf75cfbdcf9350e7acb67cb7e9246e14 Mon Sep 17 00:00:00 2001 From: Maxime Veber Date: Wed, 10 Aug 2022 18:55:41 +0200 Subject: [PATCH 395/426] Some fixes on test suite (#655) --- .github/workflows/ci.yml | 4 ++-- spec/Gaufrette/Adapter/GridFSSpec.php | 4 +++- src/Gaufrette/Adapter/AsyncAwsS3.php | 2 +- src/Gaufrette/Adapter/AwsS3.php | 2 +- src/Gaufrette/Adapter/GoogleCloudStorage.php | 2 +- src/Gaufrette/Adapter/InMemory.php | 2 +- src/Gaufrette/StreamWrapper.php | 2 +- 7 files changed, 10 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 920dd53f2..879afcf89 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: steps: - name: Set up PHP - uses: shivammathur/setup-php@2.1.0 + uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} coverage: none @@ -55,7 +55,7 @@ jobs: steps: - name: Set up PHP - uses: shivammathur/setup-php@2.1.0 + uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.run.php }} coverage: none diff --git a/spec/Gaufrette/Adapter/GridFSSpec.php b/spec/Gaufrette/Adapter/GridFSSpec.php index 2c9d864e0..87ec43f17 100644 --- a/spec/Gaufrette/Adapter/GridFSSpec.php +++ b/spec/Gaufrette/Adapter/GridFSSpec.php @@ -20,7 +20,9 @@ function let(Bucket $bucket) function letGo() { array_map(function ($res) { - @fclose($res); + if (is_resource($res)) { + @fclose($res); + } }, $this->resources); } diff --git a/src/Gaufrette/Adapter/AsyncAwsS3.php b/src/Gaufrette/Adapter/AsyncAwsS3.php index f03a9fc31..f5d064bbc 100644 --- a/src/Gaufrette/Adapter/AsyncAwsS3.php +++ b/src/Gaufrette/Adapter/AsyncAwsS3.php @@ -71,7 +71,7 @@ public function setMetadata($key, $content) */ public function getMetadata($key) { - return isset($this->metadata[$key]) ? $this->metadata[$key] : []; + return $this->metadata[$key] ?? []; } /** diff --git a/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php index 9cccd7720..2d335a1c6 100644 --- a/src/Gaufrette/Adapter/AwsS3.php +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -70,7 +70,7 @@ public function setMetadata($key, $metadata) */ public function getMetadata($key) { - return isset($this->metadata[$key]) ? $this->metadata[$key] : []; + return $this->metadata[$key] ?? []; } /** diff --git a/src/Gaufrette/Adapter/GoogleCloudStorage.php b/src/Gaufrette/Adapter/GoogleCloudStorage.php index cc9d71606..e45db2121 100644 --- a/src/Gaufrette/Adapter/GoogleCloudStorage.php +++ b/src/Gaufrette/Adapter/GoogleCloudStorage.php @@ -328,7 +328,7 @@ public function getMetadata($key) { $path = $this->computePath($key); - return isset($this->metadata[$path]) ? $this->metadata[$path] : []; + return $this->metadata[$path] ?? []; } /** diff --git a/src/Gaufrette/Adapter/InMemory.php b/src/Gaufrette/Adapter/InMemory.php index 644955786..b58cdcb42 100644 --- a/src/Gaufrette/Adapter/InMemory.php +++ b/src/Gaufrette/Adapter/InMemory.php @@ -116,7 +116,7 @@ public function keys() */ public function mtime($key) { - return isset($this->files[$key]['mtime']) ? $this->files[$key]['mtime'] : false; + return $this->files[$key]['mtime'] ?? false; } /** diff --git a/src/Gaufrette/StreamWrapper.php b/src/Gaufrette/StreamWrapper.php index cc8a92ca0..bc62f64ad 100644 --- a/src/Gaufrette/StreamWrapper.php +++ b/src/Gaufrette/StreamWrapper.php @@ -254,7 +254,7 @@ protected function createStream($path) ); $domain = $parts['host']; - $key = substr($parts['path'], 1); + $key = !empty($parts['path']) ? substr($parts['path'], 1) : ''; if (null !== $parts['query']) { $key .= '?' . $parts['query']; From f3537658c37edd7f96c454b829ac61fa4fe60b1a Mon Sep 17 00:00:00 2001 From: Maxime Veber Date: Wed, 10 Aug 2022 18:58:52 +0200 Subject: [PATCH 396/426] Update CI badge --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 856615d6f..c011a16c9 100644 --- a/README.md +++ b/README.md @@ -3,8 +3,7 @@ Gaufrette Gaufrette provides a filesystem abstraction layer. -[![Build Status](https://img.shields.io/travis/KnpLabs/Gaufrette/master.svg?style=flat-square)](http://travis-ci.org/KnpLabs/Gaufrette) -[![AppVeyor Status](https://img.shields.io/appveyor/ci/NiR-/Gaufrette/master.svg?style=flat-square)](https://ci.appveyor.com/project/NiR-/gaufrette) +[![Build Status](https://github.com/KnpLabs/Gaufrette/actions/workflows/ci.yml/badge.svg)](https://github.com/KnpLabs/Gaufrette/actions) [![Quality Score](https://img.shields.io/scrutinizer/g/KnpLabs/Gaufrette.svg?style=flat-square)](https://scrutinizer-ci.com/g/KnpLabs/Gaufrette) [![Packagist Version](https://img.shields.io/packagist/v/KnpLabs/Gaufrette.svg?style=flat-square)](https://packagist.org/packages/KnpLabs/Gaufrette) [![Total Downloads](https://img.shields.io/packagist/dt/KnpLabs/Gaufrette.svg?style=flat-square)](https://packagist.org/packages/KnpLabs/Gaufrette) From e6d0749023d14093e9ea03f8e8c1140c2406336d Mon Sep 17 00:00:00 2001 From: Maxime Veber Date: Sat, 20 Aug 2022 12:59:00 +0200 Subject: [PATCH 397/426] :art: Review makefile (#656) Don't get me wrong: it makes things clear, not things working. This will be the next step :) . --- Makefile | 66 +++++++++++++++++----- README.md | 10 ++-- docker-compose.yml | 6 +- docker/php/Dockerfile | 101 ---------------------------------- docker/php/Dockerfile_php_7_1 | 34 ++++++++++++ docker/php/Dockerfile_php_7_2 | 34 ++++++++++++ docker/php/Dockerfile_php_7_3 | 37 +++++++++++++ 7 files changed, 166 insertions(+), 122 deletions(-) delete mode 100644 docker/php/Dockerfile create mode 100644 docker/php/Dockerfile_php_7_1 create mode 100644 docker/php/Dockerfile_php_7_2 create mode 100644 docker/php/Dockerfile_php_7_3 diff --git a/Makefile b/Makefile index cea370e76..0cab7cbb8 100644 --- a/Makefile +++ b/Makefile @@ -1,27 +1,44 @@ +.DEFAULT_GOAL := help + PHP_VERSION ?= 7.2 +# +### DOCKER +# -------- +# + .PHONY: dev -dev: +docker.dev: ## Prepare the env file before running docker cp .env.dist .env .PHONY: build -build: +docker.build: ## Build the PHP docker image docker-compose build php${PHP_VERSION} .PHONY: install-deps -install-deps: +docker.deps: ## Install dependencies docker/run-task php${PHP_VERSION} composer install +.PHONY: install-all-deps +docker.all-deps: docker.deps ## Install dependencies + docker/run-task php${PHP_VERSION} composer require --no-update \ + aws/aws-sdk-php:^3.158 \ + rackspace/php-opencloud:^1.9.2 \ + google/apiclient:^1.1.3 \ + doctrine/dbal:^2.3 \ + league/flysystem:^1.0 \ + microsoft/azure-storage-blob:^1.0 \ + phpseclib/phpseclib:^2.0 \ + mongodb/mongodb:^1.1 \ + symfony/event-dispatcher:^4.4 \ + async-aws/simple-s3:^0.1.1 + .PHONY: tests -tests: +docker.tests: ## Run tests docker/run-task php${PHP_VERSION} bin/tests -.PHONY: clear-deps -clear-deps: - rm -rf vendor/ composer.lock - .PHONY: php-cs-compare -php-cs-compare: +docker.php-cs-compare: ## Run CS fixer (dry run) docker/run-task php${PHP_VERSION} vendor/bin/php-cs-fixer fix \ --diff \ --dry-run \ @@ -29,10 +46,15 @@ php-cs-compare: --verbose .PHONY: php-cs-fix -php-cs-fix: +docker.php-cs-fix: ## Run CS fixer docker/run-task php${PHP_VERSION} vendor/bin/php-cs-fixer fix -remove-phpspec: +# +### LOCAL TASKS +# ------- +# + +remove-phpspec: ## Remove adapter specs (allows you to run test suite without adapters deps) rm spec/Gaufrette/Adapter/AsyncAwsS3Spec.php rm spec/Gaufrette/Adapter/AwsS3Spec.php rm spec/Gaufrette/Adapter/OpenCloudSpec.php @@ -43,7 +65,7 @@ remove-phpspec: rm spec/Gaufrette/Adapter/GridFSSpec.php rm spec/Gaufrette/Adapter/PhpseclibSftpSpec.php -require-all-legacy: +require-all-legacy: # kept for compatibility with the old CI config, to be removed at some point composer require --no-update \ aws/aws-sdk-php:^3.158 \ rackspace/php-opencloud:^1.9.2 \ @@ -56,6 +78,24 @@ require-all-legacy: symfony/event-dispatcher:^4.4 -require-all: require-all-legacy +require-all: require-all-legacy ## Install all dependencies for adapters composer require --no-update async-aws/simple-s3:^0.1.1 +.PHONY: clear +clear: ## Remove not versioned files + rm -rf vendor/ composer.lock + +# +### OTHERS +# -------- +# + +help: SHELL=/bin/bash +help: ## Dislay this help + @IFS=$$'\n'; for line in `grep -h -E '^[a-zA-Z_#-]+:?.*?## .*$$' $(MAKEFILE_LIST)`; do if [ "$${line:0:2}" = "##" ]; then \ + echo $$line | awk 'BEGIN {FS = "## "}; {printf "\n\033[33m%s\033[0m\n", $$2}'; else \ + echo $$line | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'; fi; \ + done; unset IFS; + @echo "" + @echo "Hint: use 'make command PHP_VERSION=X.X' to specify the PHP version with docker commands." +.PHONY: help diff --git a/README.md b/README.md index c011a16c9..f11748e54 100644 --- a/README.md +++ b/README.md @@ -64,29 +64,29 @@ For `InMemory`, `Local` and `Zip` adapters everyone in this list is considered a ### Development -Requires the latest versions of : +Requires : * docker-ce * docker-compose 1) Create `.env` file : ```bash -$ make dev +$ make docker.dev ``` and configure it as you want. 2) Build the php docker image : ```bash -$ make build +$ make docker.build ``` 3) Install dependencies : ```bash -$ make install-deps +$ make docker.all-deps ``` 4) Run tests : ```bash -$ make tests +$ make docker.tests ``` You can also use a different php version, simply set the `PHP_VERSION` env var diff --git a/docker-compose.yml b/docker-compose.yml index 239b82b1b..8256432e7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -4,7 +4,7 @@ services: php7.1: build: context: . - dockerfile: docker/php/Dockerfile + dockerfile: docker/php/Dockerfile_php_7_1 target: php7.1 env_file: .env volumes: @@ -18,7 +18,7 @@ services: php7.2: build: context: . - dockerfile: docker/php/Dockerfile + dockerfile: docker/php/Dockerfile_php_7_2 target: php7.2 env_file: .env volumes: @@ -32,7 +32,7 @@ services: php7.3: build: context: . - dockerfile: docker/php/Dockerfile + dockerfile: docker/php/Dockerfile_php_7_3 target: php7.3 env_file: .env volumes: diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile deleted file mode 100644 index 30ed72c72..000000000 --- a/docker/php/Dockerfile +++ /dev/null @@ -1,101 +0,0 @@ -# To copy composer binary file -FROM composer:1.8.5 as composer - -################################################################################ - -FROM php:7.1-alpine as php7.1 - -RUN apk add --no-cache \ - autoconf \ - g++\ - git \ - libssh2-dev \ - make \ - zlib-dev \ - && pecl install \ - mongodb \ - ssh2-1.1.2 - -RUN docker-php-ext-install \ - zip \ - && docker-php-ext-enable \ - mongodb \ - ssh2 - -COPY --from=composer /usr/bin/composer /usr/bin/composer -ENV COMPOSER_HOME /var/cache/composer -RUN mkdir /var/cache/composer \ - && chown 1000:1000 /var/cache/composer - -USER 1000 - -RUN composer global require "hirak/prestissimo" --prefer-dist - -WORKDIR /app - -################################################################################ - -FROM php:7.2-alpine as php7.2 - -RUN apk add --no-cache \ - autoconf \ - g++\ - git \ - libssh2-dev \ - make \ - zlib-dev \ - && pecl install \ - mongodb \ - ssh2-1.1.2 - -RUN docker-php-ext-install \ - zip \ - && docker-php-ext-enable \ - mongodb \ - ssh2 - -COPY --from=composer /usr/bin/composer /usr/bin/composer -ENV COMPOSER_HOME /var/cache/composer -RUN mkdir /var/cache/composer \ - && chown 1000:1000 /var/cache/composer - -USER 1000 - -RUN composer global require "hirak/prestissimo" --prefer-dist - -WORKDIR /app - -################################################################################ - -FROM php:7.3-alpine as php7.3 - -RUN apk add --no-cache \ - autoconf \ - g++\ - git \ - # libssh2-dev \ - libzip-dev \ - make \ - zlib-dev \ - && pecl install \ - mongodb - # ssh2-1.1.2 - # ssh2 extension is not availble yet for php7.3 - # see https://serverpilot.io/docs/how-to-install-the-php-ssh2-extension - -RUN docker-php-ext-install \ - zip \ - && docker-php-ext-enable \ - mongodb - # ssh2 - -COPY --from=composer /usr/bin/composer /usr/bin/composer -ENV COMPOSER_HOME /var/cache/composer -RUN mkdir /var/cache/composer \ - && chown 1000:1000 /var/cache/composer - -USER 1000 - -RUN composer global require "hirak/prestissimo" --prefer-dist - -WORKDIR /app diff --git a/docker/php/Dockerfile_php_7_1 b/docker/php/Dockerfile_php_7_1 new file mode 100644 index 000000000..a46286b22 --- /dev/null +++ b/docker/php/Dockerfile_php_7_1 @@ -0,0 +1,34 @@ +# To copy composer binary file +FROM composer:1.8.5 as composer + +################################################################################ + +FROM php:7.1-alpine as php7.1 + +RUN apk add --no-cache \ + autoconf \ + g++\ + git \ + libssh2-dev \ + make \ + zlib-dev \ + && pecl install \ + mongodb \ + ssh2-1.1.2 + +RUN docker-php-ext-install \ + zip \ + && docker-php-ext-enable \ + mongodb \ + ssh2 + +COPY --from=composer /usr/bin/composer /usr/bin/composer +ENV COMPOSER_HOME /var/cache/composer +RUN mkdir /var/cache/composer \ + && chown 1000:1000 /var/cache/composer + +USER 1000 + +RUN composer global require "hirak/prestissimo" --prefer-dist + +WORKDIR /app diff --git a/docker/php/Dockerfile_php_7_2 b/docker/php/Dockerfile_php_7_2 new file mode 100644 index 000000000..d1505292d --- /dev/null +++ b/docker/php/Dockerfile_php_7_2 @@ -0,0 +1,34 @@ +# To copy composer binary file +FROM composer:1.8.5 as composer + +################################################################################ + +FROM php:7.2-alpine as php7.2 + +RUN apk add --no-cache \ + autoconf \ + g++\ + git \ + libssh2-dev \ + make \ + zlib-dev \ + && pecl install \ + mongodb \ + ssh2-1.1.2 + +RUN docker-php-ext-install \ + zip \ + && docker-php-ext-enable \ + mongodb \ + ssh2 + +COPY --from=composer /usr/bin/composer /usr/bin/composer +ENV COMPOSER_HOME /var/cache/composer +RUN mkdir /var/cache/composer \ + && chown 1000:1000 /var/cache/composer + +USER 1000 + +RUN composer global require "hirak/prestissimo" --prefer-dist + +WORKDIR /app diff --git a/docker/php/Dockerfile_php_7_3 b/docker/php/Dockerfile_php_7_3 new file mode 100644 index 000000000..c3a88770c --- /dev/null +++ b/docker/php/Dockerfile_php_7_3 @@ -0,0 +1,37 @@ +# To copy composer binary file +FROM composer:1.8.5 as composer + +################################################################################ + +FROM php:7.3-alpine as php7.3 + +RUN apk add --no-cache \ + autoconf \ + g++\ + git \ + # libssh2-dev \ + libzip-dev \ + make \ + zlib-dev \ + && pecl install \ + mongodb + # ssh2-1.1.2 + # ssh2 extension is not availble yet for php7.3 + # see https://serverpilot.io/docs/how-to-install-the-php-ssh2-extension + +RUN docker-php-ext-install \ + zip \ + && docker-php-ext-enable \ + mongodb + # ssh2 + +COPY --from=composer /usr/bin/composer /usr/bin/composer +ENV COMPOSER_HOME /var/cache/composer +RUN mkdir /var/cache/composer \ + && chown 1000:1000 /var/cache/composer + +USER 1000 + +RUN composer global require "hirak/prestissimo" --prefer-dist + +WORKDIR /app From 75b5eb3fffcbbd55dcd35207f65e71773a9cc9ff Mon Sep 17 00:00:00 2001 From: Maxime Veber Date: Wed, 21 Sep 2022 18:45:20 +0200 Subject: [PATCH 398/426] =?UTF-8?q?=E2=AC=86=EF=B8=8F=20Starts=20dependenc?= =?UTF-8?q?ies=20&=20test=20upgrades=20and=20fixes=20for=20PHP=208!=20(#65?= =?UTF-8?q?8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 5 +- Makefile | 2 +- bin/alltests.sh | 14 ++ composer.json | 6 +- spec/Gaufrette/Adapter/FtpSpec.php | 229 ------------------ spec/Gaufrette/Adapter/functions.php | 215 ---------------- src/Gaufrette/Adapter/AsyncAwsS3.php | 5 +- src/Gaufrette/Adapter/Ftp.php | 1 + .../Functional/Adapter/AsyncAwsS3Test.php | 28 ++- .../Functional/Adapter/AwsS3Test.php | 22 +- .../Adapter/AzureBlobStorageTest.php | 6 +- .../AzureMultiContainerBlobStorageTest.php | 32 +-- .../Functional/Adapter/DoctrineDbalTest.php | 8 +- .../Gaufrette/Functional/Adapter/FtpTest.php | 4 +- .../Functional/Adapter/FunctionalTestCase.php | 33 +-- .../Functional/Adapter/GridFSTest.php | 12 +- .../Functional/Adapter/LocalTest.php | 16 +- .../Functional/Adapter/OpenCloudTest.php | 4 +- .../Functional/Adapter/PhpseclibSftpTest.php | 4 +- .../Functional/Adapter/SafeLocalTest.php | 6 +- .../Gaufrette/Functional/Adapter/ZipTest.php | 10 +- .../FileStream/FunctionalTestCase.php | 10 +- .../FileStream/InMemoryBufferTest.php | 2 +- .../Functional/FileStream/LocalTest.php | 8 +- 24 files changed, 130 insertions(+), 552 deletions(-) create mode 100755 bin/alltests.sh delete mode 100644 spec/Gaufrette/Adapter/FtpSpec.php diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 879afcf89..23fa2b425 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: strategy: max-parallel: 10 matrix: - php: ['7.1', '7.2', '7.3', '7.4'] + php: ['7.4', '8.0', '8.1'] steps: - name: Set up PHP @@ -42,8 +42,7 @@ jobs: max-parallel: 10 matrix: run: - - { php: '7.3', packages: 'async-aws/simple-s3:^0.1.1', phpspec: 'spec/Gaufrette/Adapter/AsyncAwsS3Spec.php' } - - { php: '7.3', packages: 'aws/aws-sdk-php:^2.4.12', phpspec: 'spec/Gaufrette/Adapter/AwsS3Spec.php' } + - { php: '7.4', packages: 'async-aws/simple-s3:^1.0', phpspec: 'spec/Gaufrette/Adapter/AsyncAwsS3Spec.php' } - { php: '7.4', packages: 'aws/aws-sdk-php:^3.158', phpspec: 'spec/Gaufrette/Adapter/AwsS3Spec.php' } - { php: '7.4', packages: 'rackspace/php-opencloud:^1.9.2', phpspec: 'spec/Gaufrette/Adapter/OpenCloudSpec.php' } - { php: '7.4', packages: 'google/apiclient:^1.1.3', phpspec: 'spec/Gaufrette/Adapter/GoogleCloudStorageSpec.php' } diff --git a/Makefile b/Makefile index 0cab7cbb8..0f0e14d0d 100644 --- a/Makefile +++ b/Makefile @@ -79,7 +79,7 @@ require-all-legacy: # kept for compatibility with the old CI config, to be remov require-all: require-all-legacy ## Install all dependencies for adapters - composer require --no-update async-aws/simple-s3:^0.1.1 + composer require --no-update async-aws/simple-s3:^1.0 .PHONY: clear clear: ## Remove not versioned files diff --git a/bin/alltests.sh b/bin/alltests.sh new file mode 100755 index 000000000..48d8103a2 --- /dev/null +++ b/bin/alltests.sh @@ -0,0 +1,14 @@ +#!/bin/sh + +set -o allexport # export all defined variables + +source .env # load dev parameters +# +# +#DIR=$(dirname $0)/.. +# +#cd $DIR + +make require-all +composer install +vendor/bin/phpunit tests/Gaufrette/Functional/Adapter/AsyncAwsS3Test.php diff --git a/composer.json b/composer.json index dcf8e05eb..ef50abb98 100644 --- a/composer.json +++ b/composer.json @@ -16,14 +16,14 @@ } ], "require": { - "php": ">=7.1" + "php": "^7.4 || ^8.0" }, "conflict": { "microsoft/windowsazure": "<0.4.3" }, "require-dev": { - "phpspec/phpspec": "^5.1 || ^6.2", - "phpunit/phpunit": "~7.5", + "phpspec/phpspec": "^7.0", + "phpunit/phpunit": "~8.0", "mikey179/vfsstream": "v1.x-dev as 1.7.0" }, "suggest": { diff --git a/spec/Gaufrette/Adapter/FtpSpec.php b/spec/Gaufrette/Adapter/FtpSpec.php deleted file mode 100644 index 5c5b4441f..000000000 --- a/spec/Gaufrette/Adapter/FtpSpec.php +++ /dev/null @@ -1,229 +0,0 @@ -beConstructedWith('/home/l3l0', 'localhost'); - } - - function it_is_adapter() - { - $this->shouldHaveType('Gaufrette\Adapter'); - } - - function it_supports_native_list_keys() - { - $this->shouldHaveType('Gaufrette\Adapter\ListKeysAware'); - } - - function it_checks_if_file_exists_for_absolute_path() - { - $this->exists('filename')->shouldReturn(true); - $this->exists('aa/filename')->shouldReturn(false); - } - - function it_checks_if_file_exists_for_relative_path() - { - $this->beConstructedWith('/home/l3l0/relative', 'localhost'); - - $this->exists('filename')->shouldReturn(true); - $this->exists('filename2')->shouldReturn(false); - $this->exists('aa/filename')->shouldReturn(false); - $this->exists('some/otherfilename')->shouldReturn(true); - } - - function it_checks_if_dir_exists_for_symlink() - { - $this->exists('www')->shouldReturn(true); - $this->exists('vendor')->shouldReturn(true); - $this->exists('bbb')->shouldReturn(false); - } - - function it_checks_if_dir_exists_with_special_and_unicode_chars_in_name() - { - $this->beConstructedWith('/home/l3l2', 'localhost'); - - $this->exists('a b c d -> žežulička')->shouldReturn(true); - } - - function it_reads_file() - { - $this->read('filename')->shouldReturn('some content'); - } - - function it_does_not_read_file() - { - $this->read('filename2')->shouldReturn(false); - } - - function it_writes_file() - { - $this->write('filename', 'some content')->shouldReturn(12); - } - - function it_does_not_write_file() - { - $this->write('filename2', 'some content')->shouldReturn(false); - } - - function it_renames_file() - { - $this->rename('filename', 'filename2')->shouldReturn(true); - } - - function it_does_not_not_rename_file_when_target_file_is_invalid() - { - $this->rename('filename', 'invalid')->shouldReturn(false); - } - - function it_fetches_keys_without_directories_dots() - { - $this->keys()->shouldReturn(['filename', 'filename.exe', '.htaccess', 'aaa', 'aaa/filename']); - } - - function it_fetches_keys_with_spaces_and_unicode_chars() - { - $this->beConstructedWith('/home/l3l2', 'localhost'); - - $this->keys()->shouldReturn(['Žľuťoučký kůň.pdf', 'a b c d -> žežulička', 'a b c d -> žežulička/do re mi.pdf']); - } - - function it_fetches_keys_recursive() - { - $this->beConstructedWith('/home/l3l3', 'localhost'); - - $this->keys()->shouldReturn(['filename', 'filename.exe', '.htaccess', 'aaa', 'www', 'aaa/filename', 'www/filename', 'www/some', 'www/some/otherfilename']); - } - - function it_lists_keys() - { - $this->listKeys()->shouldReturn([ - 'keys' => ['filename', 'filename.exe', '.htaccess', 'aaa/filename'], - 'dirs' => ['aaa'], - ]); - - $this->listKeys('file')->shouldReturn([ - 'keys' => ['filename', 'filename.exe'], - 'dirs' => [], - ]); - - $this->listKeys('name')->shouldReturn([ - 'keys' => [], - 'dirs' => [], - ]); - - $this->listKeys('aaa')->shouldReturn([ - 'keys' => ['aaa/filename'], - 'dirs' => ['aaa'], - ]); - - $this->listKeys('aaa/')->shouldReturn([ - 'keys' => ['aaa/filename'], - 'dirs' => [], - ]); - } - - function it_fetches_mtime() - { - $this->mtime('filename')->shouldReturn(strtotime('2010-10-10 23:10:10')); - } - - function it_throws_exception_when_mtime_is_not_supported_by_server() - { - $this->shouldThrow(new \RuntimeException('Server does not support ftp_mdtm function.'))->duringMtime('invalid'); - } - - function it_deletes_file() - { - $this->delete('filename')->shouldReturn(true); - } - - function it_does_not_delete_file() - { - $this->delete('invalid')->shouldReturn(false); - } - - /** - * @param \Gaufrette\Filesystem $filesystem - */ - function it_creates_file(Filesystem $filesystem) - { - $this->createFile('filename', $filesystem)->shouldReturnAnInstanceOf('\Gaufrette\File'); - } - - /** - * @param \Gaufrette\Filesystem $filesystem - */ - function it_creates_file_in_not_existing_directory(Filesystem $filesystem) - { - $this->createFile('bb/cc/dd/filename', $filesystem)->shouldReturnAnInstanceOf('\Gaufrette\File'); - } - - function it_checks_if_given_key_is_directory() - { - $this->isDirectory('aaa')->shouldReturn(true); - $this->isDirectory('filename')->shouldReturn(false); - } - - function it_fetches_keys_with_hidden_files() - { - $this->beConstructedWith('/home/l3l1', 'localhost'); - - $this->keys()->shouldReturn(['filename', '.htaccess']); - } - - function it_checks_if_hidden_file_exists() - { - $this->beConstructedWith('/home/l3l1', 'localhost'); - - $this->exists('.htaccess')->shouldReturn(true); - } - - function it_creates_base_directory_without_warning() - { - global $createdDirectory; - $createdDirectory = ''; - - $this->beConstructedWith('/home/l3l0/new', 'localhost', ['create' => true]); - - $this->listDirectory()->shouldReturn(['keys' => [], 'dirs' => []]); - } - - function it_does_not_create_base_directory_and_should_throw_exception() - { - global $createdDirectory; - $createdDirectory = ''; - - $this->beConstructedWith('/home/l3l0/new', 'localhost', ['create' => false]); - - $this->shouldThrow(new \RuntimeException("The directory '/home/l3l0/new' does not exist."))->during('listDirectory', []); - } - - function it_fetches_keys_for_windows() - { - $this->beConstructedWith('C:\Ftp', 'localhost'); - - $this->keys()->shouldReturn(['archive', 'file1.zip', 'file2.zip']); - } - - function it_supports_sizecalculator() - { - $this->shouldImplement('Gaufrette\Adapter\SizeCalculator'); - - $this->size('/path')->shouldReturn(5000); - } - - function it_throws_an_exception_when_size_cant_be_fetched() - { - $this->shouldThrow('RuntimeException')->during('size', ['/erroneous']); - } -} diff --git a/spec/Gaufrette/Adapter/functions.php b/spec/Gaufrette/Adapter/functions.php index d2c7e4ff4..276559bfd 100644 --- a/spec/Gaufrette/Adapter/functions.php +++ b/spec/Gaufrette/Adapter/functions.php @@ -4,213 +4,6 @@ global $createdDirectory; -function ftp_delete($connection, $path) -{ - if ($path === '/home/l3l0/invalid') { - return false; - } - - return true; -} - -function ftp_mdtm($connection, $path) -{ - if ($path === '/home/l3l0/invalid') { - return -1; - } - - return \strtotime('2010-10-10 23:10:10'); -} - -function ftp_rename($connection, $from, $to) -{ - return ! ('/home/l3l0/invalid' === $from or '/home/l3l0/invalid' === $to); -} - -function ftp_fput($connection, $path, $fileResource, $mode) -{ - if ('/home/l3l0/filename' === $path) { - return true; - } - - return false; -} - -function ftp_fget($connection, &$fileResource, $path, $mode) -{ - if ('/home/l3l0/filename' === $path) { - $bytes = \fwrite($fileResource, 'some content'); - - return true; - } - - return false; -} - -function ftp_chdir($connection, $dirname) -{ - if (in_array($dirname, ['/home/l3l0', '/home/l3l0/aaa', '/home/l3l0/relative', '/home/l3l0/relative/some', '/home/l3l1', '/home/l3l2', '/home/l3l2/a b c d -> žežulička', '/home/l3l3', 'C:\Ftp'])) { - return true; - } - - global $createdDirectory; - - if ($createdDirectory && $createdDirectory === $dirname) { - return true; - } - - trigger_error(sprintf('%s: No such file or directory', $dirname), E_USER_WARNING); - - return false; -} - -function ftp_mkdir($connection, $dirname) -{ - if (in_array($dirname, ['/home/l3l0/new'])) { - global $createdDirectory; - $createdDirectory = $dirname; - - return true; - } - - return false; -} - -function ftp_connect($host, $port = 21, $timeout = 90) -{ - if ('localhost' !== $host) { - return false; - } - - return fopen('php://temp', 'r'); -} - -function ftp_close($connection) -{ - return fclose($connection); -} - -function ftp_rawlist($connection, $directory, $recursive = false) -{ - $arguments = explode(' ', $directory, 2); - if ('/home/l3l0' === end($arguments)) { - return [ - 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .', - 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..', - 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 aaa', - '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename', - '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename.exe', - '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .htaccess', - 'lrwxrwxrwx 1 vincent vincent 11 Jul 12 12:16 www -> aaa', - 'lrwxrwxrwx 1 vincent vincent 11 Jul 12 12:16 vendor -> bbb', - ]; - } - - if ('/home/l3l0/aaa' === end($arguments)) { - return [ - '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename', - ]; - } - - if ('/home/l3l0/relative' === end($arguments)) { - return [ - '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename', - '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 some', - ]; - } - - if ('/home/l3l0/relative/some' === end($arguments)) { - return [ - '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 otherfilename', - ]; - } - - if ('/home/l3l1' === end($arguments) && 0 === strpos(reset($arguments), '-al')) { - return [ - 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .', - 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..', - '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename', - '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .htaccess', - ]; - } - - if ('/home/l3l1' === end($arguments) && false === strpos(reset($arguments), '-al')) { - return [ - 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .', - 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..', - '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename', - ]; - } - - if ('/home/l3l2' === end($arguments)) { - return [ - 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .', - 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..', - 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 a b c d -> žežulička', - '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 Žľuťoučký kůň.pdf', - ]; - } - - if ('/home/l3l2/a b c d -> žežulička' === end($arguments)) { - return [ - 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .', - 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..', - '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 do re mi.pdf', - ]; - } - - if ('/home/l3l3' === end($arguments) && '-alR' === reset($arguments)) { - return [ - '/home/l3l3:', - 'total: 12', - 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .', - 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..', - 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 aaa', - '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename', - '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename.exe', - '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .htaccess', - 'drwxrwxrwx 1 vincent vincent 11 Jul 12 12:16 www', - 'lrwxrwxrwx 1 vincent vincent 11 Jul 12 12:16 vendor -> bbb', - '', - '/home/l3l3/aaa:', - 'total: 8', - '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename', - '', - '/home/l3l3/www:', - 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .', - 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..', - '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename', - 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 some', - '', - '/home/l3l3/www/some:', - 'total 5', - 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 .', - 'drwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 ..', - '-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 otherfilename', - ]; - } - - // https://github.com/KnpLabs/Gaufrette/issues/147 - if ('C:\Ftp' === end($arguments)) { - return [ - '05-26-12 08:03PM archive', - '12-04-12 06:57PM 16142 file1.zip', - '12-05-12 04:01PM 16142 file2.zip', - ]; - } - - return []; -} - -function ftp_login($connection, $username, $password) -{ - if ('invalid' === $username) { - return false; - } - - return true; -} - function time() { return \strtotime('2012-10-10 23:10:10'); @@ -274,11 +67,3 @@ function apc_exists($path) return true; } -function ftp_size($connection, $path) -{ - if ($path === '/erroneous') { - return -1; - } - - return 5000; -} diff --git a/src/Gaufrette/Adapter/AsyncAwsS3.php b/src/Gaufrette/Adapter/AsyncAwsS3.php index f5d064bbc..ea088ac43 100644 --- a/src/Gaufrette/Adapter/AsyncAwsS3.php +++ b/src/Gaufrette/Adapter/AsyncAwsS3.php @@ -2,6 +2,7 @@ namespace Gaufrette\Adapter; +use AsyncAws\Core\Configuration; use AsyncAws\SimpleS3\SimpleS3Client; use Gaufrette\Adapter; use Gaufrette\Util; @@ -276,9 +277,11 @@ protected function ensureBucketExists() $this->bucket )); } - $this->service->createBucket([ 'Bucket' => $this->bucket, + 'CreateBucketConfiguration' => [ + 'LocationConstraint' => $this->service->getConfiguration()->get(Configuration::OPTION_REGION) + ], ]); $this->bucketExists = true; diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index 13797677c..d578833e5 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -13,6 +13,7 @@ */ class Ftp implements Adapter, FileFactory, ListKeysAware, SizeCalculator { + /** @var null|resource|\FTP\Connection */ protected $connection = null; protected $directory; protected $host; diff --git a/tests/Gaufrette/Functional/Adapter/AsyncAwsS3Test.php b/tests/Gaufrette/Functional/Adapter/AsyncAwsS3Test.php index f07095c8a..559010188 100644 --- a/tests/Gaufrette/Functional/Adapter/AsyncAwsS3Test.php +++ b/tests/Gaufrette/Functional/Adapter/AsyncAwsS3Test.php @@ -15,7 +15,7 @@ class AsyncAwsS3Test extends FunctionalTestCase /** @var SimpleS3Client */ private $client; - protected function setUp() + protected function setUp(): void { if (!class_exists(SimpleS3Client::class)) { $this->markTestSkipped('You need to install async-aws/simple-s3 to run this test.'); @@ -37,13 +37,17 @@ protected function setUp() $this->createFilesystem(['create' => true]); } - protected function tearDown() + protected function tearDown(): void { if ($this->client === null) { return; } try { + $files = $this->filesystem->listKeys(); + foreach ($files as $file) { + $this->filesystem->delete($file); + } $this->client->deleteBucket(['Bucket' => $this->bucket]); } catch (\Throwable $e) { } @@ -56,10 +60,10 @@ private function createFilesystem(array $adapterOptions = []) /** * @test - * @expectedException \RuntimeException */ - public function shouldThrowExceptionIfBucketMissingAndNotCreating() + public function shouldThrowExceptionIfBucketMissingAndNotCreating(): void { + $this->expectException(\RuntimeException::class); $this->createFilesystem(); $this->filesystem->read('foo'); } @@ -67,7 +71,7 @@ public function shouldThrowExceptionIfBucketMissingAndNotCreating() /** * @test */ - public function shouldWriteObjects() + public function shouldWriteObjects(): void { $this->assertEquals(7, $this->filesystem->write('foo', 'testing')); } @@ -75,7 +79,7 @@ public function shouldWriteObjects() /** * @test */ - public function shouldCheckForObjectExistence() + public function shouldCheckForObjectExistence(): void { $this->filesystem->write('foo', ''); $this->assertTrue($this->filesystem->has('foo')); @@ -84,7 +88,7 @@ public function shouldCheckForObjectExistence() /** * @test */ - public function shouldCheckForObjectExistenceWithDirectory() + public function shouldCheckForObjectExistenceWithDirectory(): void { $this->createFilesystem(['directory' => 'bar', 'create' => true]); $this->filesystem->write('foo', ''); @@ -95,7 +99,7 @@ public function shouldCheckForObjectExistenceWithDirectory() /** * @test */ - public function shouldListKeysWithoutDirectory() + public function shouldListKeysWithoutDirectory(): void { $this->assertEquals([], $this->filesystem->listKeys()); $this->filesystem->write('test.txt', 'some content'); @@ -105,7 +109,7 @@ public function shouldListKeysWithoutDirectory() /** * @test */ - public function shouldListKeysWithDirectory() + public function shouldListKeysWithDirectory(): void { $this->createFilesystem(['create' => true, 'directory' => 'root/']); $this->filesystem->write('test.txt', 'some content'); @@ -116,7 +120,7 @@ public function shouldListKeysWithDirectory() /** * @test */ - public function shouldGetKeysWithoutDirectory() + public function shouldGetKeysWithoutDirectory(): void { $this->filesystem->write('test.txt', 'some content'); $this->assertEquals(['test.txt'], $this->filesystem->keys()); @@ -125,7 +129,7 @@ public function shouldGetKeysWithoutDirectory() /** * @test */ - public function shouldGetKeysWithDirectory() + public function shouldGetKeysWithDirectory(): void { $this->createFilesystem(['create' => true, 'directory' => 'root/']); $this->filesystem->write('test.txt', 'some content'); @@ -135,7 +139,7 @@ public function shouldGetKeysWithDirectory() /** * @test */ - public function shouldUploadWithGivenContentType() + public function shouldUploadWithGivenContentType(): void { /** @var AwsS3 $adapter */ $adapter = $this->filesystem->getAdapter(); diff --git a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php index fdf36aea5..bd22c88eb 100644 --- a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php +++ b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php @@ -17,7 +17,7 @@ class AwsS3Test extends FunctionalTestCase /** @var S3Client */ private $client; - protected function setUp() + protected function setUp(): void { $key = getenv('AWS_KEY'); $secret = getenv('AWS_SECRET'); @@ -54,7 +54,7 @@ protected function setUp() $this->createFilesystem(['create' => true]); } - protected function tearDown() + protected function tearDown(): void { if ($this->client === null || !$this->client->doesBucketExist($this->bucket)) { return; @@ -84,7 +84,7 @@ private function createFilesystem(array $adapterOptions = []) * @test * @expectedException \RuntimeException */ - public function shouldThrowExceptionIfBucketMissingAndNotCreating() + public function shouldThrowExceptionIfBucketMissingAndNotCreating(): void { $this->createFilesystem(); $this->filesystem->read('foo'); @@ -93,7 +93,7 @@ public function shouldThrowExceptionIfBucketMissingAndNotCreating() /** * @test */ - public function shouldWriteObjects() + public function shouldWriteObjects(): void { $this->assertEquals(7, $this->filesystem->write('foo', 'testing')); } @@ -101,7 +101,7 @@ public function shouldWriteObjects() /** * @test */ - public function shouldCheckForObjectExistence() + public function shouldCheckForObjectExistence(): void { $this->filesystem->write('foo', ''); $this->assertTrue($this->filesystem->has('foo')); @@ -110,7 +110,7 @@ public function shouldCheckForObjectExistence() /** * @test */ - public function shouldCheckForObjectExistenceWithDirectory() + public function shouldCheckForObjectExistenceWithDirectory(): void { $this->createFilesystem(['directory' => 'bar', 'create' => true]); $this->filesystem->write('foo', ''); @@ -121,7 +121,7 @@ public function shouldCheckForObjectExistenceWithDirectory() /** * @test */ - public function shouldListKeysWithoutDirectory() + public function shouldListKeysWithoutDirectory(): void { $this->assertEquals([], $this->filesystem->listKeys()); $this->filesystem->write('test.txt', 'some content'); @@ -131,7 +131,7 @@ public function shouldListKeysWithoutDirectory() /** * @test */ - public function shouldListKeysWithDirectory() + public function shouldListKeysWithDirectory(): void { $this->createFilesystem(['create' => true, 'directory' => 'root/']); $this->filesystem->write('test.txt', 'some content'); @@ -142,7 +142,7 @@ public function shouldListKeysWithDirectory() /** * @test */ - public function shouldGetKeysWithoutDirectory() + public function shouldGetKeysWithoutDirectory(): void { $this->filesystem->write('test.txt', 'some content'); $this->assertEquals(['test.txt'], $this->filesystem->keys()); @@ -151,7 +151,7 @@ public function shouldGetKeysWithoutDirectory() /** * @test */ - public function shouldGetKeysWithDirectory() + public function shouldGetKeysWithDirectory(): void { $this->createFilesystem(['create' => true, 'directory' => 'root/']); $this->filesystem->write('test.txt', 'some content'); @@ -161,7 +161,7 @@ public function shouldGetKeysWithDirectory() /** * @test */ - public function shouldUploadWithGivenContentType() + public function shouldUploadWithGivenContentType(): void { /** @var AwsS3 $adapter */ $adapter = $this->filesystem->getAdapter(); diff --git a/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php b/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php index 06d55755a..816df019f 100644 --- a/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php +++ b/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php @@ -18,7 +18,7 @@ class AzureBlobStorageTest extends FunctionalTestCase /** @var AzureBlobStorage */ private $adapter; - protected function setUp() + protected function setUp(): void { $account = getenv('AZURE_ACCOUNT'); $key = getenv('AZURE_KEY'); @@ -39,7 +39,7 @@ protected function setUp() * @test * @group functional */ - public function shouldGetContentType() + public function shouldGetContentType(): void { $path = '/somefile'; $content = 'Some content'; @@ -48,7 +48,7 @@ public function shouldGetContentType() $this->assertEquals('text/plain', $this->filesystem->mimeType($path)); } - protected function tearDown() + protected function tearDown(): void { if ($this->adapter === null) { return; diff --git a/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php index 87cd50f57..f01989aae 100644 --- a/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php +++ b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php @@ -17,7 +17,7 @@ class AzureMultiContainerBlobStorageTest extends FunctionalTestCase private $containers = []; - protected function setUp() + protected function setUp(): void { $this->markTestSkipped(__CLASS__ . ' is flaky.'); @@ -37,7 +37,7 @@ protected function setUp() * @test * @group functional */ - public function shouldWriteAndRead() + public function shouldWriteAndRead(): void { $path1 = $this->createUniqueContainerName('container') . '/foo'; $path2 = $this->createUniqueContainerName('test') . '/subdir/foo'; @@ -53,7 +53,7 @@ public function shouldWriteAndRead() * @test * @group functional */ - public function shouldUpdateFileContent() + public function shouldUpdateFileContent(): void { $path = $this->createUniqueContainerName('container') . '/foo'; @@ -67,7 +67,7 @@ public function shouldUpdateFileContent() * @test * @group functional */ - public function shouldCheckIfFileExists() + public function shouldCheckIfFileExists(): void { $path1 = $this->createUniqueContainerName('container') . '/foo'; $path2 = $this->createUniqueContainerName('test') . '/somefile'; @@ -86,7 +86,7 @@ public function shouldCheckIfFileExists() * @test * @group functional */ - public function shouldGetMtime() + public function shouldGetMtime(): void { $path = $this->createUniqueContainerName('container') . '/foo'; @@ -99,7 +99,7 @@ public function shouldGetMtime() * @test * @group functional */ - public function shouldGetSize() + public function shouldGetSize(): void { $path = $this->createUniqueContainerName('container') . '/foo'; @@ -112,7 +112,7 @@ public function shouldGetSize() * @test * @group functional */ - public function shouldGetMd5Hash() + public function shouldGetMd5Hash(): void { $path = $this->createUniqueContainerName('container') . '/foo'; @@ -126,7 +126,7 @@ public function shouldGetMd5Hash() * @test * @group functional */ - public function shouldGetContentType() + public function shouldGetContentType(): void { $path = $this->createUniqueContainerName('container') . '/foo'; @@ -142,7 +142,7 @@ public function shouldGetContentType() * @expectedException \RuntimeException * @expectedMessage Could not get mtime for the "foo" key */ - public function shouldFailWhenTryMtimeForKeyWhichDoesNotExist() + public function shouldFailWhenTryMtimeForKeyWhichDoesNotExist(): void { $this->assertFalse($this->filesystem->mtime('container5/foo')); } @@ -151,7 +151,7 @@ public function shouldFailWhenTryMtimeForKeyWhichDoesNotExist() * @test * @group functional */ - public function shouldRenameFile() + public function shouldRenameFile(): void { $somedir = $this->createUniqueContainerName('somedir'); $path1 = $this->createUniqueContainerName('container') . '/foo'; @@ -175,7 +175,7 @@ public function shouldRenameFile() * @test * @group functional */ - public function shouldDeleteFile() + public function shouldDeleteFile(): void { $path = $this->createUniqueContainerName('container') . '/foo'; @@ -192,7 +192,7 @@ public function shouldDeleteFile() * @test * @group functional */ - public function shouldFetchKeys() + public function shouldFetchKeys(): void { $path1 = $this->createUniqueContainerName('container-1') . '/foo'; $path2 = $this->createUniqueContainerName('container-2') . '/bar'; @@ -212,7 +212,7 @@ public function shouldFetchKeys() * @test * @group functional */ - public function shouldWorkWithHiddenFiles() + public function shouldWorkWithHiddenFiles(): void { $path = $this->createUniqueContainerName('container') . '/.foo'; @@ -227,7 +227,7 @@ public function shouldWorkWithHiddenFiles() * @test * @group functional */ - public function shouldKeepFileObjectInRegister() + public function shouldKeepFileObjectInRegister(): void { $path = $this->createUniqueContainerName('container') . '/somefile'; @@ -241,7 +241,7 @@ public function shouldKeepFileObjectInRegister() * @test * @group functional */ - public function shouldWriteToSameFile() + public function shouldWriteToSameFile(): void { $path = $this->createUniqueContainerName('container') . '/somefile'; @@ -261,7 +261,7 @@ private function createUniqueContainerName($prefix) return $container; } - protected function tearDown() + protected function tearDown(): void { foreach ($this->containers as $container) { $this->adapter->deleteContainer($container); diff --git a/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php b/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php index 462cfad46..6e323213f 100644 --- a/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php +++ b/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php @@ -11,7 +11,7 @@ class DoctrineDbalTest extends FunctionalTestCase /** @var \Doctrine\DBAL\Connection */ private $connection; - public static function setUpBeforeClass() + public static function setUpBeforeClass(): void { if (!class_exists(DriverManager::class)) { self::markTestSkipped('Package doctrine/dbal is not installed'); @@ -19,7 +19,7 @@ public static function setUpBeforeClass() parent::setUpBeforeClass(); } - protected function setUp() + protected function setUp(): void { $this->connection = DriverManager::getConnection([ 'driver' => 'pdo_sqlite', @@ -40,7 +40,7 @@ protected function setUp() $this->filesystem = new Filesystem(new DoctrineDbal($this->connection, 'gaufrette')); } - protected function tearDown() + protected function tearDown(): void { $schemaManager = $this->connection->getSchemaManager(); @@ -52,7 +52,7 @@ protected function tearDown() /** * @test */ - public function shouldListKeys() + public function shouldListKeys(): void { $this->filesystem->write('foo/foobar/bar.txt', 'data'); $this->filesystem->write('foo/bar/buzz.txt', 'data'); diff --git a/tests/Gaufrette/Functional/Adapter/FtpTest.php b/tests/Gaufrette/Functional/Adapter/FtpTest.php index 9aec24ff6..1fd3c3b1f 100644 --- a/tests/Gaufrette/Functional/Adapter/FtpTest.php +++ b/tests/Gaufrette/Functional/Adapter/FtpTest.php @@ -7,7 +7,7 @@ class FtpTest extends FunctionalTestCase { - protected function setUp() + protected function setUp(): void { $host = getenv('FTP_HOST'); $port = getenv('FTP_PORT'); @@ -23,7 +23,7 @@ protected function setUp() $this->filesystem = new Filesystem($adapter); } - protected function tearDown() + protected function tearDown(): void { if (null === $this->filesystem) { return; diff --git a/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php b/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php index 3728607aa..208ecd217 100644 --- a/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php +++ b/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php @@ -2,6 +2,7 @@ namespace Gaufrette\Functional\Adapter; +use Gaufrette\Exception\FileNotFound; use Gaufrette\Filesystem; use PHPUnit\Framework\TestCase; @@ -25,7 +26,7 @@ public function getAdapterName() return $matches[1]; } - protected function setUp() + protected function setUp(): void { $basename = $this->getAdapterName(); $filename = sprintf( @@ -48,7 +49,7 @@ protected function setUp() $this->filesystem = new Filesystem($adapter); } - protected function tearDown() + protected function tearDown(): void { if (null === $this->filesystem) { return; @@ -61,7 +62,7 @@ protected function tearDown() * @test * @group functional */ - public function shouldWriteAndRead() + public function shouldWriteAndRead(): void { $this->assertEquals(12, $this->filesystem->write('foo', 'Some content')); $this->assertEquals(13, $this->filesystem->write('test/subdir/foo', 'Some content1', true)); @@ -74,7 +75,7 @@ public function shouldWriteAndRead() * @test * @group functional */ - public function shouldUpdateFileContent() + public function shouldUpdateFileContent(): void { $this->filesystem->write('foo', 'Some content'); $this->filesystem->write('foo', 'Some content updated', true); @@ -86,7 +87,7 @@ public function shouldUpdateFileContent() * @test * @group functional */ - public function shouldCheckIfFileExists() + public function shouldCheckIfFileExists(): void { $this->assertFalse($this->filesystem->has('foo')); @@ -101,7 +102,7 @@ public function shouldCheckIfFileExists() * @test * @group functional */ - public function shouldGetMtime() + public function shouldGetMtime(): void { $this->filesystem->write('foo', 'Some content'); @@ -111,19 +112,19 @@ public function shouldGetMtime() /** * @test * @group functional - * @expectedException \RuntimeException - * @expectedMessage Could not get mtime for the "foo" key */ - public function shouldFailWhenTryMtimeForKeyWhichDoesNotExist() + public function shouldFailWhenTryMtimeForKeyWhichDoesNotExist(): void { - $this->assertFalse($this->filesystem->mtime('foo')); + $this->expectException(FileNotFound::class); + $this->expectExceptionMessage('The file "foo" was not found.'); + $this->filesystem->mtime('foo'); } /** * @test * @group functional */ - public function shouldRenameFile() + public function shouldRenameFile(): void { $this->filesystem->write('foo', 'Some content'); $this->filesystem->rename('foo', 'boo'); @@ -143,7 +144,7 @@ public function shouldRenameFile() * @test * @group functional */ - public function shouldDeleteFile() + public function shouldDeleteFile(): void { $this->filesystem->write('foo', 'Some content'); @@ -158,7 +159,7 @@ public function shouldDeleteFile() * @test * @group functional */ - public function shouldFetchKeys() + public function shouldFetchKeys(): void { $this->assertEquals([], $this->filesystem->keys()); @@ -178,7 +179,7 @@ public function shouldFetchKeys() * @test * @group functional */ - public function shouldWorkWithHiddenFiles() + public function shouldWorkWithHiddenFiles(): void { $this->filesystem->write('.foo', 'hidden'); $this->assertTrue($this->filesystem->has('.foo')); @@ -191,7 +192,7 @@ public function shouldWorkWithHiddenFiles() * @test * @group functional */ - public function shouldKeepFileObjectInRegister() + public function shouldKeepFileObjectInRegister(): void { $FileObjectA = $this->filesystem->createFile('somefile'); $FileObjectB = $this->filesystem->createFile('somefile'); @@ -203,7 +204,7 @@ public function shouldKeepFileObjectInRegister() * @test * @group functional */ - public function shouldWriteToSameFile() + public function shouldWriteToSameFile(): void { $FileObjectA = $this->filesystem->createFile('somefile'); $FileObjectA->setContent('ABC'); diff --git a/tests/Gaufrette/Functional/Adapter/GridFSTest.php b/tests/Gaufrette/Functional/Adapter/GridFSTest.php index 82468356c..d82eca951 100644 --- a/tests/Gaufrette/Functional/Adapter/GridFSTest.php +++ b/tests/Gaufrette/Functional/Adapter/GridFSTest.php @@ -8,7 +8,7 @@ class GridFSTest extends FunctionalTestCase { - protected function setUp() + protected function setUp(): void { $uri = getenv('MONGO_URI'); $dbname = getenv('MONGO_DBNAME'); @@ -28,7 +28,7 @@ protected function setUp() /** * @test */ - public function shouldListKeys() + public function shouldListKeys(): void { $this->filesystem->write('foo/foobar/bar.txt', 'data'); $this->filesystem->write('foo/bar/buzz.txt', 'data'); @@ -79,7 +79,7 @@ public function shouldListKeys() * @test * Tests metadata written to GridFS can be retrieved after writing */ - public function shouldRetrieveMetadataAfterWrite() + public function shouldRetrieveMetadataAfterWrite(): void { //Create local copy of fileadapter $fileadpt = clone $this->filesystem->getAdapter(); @@ -94,7 +94,7 @@ public function shouldRetrieveMetadataAfterWrite() * @test * Test to see if filesize works */ - public function shouldGetSize() + public function shouldGetSize(): void { $this->filesystem->write('sizetest.txt', 'data'); $this->assertEquals(4, $this->filesystem->size('sizetest.txt')); @@ -104,7 +104,7 @@ public function shouldGetSize() * @test * Should retrieve empty metadata w/o errors */ - public function shouldRetrieveEmptyMetadata() + public function shouldRetrieveEmptyMetadata(): void { $this->filesystem->write('no-metadata.txt', 'content'); @@ -115,7 +115,7 @@ public function shouldRetrieveEmptyMetadata() * @test * @group functional */ - public function shouldGetMtime() + public function shouldGetMtime(): void { if (strtolower(substr(PHP_OS, 0, 3)) === 'win') { $this->markTestSkipped('Not working on Windows.'); diff --git a/tests/Gaufrette/Functional/Adapter/LocalTest.php b/tests/Gaufrette/Functional/Adapter/LocalTest.php index 991faa974..06c106f96 100644 --- a/tests/Gaufrette/Functional/Adapter/LocalTest.php +++ b/tests/Gaufrette/Functional/Adapter/LocalTest.php @@ -9,7 +9,7 @@ class LocalTest extends FunctionalTestCase { private $directory; - protected function setUp() + protected function setUp(): void { $this->directory = sprintf('%s/filesystem', str_replace('\\', '/', __DIR__)); @@ -20,7 +20,7 @@ protected function setUp() $this->filesystem = new Filesystem(new Local($this->directory)); } - protected function tearDown() + protected function tearDown(): void { $adapter = $this->filesystem->getAdapter(); @@ -37,7 +37,7 @@ protected function tearDown() * @test * @group functional */ - public function shouldWorkWithSyslink() + public function shouldWorkWithSyslink(): void { if (strtolower(substr(PHP_OS, 0, 3)) === 'win') { $this->markTestSkipped('Symlinks are not supported on Windows.'); @@ -71,7 +71,7 @@ public function shouldWorkWithSyslink() * @covers Gaufrette\Adapter\Local * @group functional */ - public function shouldListingOnlyGivenDirectory() + public function shouldListingOnlyGivenDirectory(): void { $this->filesystem->write('aaa.txt', 'some content'); $this->filesystem->write('localDir/test.txt', 'some content'); @@ -95,7 +95,7 @@ public function shouldListingOnlyGivenDirectory() * @covers Gaufrette\Adapter\Local * @group functional */ - public function shouldListingAllKeys() + public function shouldListingAllKeys(): void { $this->filesystem->write('aaa.txt', 'some content'); $this->filesystem->write('localDir/dir1/dir2/dir3/test.txt', 'some content'); @@ -112,7 +112,7 @@ public function shouldListingAllKeys() * @test * @group functional */ - public function shouldBeAbleToClearCache() + public function shouldBeAbleToClearCache(): void { $this->filesystem->get('test.txt', true); $this->filesystem->write('test.txt', '123', true); @@ -139,7 +139,7 @@ public function shouldBeAbleToClearCache() * @test * @group functional */ - public function shouldDeleteDirectory() + public function shouldDeleteDirectory(): void { $path = $this->directory . DIRECTORY_SEPARATOR . 'delete-me.d'; mkdir($path); @@ -157,7 +157,7 @@ public function shouldDeleteDirectory() * @expectedException \InvalidArgumentException * @expectedExceptionMessage Impossible to delete the root directory of this Local adapter */ - public function shouldNotDeleteTheAdapterRootDirectory() + public function shouldNotDeleteTheAdapterRootDirectory(): void { $this->filesystem->getAdapter()->delete('/'); } diff --git a/tests/Gaufrette/Functional/Adapter/OpenCloudTest.php b/tests/Gaufrette/Functional/Adapter/OpenCloudTest.php index 54fd2ddf3..fe7096273 100644 --- a/tests/Gaufrette/Functional/Adapter/OpenCloudTest.php +++ b/tests/Gaufrette/Functional/Adapter/OpenCloudTest.php @@ -14,7 +14,7 @@ class OpenCloudTest extends FunctionalTestCase /** @var string */ private $container; - protected function setUp() + protected function setUp(): void { $username = getenv('RACKSPACE_USER') ?: ''; $apiKey = getenv('RACKSPACE_APIKEY') ?: ''; @@ -49,7 +49,7 @@ protected function setUp() $this->filesystem = new Filesystem($adapter); } - protected function tearDown() + protected function tearDown(): void { if ($this->filesystem === null) { return; diff --git a/tests/Gaufrette/Functional/Adapter/PhpseclibSftpTest.php b/tests/Gaufrette/Functional/Adapter/PhpseclibSftpTest.php index af48dda58..9a50ab685 100644 --- a/tests/Gaufrette/Functional/Adapter/PhpseclibSftpTest.php +++ b/tests/Gaufrette/Functional/Adapter/PhpseclibSftpTest.php @@ -14,7 +14,7 @@ class PhpseclibSftpTest extends FunctionalTestCase /** @var string */ private $baseDir; - protected function setUp() + protected function setUp(): void { $host = getenv('SFTP_HOST'); $port = getenv('SFTP_PORT') ?: 22; @@ -34,7 +34,7 @@ protected function setUp() $this->filesystem = new Filesystem(new PhpseclibSftp($this->sftp, $this->baseDir, true)); } - protected function tearDown() + protected function tearDown(): void { if (!isset($this->sftp)) { return; diff --git a/tests/Gaufrette/Functional/Adapter/SafeLocalTest.php b/tests/Gaufrette/Functional/Adapter/SafeLocalTest.php index 5c1f198c2..00447cc87 100644 --- a/tests/Gaufrette/Functional/Adapter/SafeLocalTest.php +++ b/tests/Gaufrette/Functional/Adapter/SafeLocalTest.php @@ -7,7 +7,7 @@ class SafeLocalTest extends FunctionalTestCase { - protected function setUp() + protected function setUp(): void { if (!file_exists($this->getDirectory())) { mkdir($this->getDirectory()); @@ -16,7 +16,7 @@ protected function setUp() $this->filesystem = new Filesystem(new SafeLocal($this->getDirectory())); } - protected function tearDown() + protected function tearDown(): void { foreach ($this->filesystem->keys() as $key) { $this->filesystem->delete($key); @@ -27,7 +27,7 @@ protected function tearDown() rmdir($this->getDirectory()); } - private function getDirectory() + private function getDirectory(): string { return sprintf('%s/filesystem', __DIR__); } diff --git a/tests/Gaufrette/Functional/Adapter/ZipTest.php b/tests/Gaufrette/Functional/Adapter/ZipTest.php index dc131f52b..f8116a0bc 100644 --- a/tests/Gaufrette/Functional/Adapter/ZipTest.php +++ b/tests/Gaufrette/Functional/Adapter/ZipTest.php @@ -7,10 +7,10 @@ class ZipTest extends FunctionalTestCase { - protected function setUp() + protected function setUp(): void { if (!extension_loaded('zip')) { - return $this->markTestSkipped('The zip extension is not available.'); + $this->markTestSkipped('The zip extension is not available.'); } elseif (strtolower(substr(PHP_OS, 0, 3)) === 'win') { $this->markTestSkipped('Zip adapter is not supported on Windows.'); } @@ -20,7 +20,7 @@ protected function setUp() $this->filesystem = new Filesystem(new Zip(__DIR__ . '/test.zip')); } - protected function tearDown() + protected function tearDown(): void { parent::tearDown(); @@ -32,7 +32,7 @@ protected function tearDown() * @expectedException \RuntimeException * @group functional */ - public function shouldNotAcceptInvalidZipArchive() + public function shouldNotAcceptInvalidZipArchive(): void { new Zip(__FILE__); } @@ -41,7 +41,7 @@ public function shouldNotAcceptInvalidZipArchive() * @test * @group functional */ - public function shouldCreateNewZipArchive() + public function shouldCreateNewZipArchive(): Zip { $tmp = tempnam(sys_get_temp_dir(), uniqid()); $za = new Zip($tmp); diff --git a/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php b/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php index 7805a22c4..4a041436d 100644 --- a/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php +++ b/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php @@ -5,7 +5,7 @@ use Gaufrette\StreamWrapper; use PHPUnit\Framework\TestCase; -class FunctionalTestCase extends TestCase +abstract class FunctionalTestCase extends TestCase { protected $filesystem; @@ -46,11 +46,11 @@ public function shouldWriteAndReadFile() /** * @test - * @expectedException \LogicException - * @expectedExceptionMessage The stream does not allow read. */ public function shouldNotReadWhenOpenInWriteMode() { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The stream does not allow read.'); $this->filesystem->write('test.txt', 'test content'); $fileHandler = fopen('gaufrette://filestream/test.txt', 'w'); @@ -62,11 +62,11 @@ public function shouldNotReadWhenOpenInWriteMode() /** * @test - * @expectedException \LogicException - * @expectedExceptionMessage The stream does not allow write. */ public function shouldNotWriteWhenOpenInReadMode() { + $this->expectException(\LogicException::class); + $this->expectExceptionMessage('The stream does not allow write.'); $this->filesystem->write('test.txt', 'test content'); $fileHandler = fopen('gaufrette://filestream/test.txt', 'r'); diff --git a/tests/Gaufrette/Functional/FileStream/InMemoryBufferTest.php b/tests/Gaufrette/Functional/FileStream/InMemoryBufferTest.php index 3b143163b..50f3fdc1e 100644 --- a/tests/Gaufrette/Functional/FileStream/InMemoryBufferTest.php +++ b/tests/Gaufrette/Functional/FileStream/InMemoryBufferTest.php @@ -7,7 +7,7 @@ class InMemoryBufferTest extends FunctionalTestCase { - protected function setUp() + protected function setUp(): void { $this->filesystem = new Filesystem(new InMemoryAdapter([])); diff --git a/tests/Gaufrette/Functional/FileStream/LocalTest.php b/tests/Gaufrette/Functional/FileStream/LocalTest.php index 1f687bb3c..49e5a8e60 100644 --- a/tests/Gaufrette/Functional/FileStream/LocalTest.php +++ b/tests/Gaufrette/Functional/FileStream/LocalTest.php @@ -9,7 +9,7 @@ class LocalTest extends FunctionalTestCase { protected $directory; - protected function setUp() + protected function setUp(): void { $this->directory = __DIR__ . DIRECTORY_SEPARATOR . 'filesystem'; @mkdir($this->directory . DIRECTORY_SEPARATOR . 'subdir', 0777, true); @@ -22,7 +22,7 @@ protected function setUp() /** * @test */ - public function shouldChmodDirectory() + public function shouldChmodDirectory(): void { if (strtolower(substr(PHP_OS, 0, 3)) === 'win') { $this->markTestSkipped('Chmod and umask are not available on Windows.'); @@ -35,7 +35,7 @@ public function shouldChmodDirectory() $this->assertEquals('0770', substr(sprintf('%o', $perms), -4)); } - protected function tearDown() + protected function tearDown(): void { $adapter = $this->filesystem->getAdapter(); @@ -51,7 +51,7 @@ protected function tearDown() /** * @test */ - public function shouldSupportsDirectory() + public function shouldSupportsDirectory(): void { $this->assertFileExists('gaufrette://filestream/subdir'); $this->assertDirectoryExists('gaufrette://filestream/subdir'); From bab98cb06a30e310c5c62187b42c9445a203ecec Mon Sep 17 00:00:00 2001 From: Maxime Veber Date: Wed, 21 Sep 2022 18:49:05 +0200 Subject: [PATCH 399/426] :green_heart: Update PHP CS Fixer! (#659) --- .gitignore | 1 + .php_cs.dist => .php-cs-fixer.dist.php | 10 ++-- composer.json | 4 +- spec/Gaufrette/Adapter/AzureBlobStorage.php | 24 ++++++-- spec/Gaufrette/Adapter/DoctrineDbalSpec.php | 9 ++- spec/Gaufrette/Adapter/OpenCloudSpec.php | 12 ++-- spec/Gaufrette/Adapter/functions.php | 1 - spec/Gaufrette/FileSpec.php | 2 +- spec/Gaufrette/FilesystemSpec.php | 10 ++-- src/Gaufrette/Adapter/AsyncAwsS3.php | 2 +- src/Gaufrette/Adapter/DoctrineDbal.php | 6 +- src/Gaufrette/Adapter/Zip.php | 60 +++++++++---------- .../Functional/Adapter/DoctrineDbalTest.php | 30 ++++++++-- .../Functional/Adapter/FunctionalTestCase.php | 3 +- .../Functional/Adapter/GridFSTest.php | 30 ++++++++-- 15 files changed, 132 insertions(+), 72 deletions(-) rename .php_cs.dist => .php-cs-fixer.dist.php (80%) diff --git a/.gitignore b/.gitignore index 441023695..6d4f35a08 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ composer.lock doc/.couscous .env .php_cs.cache +.php-cs-fixer.cache phpunit.xml tests/adapters/* !tests/adapters/*.dist diff --git a/.php_cs.dist b/.php-cs-fixer.dist.php similarity index 80% rename from .php_cs.dist rename to .php-cs-fixer.dist.php index 10089ee4a..d68945fde 100644 --- a/.php_cs.dist +++ b/.php-cs-fixer.dist.php @@ -1,6 +1,6 @@ setFinder(PhpCsFixer\Finder::create() ->exclude('vendor') ->in(__DIR__) @@ -9,17 +9,17 @@ '@PSR1' => true, '@PSR2' => true, 'array_syntax' => [ 'syntax' => 'short' ], - 'binary_operator_spaces' => [ 'align_equals' => false, 'align_double_arrow' => false ], + 'binary_operator_spaces' => true, 'blank_line_before_statement' => true, 'cast_spaces' => true, 'combine_consecutive_unsets' => true, 'concat_space' => [ 'spacing' => 'one' ], 'linebreak_after_opening_tag' => true, - 'method_argument_space' => ['ensure_fully_multiline' => false], + 'method_argument_space' => ['on_multiline' => 'ensure_fully_multiline'], 'no_blank_lines_after_class_opening' => true, 'no_blank_lines_after_phpdoc' => true, 'no_break_comment' => false, - 'no_extra_consecutive_blank_lines' => true, + 'no_extra_blank_lines' => true, 'no_spaces_around_offset' => true, 'no_trailing_comma_in_singleline_array' => true, 'no_unused_imports' => true, @@ -34,7 +34,7 @@ 'phpdoc_trim' => true, 'single_quote' => true, 'ternary_to_null_coalescing' => true, - 'trailing_comma_in_multiline_array' => true, + 'trailing_comma_in_multiline' => ['elements' => ['arrays']], 'trim_array_spaces' => true, ]) ->registerCustomFixers(new PedroTroller\CS\Fixer\Fixers()) diff --git a/composer.json b/composer.json index ef50abb98..f902eb484 100644 --- a/composer.json +++ b/composer.json @@ -24,7 +24,9 @@ "require-dev": { "phpspec/phpspec": "^7.0", "phpunit/phpunit": "~8.0", - "mikey179/vfsstream": "v1.x-dev as 1.7.0" + "mikey179/vfsstream": "v1.x-dev as 1.7.0", + "friendsofphp/php-cs-fixer": "^3.9", + "pedrotroller/php-cs-custom-fixer": "^2.28" }, "suggest": { "knplabs/knp-gaufrette-bundle": "to use with Symfony", diff --git a/spec/Gaufrette/Adapter/AzureBlobStorage.php b/spec/Gaufrette/Adapter/AzureBlobStorage.php index 3d1436fef..c6cee8572 100644 --- a/spec/Gaufrette/Adapter/AzureBlobStorage.php +++ b/spec/Gaufrette/Adapter/AzureBlobStorage.php @@ -155,8 +155,12 @@ function it_should_not_mask_exception_when_rename($blobProxyFactory, $blobProxy) function it_should_write_file($blobProxyFactory, $blobProxy) { $blobProxy - ->createBlockBlob('containerName', 'filename', 'some content', - \Mockery::type('\WindowsAzure\Blob\Models\CreateBlobOptions')) + ->createBlockBlob( + 'containerName', + 'filename', + 'some content', + \Mockery::type('\WindowsAzure\Blob\Models\CreateBlobOptions') + ) ->shouldBeCalled(); $blobProxyFactory @@ -174,8 +178,12 @@ function it_should_write_file($blobProxyFactory, $blobProxy) function it_should_return_false_when_cannot_write($blobProxyFactory, $blobProxy) { $blobProxy - ->createBlockBlob('containerName', 'filename', 'some content', - \Mockery::type('\WindowsAzure\Blob\Models\CreateBlobOptions')) + ->createBlockBlob( + 'containerName', + 'filename', + 'some content', + \Mockery::type('\WindowsAzure\Blob\Models\CreateBlobOptions') + ) ->willThrow(new ServiceException(500)); $blobProxyFactory @@ -193,8 +201,12 @@ function it_should_return_false_when_cannot_write($blobProxyFactory, $blobProxy) function it_should_not_mask_exception_when_write($blobProxyFactory, $blobProxy) { $blobProxy - ->createBlockBlob('containerName', 'filename', 'some content', - \Mockery::type('\WindowsAzure\Blob\Models\CreateBlobOptions')) + ->createBlockBlob( + 'containerName', + 'filename', + 'some content', + \Mockery::type('\WindowsAzure\Blob\Models\CreateBlobOptions') + ) ->willThrow(new \RuntimeException('write')); $blobProxyFactory diff --git a/spec/Gaufrette/Adapter/DoctrineDbalSpec.php b/spec/Gaufrette/Adapter/DoctrineDbalSpec.php index 69f4c35a9..091ee157f 100644 --- a/spec/Gaufrette/Adapter/DoctrineDbalSpec.php +++ b/spec/Gaufrette/Adapter/DoctrineDbalSpec.php @@ -78,7 +78,8 @@ function it_writes_to_new_file(Connection $connection) '"mtime"' => strtotime('2012-10-10 23:10:10'), '"checksum"' => '9893532233caff98cd083a116b013c0b', '"key"' => 'filename', - ]) + ] + ) ->shouldBeCalled(); $this->write('filename', 'some content')->shouldReturn(12); @@ -107,7 +108,8 @@ function it_write_file(Connection $connection) ], [ '"key"' => 'filename', - ]) + ] + ) ->shouldBeCalled(); $this->write('filename', 'some content')->shouldReturn(12); @@ -182,7 +184,8 @@ function it_renames_file(Connection $connection) ], [ '"key"' => 'filename', - ]) + ] + ) ->shouldBeCalled() ->willReturn(1); diff --git a/spec/Gaufrette/Adapter/OpenCloudSpec.php b/spec/Gaufrette/Adapter/OpenCloudSpec.php index 7d1738f2f..55e8a04de 100644 --- a/spec/Gaufrette/Adapter/OpenCloudSpec.php +++ b/spec/Gaufrette/Adapter/OpenCloudSpec.php @@ -180,13 +180,13 @@ function it_returns_files_as_sorted_array(Container $container, Collection $obje $objects = [$object1, $object2, $object3]; $objectList->next()->will( - function () use ($objects, &$index) { - if ($index < count($objects)) { - $index++; + function () use ($objects, &$index) { + if ($index < count($objects)) { + $index++; - return $objects[$index - 1]; - } - } + return $objects[$index - 1]; + } + } ) ->shouldBeCalledTimes(count($objects) + 1); $container->objectList()->willReturn($objectList); diff --git a/spec/Gaufrette/Adapter/functions.php b/spec/Gaufrette/Adapter/functions.php index 276559bfd..177bf120f 100644 --- a/spec/Gaufrette/Adapter/functions.php +++ b/spec/Gaufrette/Adapter/functions.php @@ -66,4 +66,3 @@ function apc_exists($path) return true; } - diff --git a/spec/Gaufrette/FileSpec.php b/spec/Gaufrette/FileSpec.php index f45608784..80576bbda 100644 --- a/spec/Gaufrette/FileSpec.php +++ b/spec/Gaufrette/FileSpec.php @@ -6,7 +6,7 @@ use PhpSpec\ObjectBehavior; interface MetadataAdapter extends \Gaufrette\Adapter, - \Gaufrette\Adapter\MetadataSupporter + \Gaufrette\Adapter\MetadataSupporter { } diff --git a/spec/Gaufrette/FilesystemSpec.php b/spec/Gaufrette/FilesystemSpec.php index 1c90f6c68..ad8d66405 100644 --- a/spec/Gaufrette/FilesystemSpec.php +++ b/spec/Gaufrette/FilesystemSpec.php @@ -8,11 +8,11 @@ use Prophecy\Argument; interface ExtendedAdapter extends \Gaufrette\Adapter, - \Gaufrette\Adapter\FileFactory, - \Gaufrette\Adapter\StreamFactory, - \Gaufrette\Adapter\ChecksumCalculator, - \Gaufrette\Adapter\MetadataSupporter, - \Gaufrette\Adapter\MimeTypeProvider + \Gaufrette\Adapter\FileFactory, + \Gaufrette\Adapter\StreamFactory, + \Gaufrette\Adapter\ChecksumCalculator, + \Gaufrette\Adapter\MetadataSupporter, + \Gaufrette\Adapter\MimeTypeProvider { } diff --git a/src/Gaufrette/Adapter/AsyncAwsS3.php b/src/Gaufrette/Adapter/AsyncAwsS3.php index ea088ac43..0ee94bb6d 100644 --- a/src/Gaufrette/Adapter/AsyncAwsS3.php +++ b/src/Gaufrette/Adapter/AsyncAwsS3.php @@ -280,7 +280,7 @@ protected function ensureBucketExists() $this->service->createBucket([ 'Bucket' => $this->bucket, 'CreateBucketConfiguration' => [ - 'LocationConstraint' => $this->service->getConfiguration()->get(Configuration::OPTION_REGION) + 'LocationConstraint' => $this->service->getConfiguration()->get(Configuration::OPTION_REGION), ], ]); $this->bucketExists = true; diff --git a/src/Gaufrette/Adapter/DoctrineDbal.php b/src/Gaufrette/Adapter/DoctrineDbal.php index d8cae0f91..59a4bd416 100644 --- a/src/Gaufrette/Adapter/DoctrineDbal.php +++ b/src/Gaufrette/Adapter/DoctrineDbal.php @@ -185,10 +185,12 @@ public function listKeys($prefix = '') return [ 'dirs' => [], - 'keys' => array_map(function ($value) { + 'keys' => array_map( + function ($value) { return $value['_key']; }, - $keys), + $keys + ), ]; } diff --git a/src/Gaufrette/Adapter/Zip.php b/src/Gaufrette/Adapter/Zip.php index ec7af7ecd..4f261f261 100644 --- a/src/Gaufrette/Adapter/Zip.php +++ b/src/Gaufrette/Adapter/Zip.php @@ -163,46 +163,46 @@ protected function reinitZipArchive() if (true !== ($resultCode = $this->zipArchive->open($this->zipFile, ZipArchive::CREATE))) { switch ($resultCode) { - case ZipArchive::ER_EXISTS: - $errMsg = 'File already exists.'; + case ZipArchive::ER_EXISTS: + $errMsg = 'File already exists.'; - break; - case ZipArchive::ER_INCONS: - $errMsg = 'Zip archive inconsistent.'; + break; + case ZipArchive::ER_INCONS: + $errMsg = 'Zip archive inconsistent.'; - break; - case ZipArchive::ER_INVAL: - $errMsg = 'Invalid argument.'; + break; + case ZipArchive::ER_INVAL: + $errMsg = 'Invalid argument.'; - break; - case ZipArchive::ER_MEMORY: - $errMsg = 'Malloc failure.'; + break; + case ZipArchive::ER_MEMORY: + $errMsg = 'Malloc failure.'; - break; - case ZipArchive::ER_NOENT: - $errMsg = 'Invalid argument.'; + break; + case ZipArchive::ER_NOENT: + $errMsg = 'Invalid argument.'; - break; - case ZipArchive::ER_NOZIP: - $errMsg = 'Not a zip archive.'; + break; + case ZipArchive::ER_NOZIP: + $errMsg = 'Not a zip archive.'; - break; - case ZipArchive::ER_OPEN: - $errMsg = 'Can\'t open file.'; + break; + case ZipArchive::ER_OPEN: + $errMsg = 'Can\'t open file.'; - break; - case ZipArchive::ER_READ: - $errMsg = 'Read error.'; + break; + case ZipArchive::ER_READ: + $errMsg = 'Read error.'; - break; - case ZipArchive::ER_SEEK: - $errMsg = 'Seek error.'; + break; + case ZipArchive::ER_SEEK: + $errMsg = 'Seek error.'; - break; - default: - $errMsg = 'Unknown error.'; + break; + default: + $errMsg = 'Unknown error.'; - break; + break; } throw new \RuntimeException(sprintf('%s', $errMsg)); diff --git a/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php b/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php index 6e323213f..0cb2b270e 100644 --- a/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php +++ b/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php @@ -72,30 +72,50 @@ public function shouldListKeys(): void $this->assertEquals( $this->filesystem->keys(), $keys['keys'], - '', 0, 10, true); + '', + 0, + 10, + true + ); $keys = $this->filesystem->listKeys('foo/foob'); $this->assertEquals( ['foo/foobar/bar.txt'], $keys['keys'], - '', 0, 10, true); + '', + 0, + 10, + true + ); $keys = $this->filesystem->listKeys('foo/'); $this->assertEquals( ['foo/foobar/bar.txt', 'foo/bar/buzz.txt'], $keys['keys'], - '', 0, 10, true); + '', + 0, + 10, + true + ); $keys = $this->filesystem->listKeys('foo'); $this->assertEquals( ['foo/foobar/bar.txt', 'foo/bar/buzz.txt', 'foobarbuz.txt', 'foo'], $keys['keys'], - '', 0, 10, true); + '', + 0, + 10, + true + ); $keys = $this->filesystem->listKeys('fooz'); $this->assertEquals( [], $keys['keys'], - '', 0, 10, true); + '', + 0, + 10, + true + ); } } diff --git a/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php b/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php index 208ecd217..a78aa0edf 100644 --- a/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php +++ b/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php @@ -36,7 +36,8 @@ protected function setUp(): void ); if (!file_exists($filename)) { - $this->markTestSkipped(<<markTestSkipped( + <<assertEquals( $this->filesystem->keys(), $keys['keys'], - '', 0, 10, true); + '', + 0, + 10, + true + ); $keys = $this->filesystem->listKeys('foo/foob'); $this->assertEquals( ['foo/foobar/bar.txt'], $keys['keys'], - '', 0, 10, true); + '', + 0, + 10, + true + ); $keys = $this->filesystem->listKeys('foo/'); $this->assertEquals( ['foo/foobar/bar.txt', 'foo/bar/buzz.txt'], $keys['keys'], - '', 0, 10, true); + '', + 0, + 10, + true + ); $keys = $this->filesystem->listKeys('foo'); $this->assertEquals( ['foo/foobar/bar.txt', 'foo/bar/buzz.txt', 'foobarbuz.txt', 'foo'], $keys['keys'], - '', 0, 10, true); + '', + 0, + 10, + true + ); $keys = $this->filesystem->listKeys('fooz'); $this->assertEquals( [], $keys['keys'], - '', 0, 10, true); + '', + 0, + 10, + true + ); } /** From 79b030a10bffd09c692246a4f7326136d068219a Mon Sep 17 00:00:00 2001 From: Maxime Veber Date: Sat, 24 Sep 2022 22:00:25 +0200 Subject: [PATCH 400/426] :bug: Fix infinite loop in ftp adapter (php 8.1) (#663) --- src/Gaufrette/Adapter/Ftp.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index d578833e5..6cc229ea2 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -490,6 +490,10 @@ private function computePath($key) */ private function isConnected() { + if (class_exists('\FTP\Connection')) { + return $this->connection instanceof \FTP\Connection; + } + return is_resource($this->connection); } From 90f8e1e726cf17c898d038a0b1358de62c601a0e Mon Sep 17 00:00:00 2001 From: Maxime Veber Date: Sat, 24 Sep 2022 22:01:11 +0200 Subject: [PATCH 401/426] :white_check_mark: Fix AWS S3 (no async) test (#660) --- tests/Gaufrette/Functional/Adapter/AwsS3Test.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php index bd22c88eb..037ba72a3 100644 --- a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php +++ b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php @@ -82,10 +82,10 @@ private function createFilesystem(array $adapterOptions = []) /** * @test - * @expectedException \RuntimeException */ public function shouldThrowExceptionIfBucketMissingAndNotCreating(): void { + $this->expectException(\RuntimeException::class); $this->createFilesystem(); $this->filesystem->read('foo'); } From 0586f16f9635a1c788ed386f3a0b29818e04d79c Mon Sep 17 00:00:00 2001 From: Maxime Veber Date: Sat, 24 Sep 2022 22:01:41 +0200 Subject: [PATCH 402/426] :white_check_mark: Fix deprecations triggered by doctrine test (#662) --- .../Functional/Adapter/DoctrineDbalTest.php | 40 +++++-------------- 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php b/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php index 0cb2b270e..df53f63c4 100644 --- a/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php +++ b/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php @@ -69,53 +69,33 @@ public function shouldListKeys(): void //these values are canonicalized to avoid wrong order or keys issue $keys = $this->filesystem->listKeys('foo'); - $this->assertEquals( + $this->assertEqualsCanonicalizing( $this->filesystem->keys(), - $keys['keys'], - '', - 0, - 10, - true + $keys['keys'] ); $keys = $this->filesystem->listKeys('foo/foob'); - $this->assertEquals( + $this->assertEqualsCanonicalizing( ['foo/foobar/bar.txt'], - $keys['keys'], - '', - 0, - 10, - true + $keys['keys'] ); $keys = $this->filesystem->listKeys('foo/'); - $this->assertEquals( + $this->assertEqualsCanonicalizing( ['foo/foobar/bar.txt', 'foo/bar/buzz.txt'], - $keys['keys'], - '', - 0, - 10, - true + $keys['keys'] ); $keys = $this->filesystem->listKeys('foo'); - $this->assertEquals( + $this->assertEqualsCanonicalizing( ['foo/foobar/bar.txt', 'foo/bar/buzz.txt', 'foobarbuz.txt', 'foo'], - $keys['keys'], - '', - 0, - 10, - true + $keys['keys'] ); $keys = $this->filesystem->listKeys('fooz'); - $this->assertEquals( + $this->assertEqualsCanonicalizing( [], - $keys['keys'], - '', - 0, - 10, - true + $keys['keys'] ); } } From 1b392c072ee4eeb377245c637bb5a09c23a31f55 Mon Sep 17 00:00:00 2001 From: Maxime Veber Date: Mon, 26 Sep 2022 00:20:56 +0200 Subject: [PATCH 403/426] :heavy_minus_sign: remove sf4 event dispatcher dependency (#665) --- .github/workflows/static.yml | 3 +-- Makefile | 4 +--- src/Gaufrette/Adapter/DoctrineDbal.php | 4 ++-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index 817027fe6..ea8346d95 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -47,13 +47,12 @@ jobs: - name: Download dependencies run: | make require-all - composer require --dev --no-update pedrotroller/php-cs-custom-fixer:2.23.0 composer update --no-interaction --prefer-dist - name: PHP-CS-Fixer run: | ./vendor/bin/php-cs-fixer --version - ./vendor/bin/php-cs-fixer fix --diff --diff-format udiff --dry-run --show-progress=none + ./vendor/bin/php-cs-fixer fix --diff --dry-run --show-progress=none psalm: name: Psalm diff --git a/Makefile b/Makefile index 0f0e14d0d..cea4c1f56 100644 --- a/Makefile +++ b/Makefile @@ -30,7 +30,6 @@ docker.all-deps: docker.deps ## Install dependencies microsoft/azure-storage-blob:^1.0 \ phpseclib/phpseclib:^2.0 \ mongodb/mongodb:^1.1 \ - symfony/event-dispatcher:^4.4 \ async-aws/simple-s3:^0.1.1 .PHONY: tests @@ -74,8 +73,7 @@ require-all-legacy: # kept for compatibility with the old CI config, to be remov league/flysystem:^1.0 \ microsoft/azure-storage-blob:^1.0 \ phpseclib/phpseclib:^2.0 \ - mongodb/mongodb:^1.1 \ - symfony/event-dispatcher:^4.4 + mongodb/mongodb:^1.1 require-all: require-all-legacy ## Install all dependencies for adapters diff --git a/src/Gaufrette/Adapter/DoctrineDbal.php b/src/Gaufrette/Adapter/DoctrineDbal.php index 59a4bd416..337b1de60 100644 --- a/src/Gaufrette/Adapter/DoctrineDbal.php +++ b/src/Gaufrette/Adapter/DoctrineDbal.php @@ -187,8 +187,8 @@ public function listKeys($prefix = '') 'dirs' => [], 'keys' => array_map( function ($value) { - return $value['_key']; - }, + return $value['_key']; + }, $keys ), ]; From a805ed6e914beb3a87e64114b14dbaae9994f656 Mon Sep 17 00:00:00 2001 From: Maxime Veber Date: Fri, 30 Sep 2022 14:52:53 +0200 Subject: [PATCH 404/426] :white_check_mark: Fix Azure blob storage tests (#661) --- tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php b/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php index 816df019f..88fda4301 100644 --- a/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php +++ b/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php @@ -28,7 +28,7 @@ protected function setUp(): void $this->markTestSkipped('Either AZURE_ACCOUNT, AZURE_KEY and/or AZURE_CONTAINER env variables are not defined.'); } - $connection = sprintf('BlobEndpoint=https://%1$s.blob.core.windows.net/;AccountName=%1$s;AccountKey=%2$s', $account, $key); + $connection = sprintf('DefaultEndpointsProtocol=https;AccountName=%s;AccountKey=%s;EndpointSuffix=core.windows.net', $account, $key); $this->container = uniqid($containerName); $this->adapter = new AzureBlobStorage(new BlobProxyFactory($connection), $this->container, true); From 6a1293a486da1d1e9c11b812c69394370a2c7a1b Mon Sep 17 00:00:00 2001 From: Maxime Veber Date: Fri, 30 Sep 2022 14:53:04 +0200 Subject: [PATCH 405/426] =?UTF-8?q?=F0=9F=91=BD=20Fix=20Google=20Cloud=20S?= =?UTF-8?q?torage=20adapter=20(#664)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + CHANGELOG.md | 7 ++ Makefile | 2 +- doc/adapters/google-cloud-storage.md | 41 ++++++---- src/Gaufrette/Adapter/GoogleCloudStorage.php | 80 +++++++++++++++---- .../Adapter/GoogleCloudStorageTest.php | 8 +- .../adapters/GoogleCloudStorage.php.dist | 39 +++++---- 7 files changed, 122 insertions(+), 56 deletions(-) diff --git a/.gitignore b/.gitignore index 6d4f35a08..0d7d1c4e4 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,4 @@ phpunit.xml tests/adapters/* !tests/adapters/*.dist vendor/ +tests/Gaufrette/Functional/adapters/*.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 27d1c9da7..b80714417 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,11 @@ +v0.11.0 +======= + +## Added + +- Google Cloud Storage: options to automatically create bucket if not exists + v0.10.0 ======= diff --git a/Makefile b/Makefile index cea4c1f56..386b3ac38 100644 --- a/Makefile +++ b/Makefile @@ -24,7 +24,7 @@ docker.all-deps: docker.deps ## Install dependencies docker/run-task php${PHP_VERSION} composer require --no-update \ aws/aws-sdk-php:^3.158 \ rackspace/php-opencloud:^1.9.2 \ - google/apiclient:^1.1.3 \ + google/apiclient:^2.12 \ doctrine/dbal:^2.3 \ league/flysystem:^1.0 \ microsoft/azure-storage-blob:^1.0 \ diff --git a/doc/adapters/google-cloud-storage.md b/doc/adapters/google-cloud-storage.md index 7f1f219ce..80a7022cd 100644 --- a/doc/adapters/google-cloud-storage.md +++ b/doc/adapters/google-cloud-storage.md @@ -6,7 +6,7 @@ currentMenu: google-cloud-storage To use the GoogleCloudStorage adapter you will need to create a connection using the [Google APIs Client Library for PHP] (https://github.com/google/google-api-php-client) and create a Client ID/Service Account in your [Developers Console] -(https://console.developers.google.com/). You can then create the `\Google_Service_Storage` which is required for the +(https://console.developers.google.com/). You can then create the `\Google\Service\Storage` which is required for the GoogleCloudStorage adapter. ## Example @@ -17,23 +17,30 @@ GoogleCloudStorage adapter. use Gaufrette\Filesystem; use Gaufrette\Adapter\GoogleCloudStorage; -$client = new \Google_Client(); -$client->setClientId('xxxxxxxxxxxxxxx.apps.googleusercontent.com'); -$client->setApplicationName('Gaufrette'); +$keyFileLocation = '/home/me/path/to/service-auth-key.json'; +$bucketName = 'gaufrette-bucket-test-' . uniqid(); +$projectId = 'your-project-id-000'; +$bucketLocation = 'EUROPE-WEST9'; + +putenv('GOOGLE_APPLICATION_CREDENTIALS=' . $keyFileLocation); -$cred = new \Google_Auth_AssertionCredentials( - 'xxxxxxxxxxxxxxx@developer.gserviceaccount.com', - array(\Google_Service_Storage::DEVSTORAGE_FULL_CONTROL), - file_get_contents('key.p12') +$client = new \Google\Client(); +$client->setApplicationName('Gaufrette'); +$client->addScope(Google\Service\Storage::DEVSTORAGE_FULL_CONTROL); +$client->useApplicationDefaultCredentials(); + +$service = new \Google\Service\Storage($client); +$adapter = new GoogleCloudStorage( + $service, + $bucketName, + [ + // Options to set for automatic creation of the bucket + GoogleCloudStorage::OPTION_CREATE_BUCKET_IF_NOT_EXISTS => true, + GoogleCloudStorage::OPTION_PROJECT_ID => $projectId, + GoogleCloudStorage::OPTION_LOCATION => $bucketLocation, + ], + true ); -$client->setAssertionCredentials($cred); -if ($client->getAuth()->isAccessTokenExpired()) { - $client->getAuth()->refreshTokenWithAssertion($cred); -} - -$service = new \Google_Service_Storage($client); -$adapter = new Gaufrette\Adapter\GoogleCloudStorage($service, $config['gcsBucket'], array( - 'acl' => 'public', -), true); + $filesystem = new Gaufrette\Filesystem($adapter); ``` diff --git a/src/Gaufrette/Adapter/GoogleCloudStorage.php b/src/Gaufrette/Adapter/GoogleCloudStorage.php index e45db2121..295dcad14 100644 --- a/src/Gaufrette/Adapter/GoogleCloudStorage.php +++ b/src/Gaufrette/Adapter/GoogleCloudStorage.php @@ -3,6 +3,12 @@ namespace Gaufrette\Adapter; use Gaufrette\Adapter; +use Google\Service\Storage; +use Google\Service\Storage\Bucket; +use Google\Service\Storage\StorageObject; +use Google\Service\Exception as ServiceException; +use Google\Service\Storage\BucketIamConfiguration; +use Google\Service\Storage\BucketIamConfigurationUniformBucketLevelAccess; use GuzzleHttp; /** @@ -12,37 +18,44 @@ */ class GoogleCloudStorage implements Adapter, MetadataSupporter, ListKeysAware { + public const OPTION_CREATE_BUCKET_IF_NOT_EXISTS = 'create'; + public const OPTION_PROJECT_ID = 'project_id'; + public const OPTION_LOCATION = 'bucket_location'; + public const OPTION_STORAGE_CLASS = 'storage_class'; + protected $service; protected $bucket; - protected $options; + protected $options = [ + self::OPTION_CREATE_BUCKET_IF_NOT_EXISTS => false, + self::OPTION_STORAGE_CLASS => 'STANDARD', + 'directory' => '', + 'acl' => 'private', + ]; protected $bucketExists; protected $metadata = []; protected $detectContentType; /** - * @param \Google_Service_Storage $service The storage service class with authenticated + * @param Storage $service The storage service class with authenticated * client and full access scope * @param string $bucket The bucket name * @param array $options Options can be directory and acl * @param bool $detectContentType Whether to detect the content type or not */ public function __construct( - \Google_Service_Storage $service, + Storage $service, $bucket, array $options = [], $detectContentType = false ) { - if (!class_exists(\Google_Service_Storage::class)) { + if (!class_exists(Storage::class)) { throw new \LogicException('You need to install package "google/apiclient" to use this adapter'); } $this->service = $service; $this->bucket = $bucket; $this->options = array_replace( - [ - 'directory' => '', - 'acl' => 'private', - ], + $this->options, $options ); @@ -80,6 +93,7 @@ public function getBucket() */ public function setBucket($bucket) { + $this->bucketExists = null; $this->bucket = $bucket; } @@ -145,7 +159,7 @@ public function write($key, $content) unset($metadata['ContentType']); } - $object = new \Google_Service_Storage_StorageObject(); + $object = new StorageObject(); $object->name = $path; if (isset($metadata['ContentDisposition'])) { @@ -182,7 +196,7 @@ public function write($key, $content) } return $object->getSize(); - } catch (\Google_Service_Exception $e) { + } catch (ServiceException $e) { return false; } } @@ -197,7 +211,7 @@ public function exists($key) try { $this->service->objects->get($this->bucket, $path); - } catch (\Google_Service_Exception $e) { + } catch (ServiceException $e) { return false; } @@ -235,7 +249,7 @@ public function delete($key) try { $this->service->objects->delete($this->bucket, $path); - } catch (\Google_Service_Exception $e) { + } catch (ServiceException $e) { return false; } @@ -259,7 +273,7 @@ public function rename($sourceKey, $targetKey) try { $this->service->objects->copy($this->bucket, $sourcePath, $this->bucket, $targetPath, $object); $this->service->objects->delete($this->bucket, $sourcePath); - } catch (\Google_Service_Exception $e) { + } catch (ServiceException $e) { return false; } @@ -301,7 +315,7 @@ public function listKeys($prefix = '') $reflectionProperty->setAccessible(true); $reflectionProperty->setValue($list, 'items'); - /** @var \Google_Service_Storage_StorageObject $object */ + /** @var StorageObject $object */ foreach ($list as $object) { $keys[] = $object->name; } @@ -347,7 +361,39 @@ protected function ensureBucketExists() $this->bucketExists = true; return; - } catch (\Google_Service_Exception $e) { + } catch (ServiceException $e) { + if ($this->options[self::OPTION_CREATE_BUCKET_IF_NOT_EXISTS]) { + if (!isset($this->options[self::OPTION_PROJECT_ID])) { + throw new \RuntimeException( + sprintf('Option "%s" missing, cannot create bucket', self::OPTION_PROJECT_ID) + ); + } + if (!isset($this->options[self::OPTION_LOCATION])) { + throw new \RuntimeException( + sprintf('Option "%s" missing, cannot create bucket', self::OPTION_LOCATION) + ); + } + + $bucketIamConfigDetail = new BucketIamConfigurationUniformBucketLevelAccess(); + $bucketIamConfigDetail->setEnabled(true); + $bucketIam = new BucketIamConfiguration(); + $bucketIam->setUniformBucketLevelAccess($bucketIamConfigDetail); + $bucket = new Bucket(); + $bucket->setName($this->bucket); + $bucket->setLocation($this->options[self::OPTION_LOCATION]); + $bucket->setStorageClass($this->options[self::OPTION_STORAGE_CLASS]); + $bucket->setIamConfiguration($bucketIam); + + $this->service->buckets->insert( + $this->options[self::OPTION_PROJECT_ID], + $bucket + ); + + $this->bucketExists = true; + + return; + } + $this->bucketExists = false; throw new \RuntimeException( @@ -372,13 +418,13 @@ protected function computePath($key) * @param string $path * @param array $options * - * @return bool|\Google_Service_Storage_StorageObject + * @return bool|StorageObject */ private function getObjectData($path, $options = []) { try { return $this->service->objects->get($this->bucket, $path, $options); - } catch (\Google_Service_Exception $e) { + } catch (ServiceException $e) { return false; } } diff --git a/tests/Gaufrette/Functional/Adapter/GoogleCloudStorageTest.php b/tests/Gaufrette/Functional/Adapter/GoogleCloudStorageTest.php index 49cb1b039..8cb6f702c 100644 --- a/tests/Gaufrette/Functional/Adapter/GoogleCloudStorageTest.php +++ b/tests/Gaufrette/Functional/Adapter/GoogleCloudStorageTest.php @@ -2,6 +2,8 @@ namespace Gaufrette\Functional\Adapter; +use Gaufrette\Adapter\GoogleCloudStorage; + /** * Functional tests for the GoogleCloudStorage adapter. * @@ -15,18 +17,16 @@ class GoogleCloudStorageTest extends FunctionalTestCase /** * @test * @group functional - * - * @expectedException \RuntimeException */ public function shouldThrowExceptionIfBucketMissing() { + $this->expectException(\RuntimeException::class); /** @var \Gaufrette\Adapter\GoogleCloudStorage $adapter */ $adapter = $this->filesystem->getAdapter(); - $oldBucket = $adapter->getOptions(); + $adapter->setOptions([GoogleCloudStorage::OPTION_CREATE_BUCKET_IF_NOT_EXISTS => false]); $adapter->setBucket('Gaufrette-' . mt_rand()); $adapter->read('foo'); - $adapter->setBucket($oldBucket); } /** diff --git a/tests/Gaufrette/Functional/adapters/GoogleCloudStorage.php.dist b/tests/Gaufrette/Functional/adapters/GoogleCloudStorage.php.dist index d24db34e0..f092c3007 100644 --- a/tests/Gaufrette/Functional/adapters/GoogleCloudStorage.php.dist +++ b/tests/Gaufrette/Functional/adapters/GoogleCloudStorage.php.dist @@ -1,23 +1,28 @@ setApplicationName('Gaufrette'); +$client->addScope(Google\Service\Storage::DEVSTORAGE_FULL_CONTROL); +$client->useApplicationDefaultCredentials(); -$key = file_get_contents($key_file_location); -$cred = new \Google_Auth_AssertionCredentials( - $service_account_name, - array(\Google_Service_Storage::DEVSTORAGE_FULL_CONTROL), - $key -); -$client->setAssertionCredentials($cred); -if ($client->getAuth()->isAccessTokenExpired()) { - $client->getAuth()->refreshTokenWithAssertion($cred); -} +$service = new \Google\Service\Storage($client); -$service = new \Google_Service_Storage($client); -return new Gaufrette\Adapter\GoogleCloudStorage($service, $bucket_name, array(), true); +return new GoogleCloudStorage( + $service, + $bucketName, + [ + GoogleCloudStorage::OPTION_CREATE_BUCKET_IF_NOT_EXISTS => true, + GoogleCloudStorage::OPTION_PROJECT_ID => $projectId, + GoogleCloudStorage::OPTION_LOCATION => $bucketLocation, + ], + true +); From e5a4057584bfe8b7b8f696c877663449daef6b31 Mon Sep 17 00:00:00 2001 From: Maxime Veber Date: Fri, 30 Sep 2022 16:51:21 +0200 Subject: [PATCH 406/426] Fix wrong dependency version in the readme (#668) --- Makefile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Makefile b/Makefile index 386b3ac38..dc0bc4d3e 100644 --- a/Makefile +++ b/Makefile @@ -68,7 +68,7 @@ require-all-legacy: # kept for compatibility with the old CI config, to be remov composer require --no-update \ aws/aws-sdk-php:^3.158 \ rackspace/php-opencloud:^1.9.2 \ - google/apiclient:^1.1.3 \ + google/apiclient:^2.12 \ doctrine/dbal:^2.3 \ league/flysystem:^1.0 \ microsoft/azure-storage-blob:^1.0 \ From 0dd315f99e7fcb9135fb5df49a5da31e9632ee2a Mon Sep 17 00:00:00 2001 From: Maxime Veber Date: Fri, 30 Sep 2022 16:51:39 +0200 Subject: [PATCH 407/426] :white_check_mark: Update LocalTest (remove deprecated) (#667) --- tests/Gaufrette/Functional/Adapter/LocalTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Gaufrette/Functional/Adapter/LocalTest.php b/tests/Gaufrette/Functional/Adapter/LocalTest.php index 06c106f96..4b319a7be 100644 --- a/tests/Gaufrette/Functional/Adapter/LocalTest.php +++ b/tests/Gaufrette/Functional/Adapter/LocalTest.php @@ -154,11 +154,11 @@ public function shouldDeleteDirectory(): void /** * @test * @group functional - * @expectedException \InvalidArgumentException - * @expectedExceptionMessage Impossible to delete the root directory of this Local adapter */ public function shouldNotDeleteTheAdapterRootDirectory(): void { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Impossible to delete the root directory of this Local adapter'); $this->filesystem->getAdapter()->delete('/'); } } From 7aa0b9917fb5a5c7fab69da6207c8cbd45fc0e6f Mon Sep 17 00:00:00 2001 From: Maxime Veber Date: Fri, 30 Sep 2022 16:52:19 +0200 Subject: [PATCH 408/426] :white_check_mark: Update GridFS test (#666) --- .../Functional/Adapter/GridFSTest.php | 40 +++++-------------- 1 file changed, 10 insertions(+), 30 deletions(-) diff --git a/tests/Gaufrette/Functional/Adapter/GridFSTest.php b/tests/Gaufrette/Functional/Adapter/GridFSTest.php index 333f5e8be..6b4860556 100644 --- a/tests/Gaufrette/Functional/Adapter/GridFSTest.php +++ b/tests/Gaufrette/Functional/Adapter/GridFSTest.php @@ -45,53 +45,33 @@ public function shouldListKeys(): void //these values are canonicalized to avoid wrong order or keys issue $keys = $this->filesystem->listKeys('foo'); - $this->assertEquals( + $this->assertEqualsCanonicalizing( $this->filesystem->keys(), - $keys['keys'], - '', - 0, - 10, - true + $keys['keys'] ); $keys = $this->filesystem->listKeys('foo/foob'); - $this->assertEquals( + $this->assertEqualsCanonicalizing( ['foo/foobar/bar.txt'], - $keys['keys'], - '', - 0, - 10, - true + $keys['keys'] ); $keys = $this->filesystem->listKeys('foo/'); - $this->assertEquals( + $this->assertEqualsCanonicalizing( ['foo/foobar/bar.txt', 'foo/bar/buzz.txt'], - $keys['keys'], - '', - 0, - 10, - true + $keys['keys'] ); $keys = $this->filesystem->listKeys('foo'); - $this->assertEquals( + $this->assertEqualsCanonicalizing( ['foo/foobar/bar.txt', 'foo/bar/buzz.txt', 'foobarbuz.txt', 'foo'], - $keys['keys'], - '', - 0, - 10, - true + $keys['keys'] ); $keys = $this->filesystem->listKeys('fooz'); - $this->assertEquals( + $this->assertEqualsCanonicalizing( [], - $keys['keys'], - '', - 0, - 10, - true + $keys['keys'] ); } From 8da9c9ebb7fd5ce3e19dab6593b01900bab48a87 Mon Sep 17 00:00:00 2001 From: Maxime Veber Date: Fri, 30 Sep 2022 17:49:29 +0200 Subject: [PATCH 409/426] :white_check_mark: Fix zip test (#670) --- tests/Gaufrette/Functional/Adapter/ZipTest.php | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/tests/Gaufrette/Functional/Adapter/ZipTest.php b/tests/Gaufrette/Functional/Adapter/ZipTest.php index f8116a0bc..af9f49ba0 100644 --- a/tests/Gaufrette/Functional/Adapter/ZipTest.php +++ b/tests/Gaufrette/Functional/Adapter/ZipTest.php @@ -15,8 +15,6 @@ protected function setUp(): void $this->markTestSkipped('Zip adapter is not supported on Windows.'); } - @touch(__DIR__ . '/test.zip'); - $this->filesystem = new Filesystem(new Zip(__DIR__ . '/test.zip')); } @@ -29,25 +27,11 @@ protected function tearDown(): void /** * @test - * @expectedException \RuntimeException * @group functional */ public function shouldNotAcceptInvalidZipArchive(): void { + $this->expectException(\RuntimeException::class); new Zip(__FILE__); } - - /** - * @test - * @group functional - */ - public function shouldCreateNewZipArchive(): Zip - { - $tmp = tempnam(sys_get_temp_dir(), uniqid()); - $za = new Zip($tmp); - - $this->assertFileExists($tmp); - - return $za; - } } From 221c7dbb167eb6e6320777deef06a5c7a9e57a81 Mon Sep 17 00:00:00 2001 From: Maxime Veber Date: Fri, 7 Oct 2022 15:09:33 +0200 Subject: [PATCH 410/426] :coffin: Remove useless adapter test (#671) --- .../Functional/adapters/AmazonS3.php.dist | 10 ---- .../adapters/AzureBlobStorage.php.dist | 8 --- .../Functional/adapters/Dropbox.php.dist | 59 ------------------- .../Functional/adapters/MogileFS.php.dist | 9 --- .../Functional/adapters/Sftp.php.dist | 12 ---- 5 files changed, 98 deletions(-) delete mode 100644 tests/Gaufrette/Functional/adapters/AmazonS3.php.dist delete mode 100644 tests/Gaufrette/Functional/adapters/AzureBlobStorage.php.dist delete mode 100644 tests/Gaufrette/Functional/adapters/Dropbox.php.dist delete mode 100644 tests/Gaufrette/Functional/adapters/MogileFS.php.dist delete mode 100644 tests/Gaufrette/Functional/adapters/Sftp.php.dist diff --git a/tests/Gaufrette/Functional/adapters/AmazonS3.php.dist b/tests/Gaufrette/Functional/adapters/AmazonS3.php.dist deleted file mode 100644 index 5b6fd748c..000000000 --- a/tests/Gaufrette/Functional/adapters/AmazonS3.php.dist +++ /dev/null @@ -1,10 +0,0 @@ - $key, 'secret' => $secret)); -$service->delete_all_objects($bucket); - -return new Gaufrette\Adapter\AmazonS3($service, $bucket); diff --git a/tests/Gaufrette/Functional/adapters/AzureBlobStorage.php.dist b/tests/Gaufrette/Functional/adapters/AzureBlobStorage.php.dist deleted file mode 100644 index 415c34477..000000000 --- a/tests/Gaufrette/Functional/adapters/AzureBlobStorage.php.dist +++ /dev/null @@ -1,8 +0,0 @@ -getAccountInfo(); - - return isset($result['uid']); -}; - -$createTokens = true; - -if (file_exists($cache)) { - $tokens = unserialize(file_get_contents($cache)); - $oauth->setToken($tokens); - - if ($checkOAuth($oauth)) { - $createTokens = false; - } else { - fwrite(STDOUT, 'Need to recreate tokens.'.PHP_EOL); - } -} - -if ($createTokens) { - $tokens = $oauth->getRequestToken(); - - fwrite(STDOUT, 'Visit the following URL and accept the application:'.PHP_EOL); - fwrite(STDOUT, $oauth->getAuthorizeUrl().PHP_EOL); - fwrite(STDOUT, 'Once you\'re done, press a key.'.PHP_EOL); - - fwrite(STDOUT, 'Visit the following url in a browser, login if necessary, and click "Allow":'.PHP_EOL); - fwrite(STDOUT, $oauth->getAuthorizeUrl().PHP_EOL.PHP_EOL); - fwrite(STDOUT, 'Press enter when this is done...'); - fgets(STDIN); - - $tokens = $oauth->getAccessToken(); - $oauth->setToken($tokens); - - if ($checkOAuth($oauth)) { - file_put_contents($cache, serialize($tokens)); - fwrite(STDOUT, 'OAuth token ready!'.PHP_EOL); - } else { - fwrite(STDOUT, 'Error with token.'.PHP_EOL); - } - - file_put_contents($cache, serialize($tokens)); -} - -$dropbox = new Dropbox_API($oauth, Dropbox_API::ROOT_SANDBOX); - -$directory = $dropbox->getMetaData('/'); -foreach ($directory['contents'] as $file) { - $dropbox->delete($file['path']); -} - -return new Gaufrette\Adapter\Dropbox($dropbox); diff --git a/tests/Gaufrette/Functional/adapters/MogileFS.php.dist b/tests/Gaufrette/Functional/adapters/MogileFS.php.dist deleted file mode 100644 index 3a7201bda..000000000 --- a/tests/Gaufrette/Functional/adapters/MogileFS.php.dist +++ /dev/null @@ -1,9 +0,0 @@ -keys() as $key) { - $adapter->delete($key); -} - -return $adapter; diff --git a/tests/Gaufrette/Functional/adapters/Sftp.php.dist b/tests/Gaufrette/Functional/adapters/Sftp.php.dist deleted file mode 100644 index 038ef297c..000000000 --- a/tests/Gaufrette/Functional/adapters/Sftp.php.dist +++ /dev/null @@ -1,12 +0,0 @@ -getSftp(), '/some/directory'); - -ssh2_exec($session->getresource(), sprintf('rm -r %s/*', $directory)); - -return $adapter; From 54a66d2fb4dabd2a814c05f7bd50c91ff808dd0a Mon Sep 17 00:00:00 2001 From: Maxime Veber Date: Fri, 7 Oct 2022 15:10:12 +0200 Subject: [PATCH 411/426] :fire: Remove support for OpenCloud (#669) --- .env.dist | 4 - Makefile | 3 - spec/Gaufrette/Adapter/OpenCloudSpec.php | 264 ------------------ src/Gaufrette/Adapter/OpenCloud.php | 255 ----------------- .../ObjectStoreFactory.php | 55 ---- .../ObjectStoreFactoryInterface.php | 18 -- .../Functional/Adapter/OpenCloudTest.php | 60 ---- 7 files changed, 659 deletions(-) delete mode 100644 spec/Gaufrette/Adapter/OpenCloudSpec.php delete mode 100644 src/Gaufrette/Adapter/OpenCloud.php delete mode 100644 src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactory.php delete mode 100644 src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactoryInterface.php delete mode 100644 tests/Gaufrette/Functional/Adapter/OpenCloudTest.php diff --git a/.env.dist b/.env.dist index 1e6a2ff15..d3cc06560 100644 --- a/.env.dist +++ b/.env.dist @@ -15,10 +15,6 @@ FTP_BASE_DIR=/gaufrette MONGO_URI=mongodb://mongodb:27017 MONGO_DBNAME=gridfs_test -RACKSPACE_USER= -RACKSPACE_APIKEY= -RACKSPACE_CONTAINER= - SFTP_HOST=sftp SFTP_PORT=22 SFTP_USER=gaufrette diff --git a/Makefile b/Makefile index dc0bc4d3e..32b2a9967 100644 --- a/Makefile +++ b/Makefile @@ -23,7 +23,6 @@ docker.deps: ## Install dependencies docker.all-deps: docker.deps ## Install dependencies docker/run-task php${PHP_VERSION} composer require --no-update \ aws/aws-sdk-php:^3.158 \ - rackspace/php-opencloud:^1.9.2 \ google/apiclient:^2.12 \ doctrine/dbal:^2.3 \ league/flysystem:^1.0 \ @@ -56,7 +55,6 @@ docker.php-cs-fix: ## Run CS fixer remove-phpspec: ## Remove adapter specs (allows you to run test suite without adapters deps) rm spec/Gaufrette/Adapter/AsyncAwsS3Spec.php rm spec/Gaufrette/Adapter/AwsS3Spec.php - rm spec/Gaufrette/Adapter/OpenCloudSpec.php rm spec/Gaufrette/Adapter/GoogleCloudStorageSpec.php rm spec/Gaufrette/Adapter/DoctrineDbalSpec.php rm spec/Gaufrette/Adapter/FlysystemSpec.php @@ -67,7 +65,6 @@ remove-phpspec: ## Remove adapter specs (allows you to run test suite without ad require-all-legacy: # kept for compatibility with the old CI config, to be removed at some point composer require --no-update \ aws/aws-sdk-php:^3.158 \ - rackspace/php-opencloud:^1.9.2 \ google/apiclient:^2.12 \ doctrine/dbal:^2.3 \ league/flysystem:^1.0 \ diff --git a/spec/Gaufrette/Adapter/OpenCloudSpec.php b/spec/Gaufrette/Adapter/OpenCloudSpec.php deleted file mode 100644 index 55e8a04de..000000000 --- a/spec/Gaufrette/Adapter/OpenCloudSpec.php +++ /dev/null @@ -1,264 +0,0 @@ - - * @author Daniel Richter - */ -class OpenCloudSpec extends ObjectBehavior -{ - /** - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container - */ - function let(Service $objectStore, Container $container) - { - $objectStore->getContainer('test')->willReturn($container); - $this->beConstructedWith($objectStore, 'test', false); - } - - function it_is_adapter() - { - $this->shouldHaveType('Gaufrette\Adapter'); - } - - /** - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\ObjectStore\Resource\DataObject $object - */ - function it_reads_file(Container $container, DataObject $object) - { - $object->getContent()->willReturn('Hello World'); - $container->getObject('test')->willReturn($object); - - $this->read('test')->shouldReturn('Hello World'); - } - - /** - * @param OpenCloud\ObjectStore\Resource\Container $container - */ - function it_reads_file_on_error_returns_false(Container $container) - { - $container->getObject('test')->willThrow(new ObjectNotFoundException()); - - $this->read('test')->shouldReturn(false); - } - - /** - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\ObjectStore\Resource\DataObject $object - */ - function it_writes_file_returns_size(Container $container, DataObject $object) - { - $testData = 'Hello World!'; - $testDataSize = strlen($testData); - - $object->getContentLength()->willReturn($testDataSize); - $container->uploadObject('test', $testData)->willReturn($object); - - $this->write('test', $testData)->shouldReturn($testDataSize); - } - - /** - * @param OpenCloud\ObjectStore\Resource\Container $container - */ - function it_writes_file_and_write_fails_returns_false(Container $container) - { - $testData = 'Hello World!'; - - $container->uploadObject('test', $testData)->willThrow(new CreateUpdateError()); - - $this->write('test', $testData)->shouldReturn(false); - } - - /** - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\ObjectStore\Resource\DataObject $object - */ - function it_returns_true_if_key_exists(Container $container, DataObject $object) - { - $container->getPartialObject('test')->willReturn($object); - - $this->exists('test')->shouldReturn(true); - } - - /** - * @param OpenCloud\ObjectStore\Resource\Container $container - */ - function it_returns_false_if_key_does_not_exist(Container $container) - { - $container->getPartialObject('test')->willThrow(new BadResponseException()); - - $this->exists('test')->shouldReturn(false); - } - - /** - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\ObjectStore\Resource\DataObject $object - */ - function it_deletes_file_on_success_returns_true(Container $container, DataObject $object) - { - $object->delete()->willReturn(null); - $container->getObject('test')->willReturn($object); - - $this->delete('test')->shouldReturn(true); - } - - /** - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\ObjectStore\Resource\DataObject $object - */ - function it_deletes_file_returns_false_on_failure(Container $container, DataObject $object) - { - $object->delete()->willThrow(new DeleteError()); - $container->getObject('test')->willReturn($object); - - $this->delete('test')->shouldReturn(false); - } - - /** - * @param OpenCloud\ObjectStore\Resource\Container $container - */ - function it_deletes_file_if_file_does_not_exist_returns_false(Container $container) - { - $container->getObject('test')->willThrow(new ObjectNotFoundException()); - - $this->delete('test')->shouldReturn(false); - } - - /** - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\ObjectStore\Resource\DataObject $object - */ - function it_returns_checksum_if_file_exists(Container $container, DataObject $object) - { - $object->getEtag()->willReturn('test String'); - $container->getObject('test')->willReturn($object); - - $this->checksum('test')->shouldReturn('test String'); - } - - /** - * @param OpenCloud\ObjectStore\Resource\Container $container - */ - function it_returns_false_when_file_does_not_exist(Container $container) - { - $container->getObject('test')->willThrow(new ObjectNotFoundException()); - - $this->checksum('test')->shouldReturn(false); - } - - /** - * @param OpenCloud\ObjectStore\Resource\Container $container - * @param OpenCloud\Common\Collection $objectList - * @param OpenCloud\ObjectStore\Resource\DataObject $object1 - * @param OpenCloud\ObjectStore\Resource\DataObject $object2 - * @param OpenCloud\ObjectStore\Resource\DataObject $object3 - */ - function it_returns_files_as_sorted_array(Container $container, Collection $objectList, DataObject $object1, DataObject $object2, DataObject $object3) - { - $outputArray = ['key1', 'key2', 'key5']; - $index = 0; - - $object1->getName()->willReturn('key5'); - $object2->getName()->willReturn('key2'); - $object3->getName()->willReturn('key1'); - - $objects = [$object1, $object2, $object3]; - - $objectList->next()->will( - function () use ($objects, &$index) { - if ($index < count($objects)) { - $index++; - - return $objects[$index - 1]; - } - } - ) ->shouldBeCalledTimes(count($objects) + 1); - - $container->objectList()->willReturn($objectList); - - $this->keys()->shouldReturn($outputArray); - } - - /** - * @param OpenCloud\ObjectStore\Service $objectStore - */ - function it_throws_exception_if_container_does_not_exist(Service $objectStore) - { - $containerName = 'container-does-not-exist'; - - $objectStore->getContainer($containerName)->willThrow(new BadResponseException()); - $this->beConstructedWith($objectStore, $containerName); - - $this->shouldThrow('\RuntimeException')->duringExists('test'); - } - - /** - * @param OpenCloud\ObjectStore\Service $objectStore - * @param OpenCloud\ObjectStore\Resource\Container $container - */ - function it_creates_container(Service $objectStore, Container $container) - { - $containerName = 'container-does-not-yet-exist'; - $filename = 'test'; - - $objectStore->getContainer($containerName)->willThrow(new BadResponseException()); - $objectStore->createContainer($containerName)->willReturn($container); - $container->getPartialObject($filename)->willThrow(new BadResponseException()); - - $this->beConstructedWith($objectStore, $containerName, true); - - $this->exists($filename)->shouldReturn(false); - } - - /** - * @param OpenCloud\ObjectStore\Service $objectStore - */ - function it_throws_exeption_if_container_creation_fails(Service $objectStore) - { - $containerName = 'container-does-not-yet-exist'; - - $objectStore->getContainer($containerName)->willThrow(new BadResponseException()); - $objectStore->createContainer($containerName)->willReturn(false); - - $this->beConstructedWith($objectStore, $containerName, true); - - $this->shouldThrow('\RuntimeException')->duringExists('test'); - } - - /** - * @param OpenCloud\ObjectStore\Resource\Container $container - */ - function it_returns_false_if_the_object_does_not_exists_when_fetching_mtime(Container $container) - { - $container->getObject('foo')->willThrow(ObjectNotFoundException::class); - - $this->mtime('foo')->shouldReturn(false); - } - - /** - * @param OpenCloud\ObjectStore\Resource\DataObject $object - * @param OpenCloud\ObjectStore\Resource\Container $container - */ - function it_fetches_file_mtime(DataObject $object, Container $container) - { - $container->getObject('foo')->willReturn($object); - $object->getLastModified()->willReturn('Tue, 13 Jun 2017 22:02:34 GMT'); - - $this->mtime('foo')->shouldReturn('1497391354'); - } -} diff --git a/src/Gaufrette/Adapter/OpenCloud.php b/src/Gaufrette/Adapter/OpenCloud.php deleted file mode 100644 index 165ed93e2..000000000 --- a/src/Gaufrette/Adapter/OpenCloud.php +++ /dev/null @@ -1,255 +0,0 @@ - - * @author Daniel Richter - */ -class OpenCloud implements Adapter, ChecksumCalculator -{ - /** - * @var Service - */ - protected $objectStore; - - /** - * @var string - */ - protected $containerName; - - /** - * @var bool - */ - protected $createContainer; - - /** - * @var Container - */ - protected $container; - - /** - * @param Service $objectStore - * @param string $containerName The name of the container - * @param bool $createContainer Whether to create the container if it does not exist - */ - public function __construct(Service $objectStore, $containerName, $createContainer = false) - { - if (!class_exists(Service::class)) { - throw new \LogicException('You need to install package "rackspace/php-opencloud" to use this adapter'); - } - $this->objectStore = $objectStore; - $this->containerName = $containerName; - $this->createContainer = $createContainer; - } - - /** - * Returns an initialized container. - * - * @throws \RuntimeException - * - * @return Container - */ - protected function getContainer() - { - if ($this->container) { - return $this->container; - } - - try { - return $this->container = $this->objectStore->getContainer($this->containerName); - } catch (BadResponseException $e) { //OpenCloud lib does not wrap this exception - if (!$this->createContainer) { - throw new \RuntimeException(sprintf('Container "%s" does not exist.', $this->containerName)); - } - } - - if (!$container = $this->objectStore->createContainer($this->containerName)) { - throw new \RuntimeException(sprintf('Container "%s" could not be created.', $this->containerName)); - } - - return $this->container = $container; - } - - /** - * Reads the content of the file. - * - * @param string $key - * - * @return string|bool if cannot read content - */ - public function read($key) - { - if ($object = $this->tryGetObject($key)) { - return $object->getContent(); - } - - return false; - } - - /** - * Writes the given content into the file. - * - * @param string $key - * @param string $content - * - * @return int|bool The number of bytes that were written into the file - */ - public function write($key, $content) - { - try { - $this->getContainer()->uploadObject($key, $content); - } catch (CreateUpdateError $updateError) { - return false; - } - - return Util\Size::fromContent($content); - } - - /** - * Indicates whether the file exists. - * - * @param string $key - * - * @return bool - */ - public function exists($key) - { - try { - $exists = $this->getContainer()->getPartialObject($key) !== false; - } catch (BadResponseException $objFetchError) { - return false; - } - - return $exists; - } - - /** - * Returns an array of all keys (files and directories). - * - * @return array - */ - public function keys() - { - $objectList = $this->getContainer()->objectList(); - $keys = []; - - while ($object = $objectList->next()) { - $keys[] = $object->getName(); - } - - sort($keys); - - return $keys; - } - - /** - * Returns the last modified time. - * - * @param string $key - * - * @return int|bool An UNIX like timestamp or false - */ - public function mtime($key) - { - if ($object = $this->tryGetObject($key)) { - return (new \DateTime($object->getLastModified()))->format('U'); - } - - return false; - } - - /** - * Deletes the file. - * - * @param string $key - * - * @return bool - */ - public function delete($key) - { - if (!$object = $this->tryGetObject($key)) { - return false; - } - - try { - $object->delete(); - } catch (DeleteError $deleteError) { - return false; - } - - return true; - } - - /** - * Renames a file. - * - * @param string $sourceKey - * @param string $targetKey - * - * @return bool - */ - public function rename($sourceKey, $targetKey) - { - if (false !== $this->write($targetKey, $this->read($sourceKey))) { - $this->delete($sourceKey); - - return true; - } - - return false; - } - - /** - * Check if key is directory. - * - * @param string $key - * - * @return bool - */ - public function isDirectory($key) - { - return false; - } - - /** - * Returns the checksum of the specified key. - * - * @param string $key - * - * @return string - */ - public function checksum($key) - { - if ($object = $this->tryGetObject($key)) { - return $object->getETag(); - } - - return false; - } - - /** - * @param string $key - * - * @return \OpenCloud\ObjectStore\Resource\DataObject|false - */ - protected function tryGetObject($key) - { - try { - return $this->getContainer()->getObject($key); - } catch (ObjectNotFoundException $objFetchError) { - return false; - } - } -} diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactory.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactory.php deleted file mode 100644 index 996ae2925..000000000 --- a/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactory.php +++ /dev/null @@ -1,55 +0,0 @@ - - */ -class ObjectStoreFactory implements ObjectStoreFactoryInterface -{ - /** - * @var OpenStack - */ - protected $connection; - - /** - * @var string - */ - protected $region; - - /** - * @var string - */ - protected $urlType; - - /** - * @var string - */ - protected $objectStoreType; - - /** - * @param OpenStack $connection - * @param string $region - * @param string $urlType - * @param string $objectStoreType - */ - public function __construct(OpenStack $connection, $region, $urlType, $objectStoreType = 'cloudFiles') - { - $this->connection = $connection; - $this->region = $region; - $this->urlType = $urlType; - $this->objectStoreType = $objectStoreType; - } - - /** - * {@inheritdoc} - */ - public function getObjectStore() - { - return $this->connection->objectStoreService($this->objectStoreType, $this->region, $this->urlType); - } -} diff --git a/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactoryInterface.php b/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactoryInterface.php deleted file mode 100644 index 1129d8ea5..000000000 --- a/src/Gaufrette/Adapter/OpenStackCloudFiles/ObjectStoreFactoryInterface.php +++ /dev/null @@ -1,18 +0,0 @@ - - */ -interface ObjectStoreFactoryInterface -{ - /** - * @return Service - */ - public function getObjectStore(); -} diff --git a/tests/Gaufrette/Functional/Adapter/OpenCloudTest.php b/tests/Gaufrette/Functional/Adapter/OpenCloudTest.php deleted file mode 100644 index fe7096273..000000000 --- a/tests/Gaufrette/Functional/Adapter/OpenCloudTest.php +++ /dev/null @@ -1,60 +0,0 @@ -markTestSkipped('Either RACKSPACE_USER, RACKSPACE_APIKEY and/or RACKSPACE_CONTAINER env vars are missing.'); - } - - $connection = new Rackspace( - 'https://identity.api.rackspacecloud.com/v2.0/', - [ - 'username' => $username, - 'apiKey' => $apiKey, - ], - [ - // Guzzle ships with outdated certs - // @see https://github.com/rackspace/php-opencloud/issues/727 - Rackspace::SSL_CERT_AUTHORITY => 'system', - Rackspace::CURL_OPTIONS => [ - CURLOPT_SSL_VERIFYPEER => true, - CURLOPT_SSL_VERIFYHOST => 2, - ], - ] - ); - - $this->container = uniqid($container); - $this->objectStore = $connection->objectStoreService('cloudFiles', 'IAD', 'publicURL'); - $this->objectStore->createContainer($this->container); - - $adapter = new OpenCloud($this->objectStore, $this->container); - $this->filesystem = new Filesystem($adapter); - } - - protected function tearDown(): void - { - if ($this->filesystem === null) { - return; - } - - $this->objectStore->getContainer($this->container)->delete(true); - } -} From 4628376e8f7ec52ecf29575a7d12ccd46083661c Mon Sep 17 00:00:00 2001 From: Maxime Veber Date: Fri, 7 Oct 2022 15:34:06 +0200 Subject: [PATCH 412/426] :memo: Remove doc about opencloud (#672) --- CHANGELOG.md | 4 ++ doc/adapters/open-cloud.md | 83 -------------------------------------- 2 files changed, 4 insertions(+), 83 deletions(-) delete mode 100644 doc/adapters/open-cloud.md diff --git a/CHANGELOG.md b/CHANGELOG.md index b80714417..dfc6d541e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ v0.11.0 - Google Cloud Storage: options to automatically create bucket if not exists +## Removed + +- Adapter for OpenCloud (#669) + v0.10.0 ======= diff --git a/doc/adapters/open-cloud.md b/doc/adapters/open-cloud.md deleted file mode 100644 index 00ef34f74..000000000 --- a/doc/adapters/open-cloud.md +++ /dev/null @@ -1,83 +0,0 @@ ---- -currentMenu: open-cloud ---- - -# OpenCloud & LazyOpenCloud - -**Warning: LazyOpenCloud adapter has been deprecated since v0.4.0 and will be removed in v1.0.0.** - -First, you will need to install the adapter: -```bash -composer require gaufrette/opencloud-adapter -``` - -To use the OpenCloud adapter you will need to create a connection using the [OpenCloud SDK](https://github.com/rackspace/php-opencloud). -You can then fetch the ObjectStore which is required for the OpenCloud adapter. - -## OpenCloud Example - -```php - 'your username', - 'password' => 'your Keystone password', - 'tenantName' => 'your tenant (project) name' - ) -); -$objectStore = $connection->objectStoreService('cloudFiles', 'LON', 'publicURL'); - -$adapter = new OpenCloudAdapter( - $objectStore, - 'container-name' -); -$filesystem = new Filesystem($adapter); -``` - -## Rackspace Example - -Rackspace uses a difference connection class - -```php - 'rackspace-user', - 'apiKey' => '0900af093093788912388fc09dde090ffee09' - ) -); - -$objectStore = $connection->objectStoreService('cloudFiles', 'LON', 'publicURL'); - -$adapter = new OpenCloudAdapter( - $objectStore, - 'container-name' -); -$filesystem = new Filesystem($adapter); -``` - -## LazyOpenCloud Example - -Instantiating the OpenCloud object store service has some overhead because it issues an authentication request, -even if you end up not using the filesystem. For better performance you can use a lazy-loading adapter which only authenticates when needed. - -```php - Date: Fri, 7 Oct 2022 16:17:07 +0200 Subject: [PATCH 413/426] :construction_worker: Fix phpstan execution on CI (#674) --- .github/workflows/ci.yml | 1 - .github/workflows/static.yml | 6 +++--- Makefile | 3 +++ phpstan.neon.dist | 3 --- src/Gaufrette/Adapter/AzureBlobStorage.php | 6 +++--- src/Gaufrette/Adapter/Flysystem.php | 2 +- src/Gaufrette/Adapter/Ftp.php | 2 +- src/Gaufrette/StreamWrapper.php | 14 +++++++------- 8 files changed, 18 insertions(+), 19 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23fa2b425..d8a0f3a2d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,7 +44,6 @@ jobs: run: - { php: '7.4', packages: 'async-aws/simple-s3:^1.0', phpspec: 'spec/Gaufrette/Adapter/AsyncAwsS3Spec.php' } - { php: '7.4', packages: 'aws/aws-sdk-php:^3.158', phpspec: 'spec/Gaufrette/Adapter/AwsS3Spec.php' } - - { php: '7.4', packages: 'rackspace/php-opencloud:^1.9.2', phpspec: 'spec/Gaufrette/Adapter/OpenCloudSpec.php' } - { php: '7.4', packages: 'google/apiclient:^1.1.3', phpspec: 'spec/Gaufrette/Adapter/GoogleCloudStorageSpec.php' } - { php: '7.4', packages: 'doctrine/dbal:^2.3', phpspec: 'spec/Gaufrette/Adapter/DoctrineDbalSpec.php' } - { php: '7.4', packages: 'league/flysystem:^1.0', phpspec: 'spec/Gaufrette/Adapter/FlysystemSpec.php' } diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml index ea8346d95..e3f643a42 100644 --- a/.github/workflows/static.yml +++ b/.github/workflows/static.yml @@ -10,7 +10,7 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: '7.4' + php-version: '8.1' extensions: mongo, mbstring, fileinfo tools: composer:v2 @@ -20,7 +20,7 @@ jobs: - name: Download dependencies run: | make require-all - composer require --dev --no-update phpstan/phpstan:0.12.48 + composer require --dev --no-update phpstan/phpstan:1.8.8 composer update --no-interaction --prefer-dist - name: PHPStan @@ -37,7 +37,7 @@ jobs: - name: Set up PHP uses: shivammathur/setup-php@v2 with: - php-version: '7.4' + php-version: '8.1' extensions: mongo, mbstring, fileinfo tools: composer:v2 diff --git a/Makefile b/Makefile index 32b2a9967..c1c435cb2 100644 --- a/Makefile +++ b/Makefile @@ -80,6 +80,9 @@ require-all: require-all-legacy ## Install all dependencies for adapters clear: ## Remove not versioned files rm -rf vendor/ composer.lock +test.phpstan: ## Run phpstan analysis + php vendor/bin/phpstan analyze --memory-limit 1G + # ### OTHERS # -------- diff --git a/phpstan.neon.dist b/phpstan.neon.dist index 0dfc69082..72b5c82e0 100644 --- a/phpstan.neon.dist +++ b/phpstan.neon.dist @@ -6,6 +6,3 @@ parameters: reportUnmatchedIgnoredErrors: false paths: - src - - scanFiles: - - vendor/google/apiclient/src/Google/Task/Retryable.php diff --git a/src/Gaufrette/Adapter/AzureBlobStorage.php b/src/Gaufrette/Adapter/AzureBlobStorage.php index 256453b94..221df9eaa 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage.php @@ -6,11 +6,11 @@ use Gaufrette\Util; use Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface; use MicrosoftAzure\Storage\Blob\Models\Blob; +use MicrosoftAzure\Storage\Blob\Models\BlobServiceOptions; use MicrosoftAzure\Storage\Blob\Models\Container; use MicrosoftAzure\Storage\Blob\Models\CreateBlobOptions; use MicrosoftAzure\Storage\Blob\Models\CreateBlockBlobOptions; use MicrosoftAzure\Storage\Blob\Models\CreateContainerOptions; -use MicrosoftAzure\Storage\Blob\Models\DeleteContainerOptions; use MicrosoftAzure\Storage\Blob\Models\ListBlobsOptions; use MicrosoftAzure\Storage\Common\Exceptions\ServiceException; @@ -130,11 +130,11 @@ public function createContainer($containerName, CreateContainerOptions $options * Deletes a container. * * @param string $containerName - * @param DeleteContainerOptions $options + * @param BlobServiceOptions $options * * @throws \RuntimeException if cannot delete the container */ - public function deleteContainer($containerName, DeleteContainerOptions $options = null) + public function deleteContainer($containerName, BlobServiceOptions $options = null) { $this->init(); diff --git a/src/Gaufrette/Adapter/Flysystem.php b/src/Gaufrette/Adapter/Flysystem.php index aab29e883..b4038e5ce 100644 --- a/src/Gaufrette/Adapter/Flysystem.php +++ b/src/Gaufrette/Adapter/Flysystem.php @@ -21,7 +21,7 @@ class Flysystem implements Adapter, ListKeysAware /** * @param AdapterInterface $adapter - * @param Config|array|null $config + * @param \League\Flysystem\Config|array|null $config */ public function __construct(AdapterInterface $adapter, $config = null) { diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index 6cc229ea2..8ea3bb141 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -501,7 +501,7 @@ private function isConnected() * Returns an opened ftp connection resource. If the connection is not * already opened, it open it before. * - * @return resource The ftp connection + * @return resource|\FTP\Connection The ftp connection */ private function getConnection() { diff --git a/src/Gaufrette/StreamWrapper.php b/src/Gaufrette/StreamWrapper.php index bc62f64ad..4497af370 100644 --- a/src/Gaufrette/StreamWrapper.php +++ b/src/Gaufrette/StreamWrapper.php @@ -21,7 +21,7 @@ class StreamWrapper */ public static function setFilesystemMap(FilesystemMap $map) { - static::$filesystemMap = $map; + self::$filesystemMap = $map; } /** @@ -31,11 +31,11 @@ public static function setFilesystemMap(FilesystemMap $map) */ public static function getFilesystemMap() { - if (null === static::$filesystemMap) { - static::$filesystemMap = static::createFilesystemMap(); + if (null === self::$filesystemMap) { + self::$filesystemMap = self::createFilesystemMap(); } - return static::$filesystemMap; + return self::$filesystemMap; } /** @@ -45,9 +45,9 @@ public static function getFilesystemMap() */ public static function register($scheme = 'gaufrette') { - static::streamWrapperUnregister($scheme); + self::streamWrapperUnregister($scheme); - if (!static::streamWrapperRegister($scheme, __CLASS__)) { + if (!self::streamWrapperRegister($scheme, __CLASS__)) { throw new \RuntimeException(sprintf( 'Could not register stream wrapper class %s for scheme %s.', __CLASS__, @@ -271,7 +271,7 @@ protected function createStream($path) )); } - return static::getFilesystemMap()->get($domain)->createStream($key); + return self::getFilesystemMap()->get($domain)->createStream($key); } protected function createStreamMode($mode) From d22b8e1fc972c01343a031edd36f70d44c7c23c6 Mon Sep 17 00:00:00 2001 From: Maxime Veber Date: Fri, 7 Oct 2022 16:17:29 +0200 Subject: [PATCH 414/426] :construction_worker: Add BC Check to Makefile (#675) --- Makefile | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Makefile b/Makefile index c1c435cb2..fc29c04ee 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,7 @@ .DEFAULT_GOAL := help PHP_VERSION ?= 7.2 +export ROOT_DIR=${PWD} # ### DOCKER @@ -76,6 +77,10 @@ require-all-legacy: # kept for compatibility with the old CI config, to be remov require-all: require-all-legacy ## Install all dependencies for adapters composer require --no-update async-aws/simple-s3:^1.0 +.PHONY: bc-check +bc-check: ## Check for backward compatibility change + docker run -v ${ROOT_DIR}:/app --rm nyholm/roave-bc-check + .PHONY: clear clear: ## Remove not versioned files rm -rf vendor/ composer.lock From b9638cc04a19a583b11a5c74b1c370562dc5f5d6 Mon Sep 17 00:00:00 2001 From: Maxime Veber Date: Fri, 7 Oct 2022 16:17:42 +0200 Subject: [PATCH 415/426] :memo: Update changelog after some fix (#676) --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dfc6d541e..14b95763e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,10 @@ v0.11.0 - Google Cloud Storage: options to automatically create bucket if not exists +## Updated + +- Azure seems to have break compatibility, phpstan detected it and it's now fixed (#674) + ## Removed - Adapter for OpenCloud (#669) From e1eccd87e38795fff4a8207974ca01e1253a789e Mon Sep 17 00:00:00 2001 From: Maxime Veber Date: Fri, 7 Oct 2022 17:38:36 +0200 Subject: [PATCH 416/426] =?UTF-8?q?=E2=9D=A4=EF=B8=8F=20Stands=20with=20Uk?= =?UTF-8?q?raine=20(#673)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.md b/README.md index f11748e54..38ecb0c62 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,9 @@ Gaufrette ========= +[![Stand With Ukraine](https://raw.githubusercontent.com/vshymanskyy/StandWithUkraine/main/banner2-direct.svg)](https://stand-with-ukraine.pp.ua) + + Gaufrette provides a filesystem abstraction layer. [![Build Status](https://github.com/KnpLabs/Gaufrette/actions/workflows/ci.yml/badge.svg)](https://github.com/KnpLabs/Gaufrette/actions) From df86fe5db0fbeb5a201d64b88a5f9596fd880338 Mon Sep 17 00:00:00 2001 From: Maxime Veber Date: Sat, 8 Oct 2022 01:04:05 +0200 Subject: [PATCH 417/426] :green_heart: Fix GoogleCloudStorage CI execution (#677) --- .github/workflows/ci.yml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d8a0f3a2d..577869799 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,14 +42,14 @@ jobs: max-parallel: 10 matrix: run: - - { php: '7.4', packages: 'async-aws/simple-s3:^1.0', phpspec: 'spec/Gaufrette/Adapter/AsyncAwsS3Spec.php' } - - { php: '7.4', packages: 'aws/aws-sdk-php:^3.158', phpspec: 'spec/Gaufrette/Adapter/AwsS3Spec.php' } - - { php: '7.4', packages: 'google/apiclient:^1.1.3', phpspec: 'spec/Gaufrette/Adapter/GoogleCloudStorageSpec.php' } - - { php: '7.4', packages: 'doctrine/dbal:^2.3', phpspec: 'spec/Gaufrette/Adapter/DoctrineDbalSpec.php' } - - { php: '7.4', packages: 'league/flysystem:^1.0', phpspec: 'spec/Gaufrette/Adapter/FlysystemSpec.php' } - - { php: '7.4', packages: 'microsoft/azure-storage-blob:^1.0', phpspec: 'spec/Gaufrette/Adapter/AzureBlobStore' } - - { php: '7.4', packages: 'mongodb/mongodb:^1.1', phpspec: 'spec/Gaufrette/Adapter/GridFSSpec.php' } - - { php: '7.4', packages: 'phpseclib/phpseclib:^2.0', phpspec: 'spec/Gaufrette/Adapter/PhpseclibSftpSpec.php' } + - { php: '8.1', packages: 'async-aws/simple-s3:^1.0', phpspec: 'spec/Gaufrette/Adapter/AsyncAwsS3Spec.php' } + - { php: '8.1', packages: 'aws/aws-sdk-php:^3.158', phpspec: 'spec/Gaufrette/Adapter/AwsS3Spec.php' } + - { php: '8.1', packages: 'google/apiclient:^2.12', phpspec: 'spec/Gaufrette/Adapter/GoogleCloudStorageSpec.php' } + - { php: '8.1', packages: 'doctrine/dbal:^2.3', phpspec: 'spec/Gaufrette/Adapter/DoctrineDbalSpec.php' } + - { php: '8.1', packages: 'league/flysystem:^1.0', phpspec: 'spec/Gaufrette/Adapter/FlysystemSpec.php' } + - { php: '8.1', packages: 'microsoft/azure-storage-blob:^1.0', phpspec: 'spec/Gaufrette/Adapter/AzureBlobStore' } + - { php: '8.1', packages: 'mongodb/mongodb:^1.1', phpspec: 'spec/Gaufrette/Adapter/GridFSSpec.php' } + - { php: '8.1', packages: 'phpseclib/phpseclib:^2.0', phpspec: 'spec/Gaufrette/Adapter/PhpseclibSftpSpec.php' } steps: - name: Set up PHP From aa121d5c66b9c91763a3e44cec7e0face82e6057 Mon Sep 17 00:00:00 2001 From: Sri Ram <51131591+sriram-km@users.noreply.github.com> Date: Sat, 8 Oct 2022 04:35:27 +0530 Subject: [PATCH 418/426] Fixed gramatical errors in README.md (#678) --- README.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 38ecb0c62..a61517175 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,13 @@ Gaufrette provides a filesystem abstraction layer. Why use Gaufrette? ------------------ -Imagine you have to manage a lot of medias in a PHP project. Lets see how to -take this situation in your advantage using Gaufrette. +Imagine you have to manage a lot of media in a PHP project. Let's see how to +take this situation to your advantage using Gaufrette. The filesystem abstraction layer permits you to develop your application without -the need to know where all those medias will be stored and how. +the need to know where all those media will be stored and how. -Another advantage of this is the possibility to update the files location +Another advantage of this is the possibility to update the location of the files without any impact on the code apart from the definition of your filesystem. In example, if your project grows up very fast and if your server reaches its limits, you can easily move your medias in an Amazon S3 server or any other @@ -34,7 +34,7 @@ Read the official [Gaufrette documentation](http://knplabs.github.io/Gaufrette/) ### Metapackages for adapters -Every maintained adapter now have a dedicated metapackage. You can [find the list on packagist](https://packagist.org/packages/gaufrette/). +Every maintained adapter now has a dedicated metapackage. You can [find the list on packagist](https://packagist.org/packages/gaufrette/). **We highly recommend you to use them as they contain their own requirements**: you don't need to worry about third-party dependencies to install before using Gaufrette anymore. @@ -44,7 +44,7 @@ Symfony integration is available through [KnpLabs/KnpGaufretteBundle](https://gi ### Maintainers -Here is the list of dedicated maintainer(s) for every adapter not deprecated. If you don't receive any response to +Here is the list of the dedicated maintainer(s) for every adapter not deprecated. If you don't receive any response to your issue or pull request in a timely manner, ping us: | Adapter | Referent | @@ -63,7 +63,7 @@ your issue or pull request in a timely manner, ping us: | PhpseclibSftp | @fabschurt | | Zip | | -For `InMemory`, `Local` and `Zip` adapters everyone in this list is considered as a maintainer. +For `InMemory`, `Local`, and `Zip` adapters everyone in this list is considered as a maintainer. ### Development @@ -77,7 +77,7 @@ $ make docker.dev ``` and configure it as you want. -2) Build the php docker image : +2) Build the PHP docker image : ```bash $ make docker.build ``` @@ -96,13 +96,13 @@ You can also use a different php version, simply set the `PHP_VERSION` env var to any of these values when calling a make target : - `7.1` - `7.2` (default) -- `7.3` (the docker setup for php 7.3 is available, however the ssh2 extension -is not installed [as it is not available for php 7.3 yet](https://serverpilot.io/docs/how-to-install-the-php-ssh2-extension)) +- `7.3` (The docker setup for PHP 7.3 is available. However, the ssh2 extension +is not installed [as it is not available for PHP 7.3 yet](https://serverpilot.io/docs/how-to-install-the-php-ssh2-extension)) See the [`docker-compose.yml`](/docker-compose.yml) file for more details. You'll need to clear the previously installed dependencies when switching from -a version to an other, to do so, run : +one version to an other. To do so, run : ```bash $ make clear-deps $ PHP_VERSION= make build install-deps From 610539674a17f4ea904a0e8a32f2004c42723709 Mon Sep 17 00:00:00 2001 From: Maxime Veber Date: Fri, 14 Oct 2022 17:28:53 +0200 Subject: [PATCH 419/426] :arrow_up: Compatibility with doctrine/dbal 3.x (#681) --- .github/workflows/ci.yml | 1 + Makefile | 4 +- spec/Gaufrette/Adapter/DoctrineDbalSpec.php | 63 +++++++++++++++---- src/Gaufrette/Adapter/DoctrineDbal.php | 28 ++++++++- .../Functional/Adapter/DoctrineDbalTest.php | 9 ++- 5 files changed, 87 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 577869799..691079722 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,7 @@ jobs: - { php: '8.1', packages: 'aws/aws-sdk-php:^3.158', phpspec: 'spec/Gaufrette/Adapter/AwsS3Spec.php' } - { php: '8.1', packages: 'google/apiclient:^2.12', phpspec: 'spec/Gaufrette/Adapter/GoogleCloudStorageSpec.php' } - { php: '8.1', packages: 'doctrine/dbal:^2.3', phpspec: 'spec/Gaufrette/Adapter/DoctrineDbalSpec.php' } + - { php: '8.1', packages: 'doctrine/dbal:^3.4', phpspec: 'spec/Gaufrette/Adapter/DoctrineDbalSpec.php' } - { php: '8.1', packages: 'league/flysystem:^1.0', phpspec: 'spec/Gaufrette/Adapter/FlysystemSpec.php' } - { php: '8.1', packages: 'microsoft/azure-storage-blob:^1.0', phpspec: 'spec/Gaufrette/Adapter/AzureBlobStore' } - { php: '8.1', packages: 'mongodb/mongodb:^1.1', phpspec: 'spec/Gaufrette/Adapter/GridFSSpec.php' } diff --git a/Makefile b/Makefile index fc29c04ee..b4942d832 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ docker.all-deps: docker.deps ## Install dependencies docker/run-task php${PHP_VERSION} composer require --no-update \ aws/aws-sdk-php:^3.158 \ google/apiclient:^2.12 \ - doctrine/dbal:^2.3 \ + doctrine/dbal:^3.4 \ league/flysystem:^1.0 \ microsoft/azure-storage-blob:^1.0 \ phpseclib/phpseclib:^2.0 \ @@ -67,7 +67,7 @@ require-all-legacy: # kept for compatibility with the old CI config, to be remov composer require --no-update \ aws/aws-sdk-php:^3.158 \ google/apiclient:^2.12 \ - doctrine/dbal:^2.3 \ + doctrine/dbal:^3.4 \ league/flysystem:^1.0 \ microsoft/azure-storage-blob:^1.0 \ phpseclib/phpseclib:^2.0 \ diff --git a/spec/Gaufrette/Adapter/DoctrineDbalSpec.php b/spec/Gaufrette/Adapter/DoctrineDbalSpec.php index 091ee157f..4e723e148 100644 --- a/spec/Gaufrette/Adapter/DoctrineDbalSpec.php +++ b/spec/Gaufrette/Adapter/DoctrineDbalSpec.php @@ -6,7 +6,7 @@ require_once 'functions.php'; use Doctrine\DBAL\Connection; -use Doctrine\DBAL\Driver\Statement; +use Doctrine\DBAL\Result; use PhpSpec\ObjectBehavior; use Prophecy\Argument; @@ -46,13 +46,18 @@ function it_checks_if_file_exists(Connection $connection) return sprintf('"%s"', $argument[0]); }); + $method = 'fetchOne'; // dbal 3.x + if (!method_exists(Connection::class, 'fetchAllAssociative')) { + $method = 'fetchColumn'; // BC layer for dbal 2.x + } + $connection - ->fetchColumn('SELECT COUNT("key") FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) + ->$method('SELECT COUNT("key") FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) ->willReturn(12); $this->exists('filename')->shouldReturn(true); $connection - ->fetchColumn('SELECT COUNT("key") FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) + ->$method('SELECT COUNT("key") FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) ->willReturn(0); $this->exists('filename')->shouldReturn(false); } @@ -67,8 +72,14 @@ function it_writes_to_new_file(Connection $connection) ->will(function ($argument) { return sprintf('"%s"', $argument[0]); }); + + $method = 'fetchOne'; // dbal 3.x + if (!method_exists(Connection::class, 'fetchAllAssociative')) { + $method = 'fetchColumn'; // BC layer for dbal 2.x + } + $connection - ->fetchColumn('SELECT COUNT("key") FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) + ->$method('SELECT COUNT("key") FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) ->willReturn(false); $connection ->insert( @@ -90,13 +101,18 @@ function it_writes_to_new_file(Connection $connection) */ function it_write_file(Connection $connection) { + $method = 'fetchOne'; // dbal 3.x + if (!method_exists(Connection::class, 'fetchAllAssociative')) { + $method = 'fetchColumn'; // BC layer for dbal 2.x + } + $connection ->quoteIdentifier(Argument::any()) ->will(function ($argument) { return sprintf('"%s"', $argument[0]); }); $connection - ->fetchColumn('SELECT COUNT("key") FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) + ->$method('SELECT COUNT("key") FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) ->willReturn(true); $connection ->update( @@ -120,13 +136,18 @@ function it_write_file(Connection $connection) */ function it_reads_file(Connection $connection) { + $method = 'fetchOne'; // dbal 3.x + if (!method_exists(Connection::class, 'fetchAllAssociative')) { + $method = 'fetchColumn'; // BC layer for dbal 2.x + } + $connection ->quoteIdentifier(Argument::any()) ->will(function ($argument) { return sprintf('"%s"', $argument[0]); }); $connection - ->fetchColumn('SELECT "content" FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) + ->$method('SELECT "content" FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) ->willReturn('some content'); $this->read('filename')->shouldReturn('some content'); @@ -137,13 +158,18 @@ function it_reads_file(Connection $connection) */ function it_calculates_checksum(Connection $connection) { + $method = 'fetchOne'; // dbal 3.x + if (!method_exists(Connection::class, 'fetchAllAssociative')) { + $method = 'fetchColumn'; // BC layer for dbal 2.x + } + $connection ->quoteIdentifier(Argument::any()) ->will(function ($argument) { return sprintf('"%s"', $argument[0]); }); $connection - ->fetchColumn('SELECT "checksum" FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) + ->$method('SELECT "checksum" FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) ->willReturn(1234); $this->checksum('filename')->shouldReturn(1234); @@ -154,13 +180,18 @@ function it_calculates_checksum(Connection $connection) */ function it_gets_mtime(Connection $connection) { + $method = 'fetchOne'; // dbal 3.x + if (!method_exists(Connection::class, 'fetchAllAssociative')) { + $method = 'fetchColumn'; // BC layer for dbal 2.x + } + $connection ->quoteIdentifier(Argument::any()) ->will(function ($argument) { return sprintf('"%s"', $argument[0]); }); $connection - ->fetchColumn('SELECT "mtime" FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) + ->$method('SELECT "mtime" FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) ->willReturn(1234); $this->mtime('filename')->shouldReturn(1234); @@ -194,11 +225,19 @@ function it_renames_file(Connection $connection) /** * @param \Doctrine\DBAL\Connection $connection - * @param \Doctrine\DBAL\Statement $stmt */ - function it_get_keys(Connection $connection, Statement $stmt) + function it_get_keys(Connection $connection, $result) { - $stmt->fetchAll(\PDO::FETCH_COLUMN)->willReturn(['filename', 'filename1', 'filename2']); + if (class_exists(Result::class)) { + // dbal 3.x + $result->beADoubleOf(Result::class); + $result->fetchFirstColumn()->willReturn(['filename', 'filename1', 'filename2']); + } else { + // BC layer for dbal 2.x + $result->beADoubleOf(\Doctrine\DBAL\Statement::class); + $result->fetchAll(\PDO::FETCH_COLUMN)->willReturn(['filename', 'filename1', 'filename2']); + } + $connection ->quoteIdentifier(Argument::any()) ->will(function ($argument) { @@ -206,7 +245,7 @@ function it_get_keys(Connection $connection, Statement $stmt) }); $connection ->executeQuery('SELECT "key" FROM "someTableName"') - ->willReturn($stmt); + ->willReturn($result); $this->keys()->shouldReturn(['filename', 'filename1', 'filename2']); } diff --git a/src/Gaufrette/Adapter/DoctrineDbal.php b/src/Gaufrette/Adapter/DoctrineDbal.php index 337b1de60..77066d2fb 100644 --- a/src/Gaufrette/Adapter/DoctrineDbal.php +++ b/src/Gaufrette/Adapter/DoctrineDbal.php @@ -2,6 +2,7 @@ namespace Gaufrette\Adapter; +use Doctrine\DBAL\Result; use Gaufrette\Adapter; use Gaufrette\Util; use Doctrine\DBAL\Connection; @@ -52,6 +53,12 @@ public function keys() $this->getQuotedTable() )); + if (class_exists(Result::class)) { + // dbal 3.x + return $stmt->fetchFirstColumn(); + } + + // BC layer for dbal 2.x return $stmt->fetchAll(\PDO::FETCH_COLUMN); } @@ -88,7 +95,12 @@ public function checksum($key) */ public function exists($key) { - return (boolean) $this->connection->fetchColumn( + $method = 'fetchOne'; // dbal 3.x + if (!method_exists(Connection::class, $method)) { + $method = 'fetchColumn'; // BC layer for dbal 2.x + } + + return (boolean) $this->connection->$method( sprintf( 'SELECT COUNT(%s) FROM %s WHERE %s = :key', $this->getQuotedColumn('key'), @@ -153,7 +165,12 @@ public function isDirectory($key) private function getColumnValue($key, $column) { - $value = $this->connection->fetchColumn( + $method = 'fetchOne'; // dbal 3.x + if (!method_exists(Connection::class, $method)) { + $method = 'fetchColumn'; // BC layer for dbal 2.x + } + + $value = $this->connection->$method( sprintf( 'SELECT %s FROM %s WHERE %s = :key', $this->getQuotedColumn($column), @@ -173,7 +190,12 @@ public function listKeys($prefix = '') { $prefix = trim($prefix); - $keys = $this->connection->fetchAll( + $method = 'fetchAllAssociative'; // dbal 3.x + if (!method_exists(Connection::class, 'fetchAllAssociative')) { + $method = 'fetchAll'; // BC layer for dbal 2.x + } + + $keys = $this->connection->$method( sprintf( 'SELECT %s AS _key FROM %s WHERE %s LIKE :pattern', $this->getQuotedColumn('key'), diff --git a/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php b/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php index df53f63c4..aa9d8a39f 100644 --- a/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php +++ b/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php @@ -29,7 +29,14 @@ protected function setUp(): void $schema = $this->connection->getSchemaManager()->createSchema(); $table = $schema->createTable('gaufrette'); - $table->addColumn('key', 'string', ['unique' => true]); + $column = $table->addColumn('key', 'string'); + if (method_exists($column, 'setPlatformOption')) { + // dbal 3.4+ + $column->setPlatformOption('unique', true); + } else { + // BC layer dbal 2.x + $column->setUnique(true); + } $table->addColumn('content', 'blob'); $table->addColumn('mtime', 'integer'); $table->addColumn('checksum', 'string', ['length' => 32]); From e989211caad6059d1558ae124d08f0d84f8b1240 Mon Sep 17 00:00:00 2001 From: Maxime Veber Date: Fri, 14 Oct 2022 17:33:54 +0200 Subject: [PATCH 420/426] :memo: update changelog with latests changes (#682) --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 14b95763e..3f8e7a06c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ v0.11.0 ## Added - Google Cloud Storage: options to automatically create bucket if not exists +- Support for PHP 8.0+ +- Support for doctrine/dbal 3.x ## Updated From d193cb7026587ee9e25fd1a597ccc0dbb59a2123 Mon Sep 17 00:00:00 2001 From: Maxime Veber Date: Fri, 14 Oct 2022 17:34:04 +0200 Subject: [PATCH 421/426] :hammer: update script to run all tests (#680) --- bin/alltests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bin/alltests.sh b/bin/alltests.sh index 48d8103a2..0643cdce7 100755 --- a/bin/alltests.sh +++ b/bin/alltests.sh @@ -11,4 +11,4 @@ source .env # load dev parameters make require-all composer install -vendor/bin/phpunit tests/Gaufrette/Functional/Adapter/AsyncAwsS3Test.php +vendor/bin/phpunit From 9bc0118ad1db27f55e6ec930bf9daac2fbcb801c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Robert=20Boche=C5=84ski?= Date: Thu, 3 Nov 2022 18:16:20 +0100 Subject: [PATCH 422/426] Fixed azure blob write method (#683) --- src/Gaufrette/Adapter/AzureBlobStorage.php | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Gaufrette/Adapter/AzureBlobStorage.php b/src/Gaufrette/Adapter/AzureBlobStorage.php index 221df9eaa..c153f9c01 100644 --- a/src/Gaufrette/Adapter/AzureBlobStorage.php +++ b/src/Gaufrette/Adapter/AzureBlobStorage.php @@ -198,6 +198,11 @@ public function write($key, $content) $options->setContentType($contentType); } + $size = is_resource($content) + ? Util\Size::fromResource($content) + : Util\Size::fromContent($content) + ; + try { if ($this->multiContainerMode) { $this->createContainer($containerName); @@ -209,11 +214,8 @@ public function write($key, $content) return false; } - if (is_resource($content)) { - return Util\Size::fromResource($content); - } - return Util\Size::fromContent($content); + return $size; } /** From 1961e117faba1bca8367cb8e3e60c61633dbeb9c Mon Sep 17 00:00:00 2001 From: William Desportes Date: Thu, 3 Nov 2022 18:17:06 +0100 Subject: [PATCH 423/426] Fix #649 - Remove useless files from users and servers (#679) --- .gitattributes | 13 +++++++++++++ composer.json | 13 +++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..af577adcc --- /dev/null +++ b/.gitattributes @@ -0,0 +1,13 @@ +/.gitattributes export-ignore +/.gitignore export-ignore +/.github export-ignore +/.php-cs-fixer.dist.php export-ignore +/phpstan.neon.dist export-ignore +/phpstan-baseline.neon export-ignore +/.editorconfig export-ignore +/.travis.yml export-ignore +/appveyor.yml export-ignore +/psalm.baseline.xml export-ignore +/psalm.xml export-ignore +/bin export-ignore +/doc export-ignore diff --git a/composer.json b/composer.json index f902eb484..dbe5941a8 100644 --- a/composer.json +++ b/composer.json @@ -42,5 +42,18 @@ "branch-alias": { "dev-master": "0.10.x-dev" } + }, + "archive": { + "exclude": [ + "/tests", + "/phpunit.xml.dist", + "/.env.dist", + "/spec", + "/Makefile", + "/phpspec.yml", + "/docker-compose.yml", + "/docker", + "/.dockerignore" + ] } } From 3cc396cae0f7c7d3f965e6b69d8fc546ffa0f94c Mon Sep 17 00:00:00 2001 From: Maxime Veber Date: Thu, 3 Nov 2022 18:26:17 +0100 Subject: [PATCH 424/426] Prepare release 0.11.1 --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f8e7a06c..7a4cbec4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +v0.11.1 +======= + +## Added + +- gitattributes file to avoid download too much files on install with composer in #679 + +## Fixed + +- Azure integraton with stream was not working properly, it has been fixed in #683 v0.11.0 ======= From dcfb86a1e37b197af84eb1da4fa04218c430a34c Mon Sep 17 00:00:00 2001 From: Antoine Lamirault Date: Thu, 19 Oct 2023 16:18:36 +0200 Subject: [PATCH 425/426] Add tests and docker folders in .gitattributes (#704) --- .gitattributes | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitattributes b/.gitattributes index af577adcc..b5b38e2e6 100644 --- a/.gitattributes +++ b/.gitattributes @@ -11,3 +11,5 @@ /psalm.xml export-ignore /bin export-ignore /doc export-ignore +/docker export-ignore +/tests export-ignore From ceb9f239f9c22f452bc09814cc4ad0700b26078e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin?= Date: Thu, 7 Dec 2023 16:19:07 +0100 Subject: [PATCH 426/426] fix: make commande to remove phpspec adapter (#707) * fix: make commande to remove phpspec adapter * Update Makefile Co-authored-by: Pierre PLAZANET --------- Co-authored-by: Pierre PLAZANET --- Makefile | 7 ++++--- ...lobStorage.php => AzureBlobStorageSpec.php} | 2 +- spec/Gaufrette/Adapter/ZipSpec.php | 18 ------------------ 3 files changed, 5 insertions(+), 22 deletions(-) rename spec/Gaufrette/Adapter/{AzureBlobStorage.php => AzureBlobStorageSpec.php} (99%) delete mode 100644 spec/Gaufrette/Adapter/ZipSpec.php diff --git a/Makefile b/Makefile index b4942d832..f18c653d9 100644 --- a/Makefile +++ b/Makefile @@ -53,13 +53,14 @@ docker.php-cs-fix: ## Run CS fixer # ------- # -remove-phpspec: ## Remove adapter specs (allows you to run test suite without adapters deps) +remove-phpspec: ## Remove adapter specs (allows you to run test suite without adapters that needs deps) rm spec/Gaufrette/Adapter/AsyncAwsS3Spec.php rm spec/Gaufrette/Adapter/AwsS3Spec.php - rm spec/Gaufrette/Adapter/GoogleCloudStorageSpec.php + rm spec/Gaufrette/Adapter/AzureBlobStorageSpec.php + rm -r spec/Gaufrette/Adapter/AzureBlobStorage rm spec/Gaufrette/Adapter/DoctrineDbalSpec.php rm spec/Gaufrette/Adapter/FlysystemSpec.php - rm -r spec/Gaufrette/Adapter/AzureBlobStorage + rm spec/Gaufrette/Adapter/GoogleCloudStorageSpec.php rm spec/Gaufrette/Adapter/GridFSSpec.php rm spec/Gaufrette/Adapter/PhpseclibSftpSpec.php diff --git a/spec/Gaufrette/Adapter/AzureBlobStorage.php b/spec/Gaufrette/Adapter/AzureBlobStorageSpec.php similarity index 99% rename from spec/Gaufrette/Adapter/AzureBlobStorage.php rename to spec/Gaufrette/Adapter/AzureBlobStorageSpec.php index c6cee8572..977cdcf56 100644 --- a/spec/Gaufrette/Adapter/AzureBlobStorage.php +++ b/spec/Gaufrette/Adapter/AzureBlobStorageSpec.php @@ -7,7 +7,7 @@ use WindowsAzure\Blob\Models\Blob; use WindowsAzure\Common\ServiceException; -class AzureBlobStorage extends ObjectBehavior +class AzureBlobStorageSpec extends ObjectBehavior { /** * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory diff --git a/spec/Gaufrette/Adapter/ZipSpec.php b/spec/Gaufrette/Adapter/ZipSpec.php deleted file mode 100644 index 76bb1c13a..000000000 --- a/spec/Gaufrette/Adapter/ZipSpec.php +++ /dev/null @@ -1,18 +0,0 @@ -beConstructedWith('somefile'); - } - - function it_is_adapter() - { - $this->shouldHaveType('Gaufrette\Adapter'); - } -}