diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..5a31182db --- /dev/null +++ b/.dockerignore @@ -0,0 +1,2 @@ +# ignore all by default +* diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 000000000..299bd7006 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,18 @@ +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 + +[*.yml] +indent_size = 2 + +[Makefile] +indent_style = tab diff --git a/.env.dist b/.env.dist new file mode 100644 index 000000000..d3cc06560 --- /dev/null +++ b/.env.dist @@ -0,0 +1,22 @@ +AWS_KEY= +AWS_SECRET= +AWS_BUCKET= + +AZURE_ACCOUNT= +AZURE_KEY= +AZURE_CONTAINER= + +FTP_HOST=ftp +FTP_PORT=21 +FTP_USER=gaufrette +FTP_PASSWORD=gaufrette +FTP_BASE_DIR=/gaufrette + +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 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..b5b38e2e6 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,15 @@ +/.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 +/docker export-ignore +/tests export-ignore 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/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..691079722 --- /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.4', '8.0', '8.1'] + + steps: + - name: Set up PHP + uses: shivammathur/setup-php@v2 + 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: '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: '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' } + - { php: '8.1', packages: 'phpseclib/phpseclib:^2.0', phpspec: 'spec/Gaufrette/Adapter/PhpseclibSftpSpec.php' } + + steps: + - name: Set up PHP + uses: shivammathur/setup-php@v2 + 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 new file mode 100644 index 000000000..e3f643a42 --- /dev/null +++ b/.github/workflows/static.yml @@ -0,0 +1,80 @@ +on: [push, pull_request] +name: Static analysis + +jobs: + phpstan: + name: PHPStan + runs-on: ubuntu-latest + + steps: + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.1' + 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:1.8.8 + composer update --no-interaction --prefer-dist + + - name: PHPStan + 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: + php-version: '8.1' + extensions: mongo, mbstring, fileinfo + tools: composer:v2 + + - name: Checkout code + uses: actions/checkout@v2 + + - name: Download dependencies + run: | + make require-all + composer update --no-interaction --prefer-dist + + - name: PHP-CS-Fixer + run: | + ./vendor/bin/php-cs-fixer --version + ./vendor/bin/php-cs-fixer fix --diff --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 + run: | + ./vendor/bin/psalm.phar --version + ./vendor/bin/psalm.phar --output-format=github --no-progress --show-info=false --stats diff --git a/.gitignore b/.gitignore index a91fe0141..0d7d1c4e4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,13 @@ -vendor/ +bin/* +!bin/tests +!bin/tests-all +composer.lock +doc/.couscous +.env +.php_cs.cache +.php-cs-fixer.cache phpunit.xml tests/adapters/* !tests/adapters/*.dist -bin/* -!bin/configure_test_env.sh +vendor/ +tests/Gaufrette/Functional/adapters/*.php diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php new file mode 100644 index 000000000..d68945fde --- /dev/null +++ b/.php-cs-fixer.dist.php @@ -0,0 +1,41 @@ +setFinder(PhpCsFixer\Finder::create() + ->exclude('vendor') + ->in(__DIR__) + ) + ->setRules([ + '@PSR1' => true, + '@PSR2' => true, + 'array_syntax' => [ 'syntax' => 'short' ], + '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' => ['on_multiline' => 'ensure_fully_multiline'], + 'no_blank_lines_after_class_opening' => true, + 'no_blank_lines_after_phpdoc' => true, + 'no_break_comment' => false, + 'no_extra_blank_lines' => 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, + 'single_quote' => true, + 'ternary_to_null_coalescing' => true, + 'trailing_comma_in_multiline' => ['elements' => ['arrays']], + 'trim_array_spaces' => true, + ]) + ->registerCustomFixers(new PedroTroller\CS\Fixer\Fixers()) +; diff --git a/.travis.yml b/.travis.yml index 749fe81e2..70deaa79e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,13 +1,46 @@ -language: php - -php: - - 5.3 - - 5.4 - -before_script: - - ./bin/configure_test_env.sh - - composer self-update && composer install --dev --prefer-source - -script: - - php bin/phpspec run -fpretty --verbose - - bin/phpunit +language: php + +sudo: required + +cache: + directories: + - $HOME/.composer/cache/files + +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=gaufrette + FTP_HOST=localhost + FTP_PORT=21 + FTP_USER=gaufrette + FTP_PASSWORD=gaufrette + FTP_BASE_DIR=/gaufrette/ + +matrix: + include: + - 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: + - make require-all-legacy + - composer update --prefer-dist --no-progress --no-suggest --ansi + +script: + - php vendor/bin/phpunit --verbose diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..7a4cbec4a --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,210 @@ +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 +======= + +## Added + +- Google Cloud Storage: options to automatically create bucket if not exists +- Support for PHP 8.0+ +- Support for doctrine/dbal 3.x + +## Updated + +- Azure seems to have break compatibility, phpstan detected it and it's now fixed (#674) + +## Removed + +- Adapter for OpenCloud (#669) + +v0.10.0 +======= + +## Added + +- Support for AsyncAws + +## 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 +====== + +## 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 +====== + +## Changes + +- Change Filesystem to FilesystemInterface in File's constructor (#608) + +Thank you @athlan for your contribution ! + +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) + +Thank you @athos7933, @bsperduto and @nicolasmure for your contributions ! + +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 +==== + +## 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 +==== + +## 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 +==== + +## 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 +==== + +* 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. diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..f18c653d9 --- /dev/null +++ b/Makefile @@ -0,0 +1,105 @@ +.DEFAULT_GOAL := help + +PHP_VERSION ?= 7.2 +export ROOT_DIR=${PWD} + +# +### DOCKER +# -------- +# + +.PHONY: dev +docker.dev: ## Prepare the env file before running docker + cp .env.dist .env + +.PHONY: build +docker.build: ## Build the PHP docker image + docker-compose build php${PHP_VERSION} + +.PHONY: 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 \ + google/apiclient:^2.12 \ + doctrine/dbal:^3.4 \ + league/flysystem:^1.0 \ + microsoft/azure-storage-blob:^1.0 \ + phpseclib/phpseclib:^2.0 \ + mongodb/mongodb:^1.1 \ + async-aws/simple-s3:^0.1.1 + +.PHONY: tests +docker.tests: ## Run tests + docker/run-task php${PHP_VERSION} bin/tests + +.PHONY: 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 \ + --show-progress=none \ + --verbose + +.PHONY: php-cs-fix +docker.php-cs-fix: ## Run CS fixer + docker/run-task php${PHP_VERSION} vendor/bin/php-cs-fixer fix + +# +### LOCAL TASKS +# ------- +# + +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/AzureBlobStorageSpec.php + rm -r spec/Gaufrette/Adapter/AzureBlobStorage + rm spec/Gaufrette/Adapter/DoctrineDbalSpec.php + rm spec/Gaufrette/Adapter/FlysystemSpec.php + rm spec/Gaufrette/Adapter/GoogleCloudStorageSpec.php + rm spec/Gaufrette/Adapter/GridFSSpec.php + rm spec/Gaufrette/Adapter/PhpseclibSftpSpec.php + +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 \ + google/apiclient:^2.12 \ + doctrine/dbal:^3.4 \ + league/flysystem:^1.0 \ + microsoft/azure-storage-blob:^1.0 \ + phpseclib/phpseclib:^2.0 \ + mongodb/mongodb:^1.1 + + +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 + +test.phpstan: ## Run phpstan analysis + php vendor/bin/phpstan analyze --memory-limit 1G + +# +### 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.markdown b/README.markdown deleted file mode 100644 index 340764a64..000000000 --- a/README.markdown +++ /dev/null @@ -1,268 +0,0 @@ -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. - -[![Build Status](https://secure.travis-ci.org/KnpLabs/Gaufrette.png)](http://travis-ci.org/KnpLabs/Gaufrette) - -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. - -Try it! -------- - -### 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). - -```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 username', - 'password' => 'your Keystone password', - 'tenantName' => 'your tenant (project) name' - )); - -$objectStore = $connection->ObjectStore('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' - )); - -``` - -Using FTP adapters ---------------- - -Some FTP servers need valid configuration so Gaufrette can working 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/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'; -``` - -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% - - - - - -``` - -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 ---------------- - -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(); - -echo file_get_contents('gaufrette://foo/hello.txt'); // Says "Hello World!" -``` - -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: - - $ cd gaufrette - $ php composer.phar install --dev - $ sh bin/configure_test_env.sh - -It will avoid skip a lot of tests. - -### Launch the Test Suite - -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? diff --git a/README.md b/README.md new file mode 100644 index 000000000..a61517175 --- /dev/null +++ b/README.md @@ -0,0 +1,124 @@ +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) +[![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? +------------------ + +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 media will be stored and how. + +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 +solution. + +### Documentation + +Read the official [Gaufrette documentation](http://knplabs.github.io/Gaufrette/). + +### Metapackages for adapters + +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. + +### Symfony integration + +Symfony integration is available through [KnpLabs/KnpGaufretteBundle](https://github.com/KnpLabs/KnpGaufretteBundle). + +### Maintainers + +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 | +|--------------------|-----------------------------| +| AsyncAws S3 | @Nyholm | +| 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. + +### Development + +Requires : + * docker-ce + * docker-compose + +1) Create `.env` file : +```bash +$ make docker.dev +``` +and configure it as you want. + +2) Build the PHP docker image : +```bash +$ make docker.build +``` + +3) Install dependencies : +```bash +$ make docker.all-deps +``` + +4) Run tests : +```bash +$ make docker.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 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 +one version to an other. To do so, run : +```bash +$ 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. diff --git a/appveyor.yml b/appveyor.yml new file mode 100644 index 000000000..422a4597f --- /dev/null +++ b/appveyor.yml @@ -0,0 +1,69 @@ +build: false +platform: 'x86' +clone_folder: C:\projects\gaufrette +branches: + except: + - gh-pages + +cache: + - '%LOCALAPPDATA%\Composer\files' + - c:\php -> appveyor.yml + +environment: + AWS_KEY: + secure: nMVfvhojgVc9VBlrYXeddaYIQHlVAlupWmxNjXMlsB0= + AWS_SECRET: + secure: MiAf2q7mnqSIZSoChd9v3pwUrXHeAEpZP8Zc0E0qjiOBXPvplzJVtrkWRwTNlQl3 + AWS_BUCKET: gaufretteci + RACKSPACE_USER: + secure: aEIZdm+IXe9dGLOnSmgu8A== + RACKSPACE_APIKEY: + secure: CqBlB4EVIM2FMaYxBdplTaQzw8JgKElxYvKYpXUXPHpOdwJcnYy0gk2KdjF4fXDM + RACKSPACE_CONTAINER: gaufretteci + AZURE_ACCOUNT: + secure: s6JCZ8ztVGvVYprceONZOg== + 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:\php;%PATH% + - SET PHP=1 + +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-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.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 + - 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 + - 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 + - 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: + - cd C:\projects\gaufrette + - vendor\bin\phpunit --verbose -c phpunit.xml.dist diff --git a/bin/alltests.sh b/bin/alltests.sh new file mode 100755 index 000000000..0643cdce7 --- /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 diff --git a/bin/configure_test_env.sh b/bin/configure_test_env.sh deleted file mode 100755 index ee582373c..000000000 --- a/bin/configure_test_env.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/bin/bash - -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 - -cp tests/Gaufrette/Functional/adapters/DoctrineDbal.php.dist tests/Gaufrette/Functional/adapters/DoctrineDbal.php -f diff --git a/bin/tests b/bin/tests new file mode 100755 index 000000000..953e4fd07 --- /dev/null +++ b/bin/tests @@ -0,0 +1,9 @@ +#!/bin/sh + +set -o nounset +set -o xtrace + +DIR=$(dirname $0)/.. + +${DIR}/vendor/bin/phpunit tests +${DIR}/vendor/bin/phpspec run --format=pretty --no-code-generation diff --git a/composer.json b/composer.json index 6e46d0264..dbe5941a8 100644 --- a/composer.json +++ b/composer.json @@ -1,9 +1,8 @@ { "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"], - "minimum-stability": "dev", "homepage": "http://knplabs.com", "license": "MIT", "authors": [ @@ -16,66 +15,45 @@ "homepage": "http://github.com/knplabs/Gaufrette/contributors" } ], - "repositories": [ - { - "type": "package", - "package": { - "name": "rackspace/php-cloudfiles", - "version": "master", - "source": { - "url": "git://github.com/rackspace/php-cloudfiles.git", - "type": "git", - "reference": "master" - }, - "dist": { - "url": "https://github.com/rackspace/php-cloudfiles/zipball/master", - "type": "zip" - }, - "autoload": { - "files": ["cloudfiles.php"] - } - } - }, - { - "type": "vcs", - "url": "https://github.com/Herzult/php-ssh" - } - ], "require": { - "php": ">=5.3.2" + "php": "^7.4 || ^8.0" + }, + "conflict": { + "microsoft/windowsazure": "<0.4.3" }, "require-dev": { - "amazonwebservices/aws-sdk-for-php": "1.5.*", - "rackspace/php-opencloud" : "dev-master", - "phpspec/phpspec2": "dev-master", - "rackspace/php-cloudfiles": "*", - "doctrine/dbal": ">=2.3", - "dropbox-php/dropbox-php": "*", - "herzult/php-ssh": "*", - "phpunit/phpunit": "3.7.*" + "phpspec/phpspec": "^7.0", + "phpunit/phpunit": "~8.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": "*", - "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", - "doctrine/dbal": "to use the Doctrine DBAL adapter", - "ext-zip": "to use the Zip adapter", - "ext-apc": "to use the APC adapter", - "ext-curl": "*", - "ext-mbstring": "*", - "ext-mongo": "*", - "ext-fileinfo": "*" + "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": { "psr-0": { "Gaufrette": "src/" } }, + "autoload-dev": { + "psr-0": { "Gaufrette\\Functional": "tests/" } + }, "extra": { "branch-alias": { - "dev-master": "0.2.x-dev" + "dev-master": "0.10.x-dev" } }, - "config": { - "bin-dir": "bin" + "archive": { + "exclude": [ + "/tests", + "/phpunit.xml.dist", + "/.env.dist", + "/spec", + "/Makefile", + "/phpspec.yml", + "/docker-compose.yml", + "/docker", + "/.dockerignore" + ] } } diff --git a/composer.lock b/composer.lock deleted file mode 100644 index 480de3e9b..000000000 --- a/composer.lock +++ /dev/null @@ -1,1322 +0,0 @@ -{ - "hash": "43a051992b6dbce50d171784fadcc0d1", - "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": "1.5.17.1" - }, - "dist": { - "type": "zip", - "url": "https://github.com/amazonwebservices/aws-sdk-for-php/archive/1.5.17.1.zip", - "reference": "1.5.17.1", - "shasum": "" - }, - "require": { - "php": ">=5.2.0" - }, - "time": "2012-11-26 23:59:55", - "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" - ] - }, - { - "name": "doctrine/annotations", - "version": "v1.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/annotations.git", - "reference": "v1.0" - }, - "dist": { - "type": "zip", - "url": "https://github.com/doctrine/annotations/archive/v1.0.zip", - "reference": "v1.0", - "shasum": "" - }, - "require": { - "doctrine/lexer": "1.*", - "php": ">=5.3.2" - }, - "require-dev": { - "doctrine/cache": "1.*" - }, - "time": "2013-01-12 19:23:32", - "type": "library", - "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/" - }, - { - "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": "https://github.com/schmittjoh", - "role": "Developer of wrapped JMSSerializerBundle" - } - ], - "description": "Docblock Annotations Parser", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "annotations", - "docblock", - "parser" - ] - }, - { - "name": "doctrine/cache", - "version": "v1.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/cache.git", - "reference": "v1.0" - }, - "dist": { - "type": "zip", - "url": "https://github.com/doctrine/cache/archive/v1.0.zip", - "reference": "v1.0", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "time": "2013-01-10 22:43:46", - "type": "library", - "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/" - }, - { - "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": "https://github.com/schmittjoh", - "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" - ] - }, - { - "name": "doctrine/collections", - "version": "v1.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/collections.git", - "reference": "v1.0" - }, - "dist": { - "type": "zip", - "url": "https://github.com/doctrine/collections/archive/v1.0.zip", - "reference": "v1.0", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "time": "2013-01-12 16:36:50", - "type": "library", - "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/" - }, - { - "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": "https://github.com/schmittjoh", - "role": "Developer of wrapped JMSSerializerBundle" - } - ], - "description": "Collections Abstraction library", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "array", - "collections", - "iterator" - ] - }, - { - "name": "doctrine/common", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/doctrine/common", - "reference": "53859ae1c84ccf1a5aa58c8379c69cd9adedf03a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/common/zipball/53859ae1c84ccf1a5aa58c8379c69cd9adedf03a", - "reference": "53859ae1c84ccf1a5aa58c8379c69cd9adedf03a", - "shasum": "" - }, - "require": { - "doctrine/annotations": "1.*", - "doctrine/cache": "1.*", - "doctrine/collections": "1.*", - "doctrine/inflector": "1.*", - "doctrine/lexer": "1.*", - "php": ">=5.3.2" - }, - "time": "2013-01-29 12:48:56", - "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/" - }, - { - "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": "https://github.com/schmittjoh", - "role": "Developer of wrapped JMSSerializerBundle" - } - ], - "description": "Common Library for Doctrine projects", - "homepage": "http://www.doctrine-project.org", - "keywords": [ - "annotations", - "collections", - "eventmanager", - "persistence", - "spl" - ] - }, - { - "name": "doctrine/dbal", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/doctrine/dbal", - "reference": "eb6ee9a86421ba534d7c5514b190d1d06b30d4b1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/doctrine/dbal/zipball/eb6ee9a86421ba534d7c5514b190d1d06b30d4b1", - "reference": "eb6ee9a86421ba534d7c5514b190d1d06b30d4b1", - "shasum": "" - }, - "require": { - "doctrine/common": "2.4.x-dev", - "php": ">=5.3.2" - }, - "require-dev": { - "symfony/console": "2.*" - }, - "suggest": { - "symfony/console": "For helpful console commands such as SQL execution and import of files." - }, - "time": "2013-02-09 23:28:29", - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.4.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/" - }, - { - "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" - ] - }, - { - "name": "doctrine/inflector", - "version": "v1.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/inflector.git", - "reference": "v1.0" - }, - "dist": { - "type": "zip", - "url": "https://github.com/doctrine/inflector/archive/v1.0.zip", - "reference": "v1.0", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "time": "2013-01-10 21:49:15", - "type": "library", - "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/" - }, - { - "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": "https://github.com/schmittjoh", - "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", - "pluarlize", - "singuarlize", - "string" - ] - }, - { - "name": "doctrine/lexer", - "version": "v1.0", - "source": { - "type": "git", - "url": "https://github.com/doctrine/lexer.git", - "reference": "v1.0" - }, - "dist": { - "type": "zip", - "url": "https://github.com/doctrine/lexer/archive/v1.0.zip", - "reference": "v1.0", - "shasum": "" - }, - "require": { - "php": ">=5.3.2" - }, - "time": "2013-01-12 18:59:04", - "type": "library", - "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": "https://github.com/schmittjoh", - "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" - ] - }, - { - "name": "dropbox-php/dropbox-php", - "version": "dev-master", - "source": { - "type": "git", - "url": "http://github.com/Dropbox-PHP/dropbox-php", - "reference": "3dbc9912ce2846d46b3b0eaf72c3bd9f6a3efef0" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Dropbox-PHP/dropbox-php/zipball/3dbc9912ce2846d46b3b0eaf72c3bd9f6a3efef0", - "reference": "3dbc9912ce2846d46b3b0eaf72c3bd9f6a3efef0", - "shasum": "" - }, - "require": { - "php": ">=5.3.0" - }, - "suggest": { - "http_oauth": ">=1.1.18", - "oauth": "*" - }, - "time": "2013-01-19 02:59:05", - "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" - ] - }, - { - "name": "herzult/php-ssh", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/Herzult/php-ssh.git", - "reference": "49bce71cb7f3d51aed3009d8f301f1593308cc0a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/Herzult/php-ssh/zipball/49bce71cb7f3d51aed3009d8f301f1593308cc0a", - "reference": "49bce71cb7f3d51aed3009d8f301f1593308cc0a", - "shasum": "" - }, - "require": { - "ext-ssh2": "*", - "php": ">=5.3.2" - }, - "require-dev": { - "phpunit/phpunit": "3.7.*" - }, - "time": "2013-01-31 21:44:04", - "type": "library", - "autoload": { - "psr-0": { - "Ssh": "src/" - } - }, - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Antoine Hérault", - "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" - ], - "support": { - "source": "https://github.com/Herzult/php-ssh/tree/master", - "issues": "https://github.com/Herzult/php-ssh/issues" - } - }, - { - "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" - }, - "time": "2012-01-24 12:22:39", - "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" - ] - }, - { - "name": "phpspec/php-diff", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/phpspec/php-diff.git", - "reference": "8d82ac415225fac373a4073ba14b1fe286aa2312" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/php-diff/zipball/8d82ac415225fac373a4073ba14b1fe286aa2312", - "reference": "8d82ac415225fac373a4073ba14b1fe286aa2312", - "shasum": "" - }, - "time": "2012-11-08 08:55:45", - "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)." - }, - { - "name": "phpspec/phpspec2", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/phpspec/phpspec2.git", - "reference": "c6140c098d79a4ed10eba2411817a32f72d4a367" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/phpspec/phpspec2/zipball/c6140c098d79a4ed10eba2411817a32f72d4a367", - "reference": "c6140c098d79a4ed10eba2411817a32f72d4a367", - "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" - }, - "time": "2013-02-08 11:00:28", - "bin": [ - "bin/phpspec" - ], - "type": "library", - "autoload": { - "psr-0": { - "PHPSpec2": "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.4", - "homepage": "http://phpspec.net/", - "keywords": [ - "BDD", - "SpecBDD", - "TDD", - "spec", - "specification" - ] - }, - { - "name": "phpunit/php-code-coverage", - "version": "1.2.x-dev", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "1.2.9" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/1.2.9", - "reference": "1.2.9", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "phpunit/php-file-iterator": ">=1.3.0@stable", - "phpunit/php-text-template": ">=1.1.1@stable", - "phpunit/php-token-stream": ">=1.1.3@stable" - }, - "suggest": { - "ext-dom": "*", - "ext-xdebug": ">=2.0.5" - }, - "time": "2013-02-26 18:55:56", - "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": "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" - ] - }, - { - "name": "phpunit/php-file-iterator", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "2deb24c65ea78e126daa8d45b2089ddc29ec1d26" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/2deb24c65ea78e126daa8d45b2089ddc29ec1d26", - "reference": "2deb24c65ea78e126daa8d45b2089ddc29ec1d26", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "time": "2013-01-07 10:47:05", - "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" - ] - }, - { - "name": "phpunit/php-text-template", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "1eeef106193d2f8c539728e566bb4793071a9e18" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/1eeef106193d2f8c539728e566bb4793071a9e18", - "reference": "1eeef106193d2f8c539728e566bb4793071a9e18", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "time": "2013-01-07 10:56:17", - "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" - ] - }, - { - "name": "phpunit/php-timer", - "version": "1.0.x-dev", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "ecf7920b27003a9412b07dad79dbb5ad1249e6c3" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/ecf7920b27003a9412b07dad79dbb5ad1249e6c3", - "reference": "ecf7920b27003a9412b07dad79dbb5ad1249e6c3", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "time": "2013-01-30 06:08:51", - "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" - ] - }, - { - "name": "phpunit/php-token-stream", - "version": "dev-master", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/php-token-stream.git", - "reference": "c25dd88e1592e66dee2553c99ef244203d5a1b98" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/c25dd88e1592e66dee2553c99ef244203d5a1b98", - "reference": "c25dd88e1592e66dee2553c99ef244203d5a1b98", - "shasum": "" - }, - "require": { - "ext-tokenizer": "*", - "php": ">=5.3.3" - }, - "time": "2013-01-07 10:56:35", - "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": "Wrapper around PHP's tokenizer extension.", - "homepage": "https://github.com/sebastianbergmann/php-token-stream/", - "keywords": [ - "tokenizer" - ] - }, - { - "name": "phpunit/phpunit", - "version": "3.7.x-dev", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "3.7.15" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/3.7.15", - "reference": "3.7.15", - "shasum": "" - }, - "require": { - "ext-dom": "*", - "ext-pcre": "*", - "ext-reflection": "*", - "ext-spl": "*", - "php": ">=5.3.3", - "phpunit/php-code-coverage": ">=1.2.1,<1.3.0", - "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", - "symfony/yaml": ">=2.2.0" - }, - "require-dev": { - "pear-pear/pear": "1.9.4" - }, - "suggest": { - "ext-json": "*", - "ext-simplexml": "*", - "ext-tokenizer": "*", - "phpunit/php-invoker": ">=1.1.0,<1.2.0" - }, - "time": "2013-03-01 11:55:06", - "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" - ] - }, - { - "name": "phpunit/phpunit-mock-objects", - "version": "1.2.x-dev", - "source": { - "type": "git", - "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", - "reference": "d49b5683200b5db9b1c64cb06f52f50d147891c4" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/d49b5683200b5db9b1c64cb06f52f50d147891c4", - "reference": "d49b5683200b5db9b1c64cb06f52f50d147891c4", - "shasum": "" - }, - "require": { - "php": ">=5.3.3", - "phpunit/php-text-template": ">=1.1.1@stable" - }, - "suggest": { - "ext-soap": "*" - }, - "time": "2013-02-05 07:46:41", - "type": "library", - "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" - ] - }, - { - "name": "rackspace/php-cloudfiles", - "version": "master", - "source": { - "type": "git", - "url": "git://github.com/rackspace/php-cloudfiles.git", - "reference": "master" - }, - "dist": { - "type": "zip", - "url": "https://github.com/rackspace/php-cloudfiles/zipball/master", - "reference": null, - "shasum": null - }, - "type": "library", - "autoload": { - "files": [ - "cloudfiles.php" - ] - }, - "time": "2013-01-10 18:09:51" - }, - { - "name": "symfony/console", - "version": "2.1.x-dev", - "target-dir": "Symfony/Component/Console", - "source": { - "type": "git", - "url": "https://github.com/symfony/Console", - "reference": "v2.1.7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/Console/zipball/v2.1.7", - "reference": "v2.1.7", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "time": "2013-01-17 15:20:05", - "type": "library", - "autoload": { - "psr-0": { - "Symfony\\Component\\Console": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "http://symfony.com/contributors" - } - ], - "description": "Symfony Console Component", - "homepage": "http://symfony.com" - }, - { - "name": "symfony/event-dispatcher", - "version": "2.1.x-dev", - "target-dir": "Symfony/Component/EventDispatcher", - "source": { - "type": "git", - "url": "https://github.com/symfony/EventDispatcher", - "reference": "e1d18ff0ff6f3e45ac82f000bc221135df635527" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/EventDispatcher/zipball/e1d18ff0ff6f3e45ac82f000bc221135df635527", - "reference": "e1d18ff0ff6f3e45ac82f000bc221135df635527", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "require-dev": { - "symfony/dependency-injection": "2.1.*" - }, - "suggest": { - "symfony/dependency-injection": "2.1.*", - "symfony/http-kernel": "2.1.*" - }, - "time": "2013-02-11 11:26:14", - "type": "library", - "autoload": { - "psr-0": { - "Symfony\\Component\\EventDispatcher": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "http://symfony.com/contributors" - } - ], - "description": "Symfony EventDispatcher Component", - "homepage": "http://symfony.com" - }, - { - "name": "symfony/finder", - "version": "2.1.x-dev", - "target-dir": "Symfony/Component/Finder", - "source": { - "type": "git", - "url": "https://github.com/symfony/Finder", - "reference": "v2.1.7" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/Finder/zipball/v2.1.7", - "reference": "v2.1.7", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "time": "2013-01-09 08:51:07", - "type": "library", - "autoload": { - "psr-0": { - "Symfony\\Component\\Finder": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "http://symfony.com/contributors" - } - ], - "description": "Symfony Finder Component", - "homepage": "http://symfony.com" - }, - { - "name": "symfony/yaml", - "version": "dev-master", - "target-dir": "Symfony/Component/Yaml", - "source": { - "type": "git", - "url": "https://github.com/symfony/Yaml.git", - "reference": "f198ac28048eeceae852419c076123aaee59cd1c" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/symfony/Yaml/zipball/f198ac28048eeceae852419c076123aaee59cd1c", - "reference": "f198ac28048eeceae852419c076123aaee59cd1c", - "shasum": "" - }, - "require": { - "php": ">=5.3.3" - }, - "time": "2013-01-31 21:39:01", - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "2.3-dev" - } - }, - "autoload": { - "psr-0": { - "Symfony\\Component\\Yaml\\": "" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Fabien Potencier", - "email": "fabien@symfony.com" - }, - { - "name": "Symfony Community", - "homepage": "http://symfony.com/contributors" - } - ], - "description": "Symfony Yaml Component", - "homepage": "http://symfony.com" - } - ], - "aliases": [ - - ], - "minimum-stability": "dev", - "stability-flags": { - "phpspec/phpspec2": 20 - } -} diff --git a/doc/adapters/amazon-s3.md b/doc/adapters/amazon-s3.md new file mode 100644 index 000000000..db2edc6f7 --- /dev/null +++ b/doc/adapters/amazon-s3.md @@ -0,0 +1,20 @@ +--- +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 +``\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..0ea013441 --- /dev/null +++ b/doc/adapters/apc.md @@ -0,0 +1,25 @@ +--- +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 + +`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', + 'accessKeySecret' => 'your_secret', + 'region' => 'eu-west-1', +]); + +$adapter = new AwsS3Adapter($s3client, 'your-bucket-name'); +$filesystem = new Filesystem($adapter); +``` diff --git a/doc/adapters/aws-s3.md b/doc/adapters/aws-s3.md new file mode 100644 index 000000000..b57145ed3 --- /dev/null +++ b/doc/adapters/aws-s3.md @@ -0,0 +1,87 @@ +--- +currentMenu: aws-s3 +--- + +# AWS S3 + +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 +``` + +In order to use this adapter you'll need an access key and a secret key. + +## Example + +```php + [ + 'key' => 'your_key_here', + 'secret' => 'your_secret', + ], + 'version' => 'latest', + 'region' => 'eu-west-1', +]); +// For aws-sdk-php v2 +$s3client = S3Client::factory([ + '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); +``` + +## 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) + +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 +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "s3:CreateBucket", + "s3:ListBucket" + ], + "Resource": [ + "arn:aws:s3:::bucket_name" + ] + }, + { + "Effect": "Allow", + "Action": [ + "s3:PutObject", + "s3:PutObjectAcl", + "s3:GetObject", + "s3:DeleteObject" + ], + "Resource": [ + "arn:aws:s3:::bucket_name/*" + ] + } + ] +} +``` diff --git a/doc/adapters/azure-blob-storage.md b/doc/adapters/azure-blob-storage.md new file mode 100644 index 000000000..38a3df98e --- /dev/null +++ b/doc/adapters/azure-blob-storage.md @@ -0,0 +1,55 @@ +--- +currentMenu: azure-blob-storage +--- + +# AzureBlobStorage + +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 +``` + +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). + +## 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 +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/doc/adapters/doctrine-dbal.md b/doc/adapters/doctrine-dbal.md new file mode 100644 index 000000000..e629ec6df --- /dev/null +++ b/doc/adapters/doctrine-dbal.md @@ -0,0 +1,37 @@ +--- +currentMenu: doctrine-dbal +-------------------------- + +# Doctrine DBAL + +First, you will need to install the adapter: +```bash +composer require gaufrette/doctrine-dbal-adapter +``` + +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, which allows you to override the default column names + +```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/adapters/ftp.md b/doc/adapters/ftp.md new file mode 100644 index 000000000..767c979bb --- /dev/null +++ b/doc/adapters/ftp.md @@ -0,0 +1,54 @@ +--- +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. + +## Server configuration + +Some FTP servers does not show hidden files by default. You will probably need to tweak your server configuration. + +### Pure Ftpd + +```bash +echo "yes" > /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/google-cloud-storage.md b/doc/adapters/google-cloud-storage.md new file mode 100644 index 000000000..80a7022cd --- /dev/null +++ b/doc/adapters/google-cloud-storage.md @@ -0,0 +1,46 @@ +--- +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] +(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 +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 +); + +$filesystem = new Gaufrette\Filesystem($adapter); +``` diff --git a/doc/adapters/grid-fs.md b/doc/adapters/grid-fs.md new file mode 100644 index 000000000..68dae7d89 --- /dev/null +++ b/doc/adapters/grid-fs.md @@ -0,0 +1,31 @@ +--- +currentMenu: grid-fs +--- + +# GridFS + +## Prerequisites + +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. + +First can install the MongoDB extension with: + +```bash +pecl install mongodb +``` + +Then, install the adapter: +```bash +composer require gaufrette/gridfs-adapter +``` + +## Usage + +```php + +$client = new \MongoDB\Client('mongodb://localhost:27017'); +$db = $client->selectDatabase('dbname'); + +$adapter = new \Gaufrette\Adapter\GridFS($db->selectGridFSBucket()); +``` diff --git a/doc/adapters/in-memory.md b/doc/adapters/in-memory.md new file mode 100644 index 000000000..e36412984 --- /dev/null +++ b/doc/adapters/in-memory.md @@ -0,0 +1,26 @@ +--- +currentMenu: in-memory +--- + +# InMemory + +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. + +``` + 'its content')); +$filesystem = new Filesystem($adapter); +``` diff --git a/doc/adapters/local.md b/doc/adapters/local.md new file mode 100644 index 000000000..820eeb292 --- /dev/null +++ b/doc/adapters/local.md @@ -0,0 +1,38 @@ +--- +currentMenu: local +--- + +# Local & SafeLocal + +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 +getAdapter()->delete($dirKey); +``` + +Note that you can't delete the root directory of the Local adapter. diff --git a/doc/adapters/phpseclib-sftp.md b/doc/adapters/phpseclib-sftp.md new file mode 100644 index 000000000..f987018d5 --- /dev/null +++ b/doc/adapters/phpseclib-sftp.md @@ -0,0 +1,27 @@ +--- +currentMenu: phpseclib-sftp +--- + +# phpseclib adapter + +*N.B.* It is recommended to use this adapter over [SFTP](sftp.html). + +## Prerequisites + +First, you will need to install the adapter: +```bash +composer require gaufrette/phpseclib-sftp-adapter +``` + +## Configuration + +```php + +$sftp = new phpseclib\Net\SFTP($host = 'localhost', $port = 22); + +//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/doc/adapters/sftp.md b/doc/adapters/sftp.md new file mode 100644 index 000000000..6d716cee8 --- /dev/null +++ b/doc/adapters/sftp.md @@ -0,0 +1,45 @@ +--- +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. + +This adapter is based on the `ssh2` extension. If you don't have this extension available and you can't install it, +the [`PhpseclibSftp`](phpseclib-sftp.html) adapter is based on a full-php ssh client. + +## Prerequisites + +* [PHP-SSH](https://github.com/Herzult/php-ssh) +* [SSH2 extension](http://www.php.net/manual/en/book.ssh2.php) + +You can install it via: + +```bash +composer require herzult/php-ssh:^1.1 +pecl install ssh2-beta +``` + +## Example + +The first argument should be an instance of `\Ssh\Client`. Please refer to +[`herzult/php-ssh`](https://github.com/Herzult/php-ssh) documentation to know how to build it. + +The second argument is the base directory you want to use. + +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). + +```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!" + +// You can also rename your file like this: +$file->rename('my/new/file'); +``` diff --git a/doc/caching.md b/doc/caching.md new file mode 100644 index 000000000..529830456 --- /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 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 + +Here is an example of how to cache an ftp filesystem: + +```php + 'root/dir'])); +$filesystem = new ResolvableFilesystem( + $decorated, + 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 +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/doc/implementing-new-adapter.md b/doc/implementing-new-adapter.md new file mode 100644 index 000000000..db0d4754b --- /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 sure `KnpStorage` implements all Gaufrette `Adapterinterface` 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! diff --git a/doc/installation.md b/doc/installation.md new file mode 100644 index 000000000..8cf5d1ea9 --- /dev/null +++ b/doc/installation.md @@ -0,0 +1,11 @@ +--- +currentMenu: installation +--- + +# Installation + +You can install it through Composer: + +```bash +$ composer require knplabs/gaufrette +``` 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. 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!" +``` 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 000000000..81c9ad949 Binary files /dev/null and b/doc/website/fonts/FontAwesome.otf differ diff --git a/doc/website/fonts/fontawesome-webfont.eot b/doc/website/fonts/fontawesome-webfont.eot new file mode 100644 index 000000000..84677bc0c Binary files /dev/null and b/doc/website/fonts/fontawesome-webfont.eot differ diff --git a/doc/website/fonts/fontawesome-webfont.svg b/doc/website/fonts/fontawesome-webfont.svg new file mode 100644 index 000000000..d907b25ae --- /dev/null +++ b/doc/website/fonts/fontawesome-webfont.svg @@ -0,0 +1,520 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ 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 000000000..96a3639cd Binary files /dev/null and b/doc/website/fonts/fontawesome-webfont.ttf differ diff --git a/doc/website/fonts/fontawesome-webfont.woff b/doc/website/fonts/fontawesome-webfont.woff new file mode 100644 index 000000000..628b6a52a Binary files /dev/null and b/doc/website/fonts/fontawesome-webfont.woff differ diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 000000000..8256432e7 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,57 @@ +version: '3.4' + +services: + php7.1: + build: + context: . + dockerfile: docker/php/Dockerfile_php_7_1 + 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_php_7_2 + target: php7.2 + env_file: .env + volumes: + - './:/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_php_7_3 + target: php7.3 + env_file: .env + volumes: + - './:/app' + - './docker/php/php.ini:/usr/local/etc/php/php.ini:ro' + depends_on: + - mongodb + - sftp + - ftp + + mongodb: + image: mongo + + sftp: + image: atmoz/sftp:alpine + command: gaufrette:gaufrette:::gaufrette + + ftp: + build: + context: ./docker/ftp + environment: + PUBLICHOST: 'ftp' diff --git a/docker/ftp/Dockerfile b/docker/ftp/Dockerfile new file mode 100644 index 000000000..8cf44f07a --- /dev/null +++ b/docker/ftp/Dockerfile @@ -0,0 +1,3 @@ +FROM stilliard/pure-ftpd + +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/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 diff --git a/docker/php/php.ini b/docker/php/php.ini new file mode 100644 index 000000000..fe64554e1 --- /dev/null +++ b/docker/php/php.ini @@ -0,0 +1,2 @@ +apc.enable_cli = 1 +date.timezone = UTC 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}" diff --git a/phpspec.yml b/phpspec.yml new file mode 100644 index 000000000..e69de29bb diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 000000000..2c7c957ee --- /dev/null +++ b/phpstan-baseline.neon @@ -0,0 +1,257 @@ +parameters: + ignoreErrors: + - + 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: "#^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: "#^Call to function is_resource\\(\\) with string will always evaluate to false\\.$#" + count: 1 + path: src/Gaufrette/Adapter/GoogleCloudStorage.php + + - + message: "#^Property Gaufrette\\\\Adapter\\\\GridFS\\:\\:\\$bucket has unknown class MongoDB\\\\GridFS\\\\Bucket as its type\\.$#" + count: 1 + path: src/Gaufrette/Adapter/GridFS.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: "#^Call to method openDownloadStreamByName\\(\\) on an unknown class MongoDB\\\\GridFS\\\\Bucket\\.$#" + count: 1 + path: src/Gaufrette/Adapter/GridFS.php + + - + message: "#^Caught class MongoDB\\\\GridFS\\\\Exception\\\\FileNotFoundException not found\\.$#" + count: 2 + path: src/Gaufrette/Adapter/GridFS.php + + - + message: "#^Call to method openUploadStream\\(\\) on an unknown class MongoDB\\\\GridFS\\\\Bucket\\.$#" + count: 2 + path: src/Gaufrette/Adapter/GridFS.php + + - + message: "#^Unreachable statement \\- code above always terminates\\.$#" + count: 1 + path: src/Gaufrette/Adapter/GridFS.php + + - + message: "#^Call to method downloadToStreamByName\\(\\) on an unknown class MongoDB\\\\GridFS\\\\Bucket\\.$#" + count: 1 + path: src/Gaufrette/Adapter/GridFS.php + + - + message: "#^Call to method findOne\\(\\) on an unknown class MongoDB\\\\GridFS\\\\Bucket\\.$#" + count: 6 + path: src/Gaufrette/Adapter/GridFS.php + + - + message: "#^Call to method find\\(\\) on an unknown class MongoDB\\\\GridFS\\\\Bucket\\.$#" + count: 2 + path: src/Gaufrette/Adapter/GridFS.php + + - + message: "#^Call to method delete\\(\\) on an unknown class MongoDB\\\\GridFS\\\\Bucket\\.$#" + count: 1 + path: src/Gaufrette/Adapter/GridFS.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 \\(\\$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: "#^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: "#^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..72b5c82e0 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,8 @@ +includes: + - phpstan-baseline.neon + +parameters: + level: 5 + reportUnmatchedIgnoredErrors: false + paths: + - src diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f23020bc1..456decf12 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -8,26 +8,9 @@ convertWarningsToExceptions="true" processIsolation="false" stopOnFailure="false" - syntaxCheck="false" - bootstrap="tests/bootstrap.php.dist" + bootstrap="vendor/autoload.php" > - - - - - + 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 @@ + + + + + + + + + diff --git a/spec/Gaufrette/Adapter/AclAwareAmazonS3.php b/spec/Gaufrette/Adapter/AclAwareAmazonS3.php deleted file mode 100644 index 9e955010d..000000000 --- a/spec/Gaufrette/Adapter/AclAwareAmazonS3.php +++ /dev/null @@ -1,234 +0,0 @@ -beConstructedWith($adapter, $service, 'bucketName'); - } - - function it_should_be_initializable() - { - $this->shouldHaveType('Gaufrette\Adapter\AclAwareAmazonS3'); - $this->shouldHaveType('Gaufrette\Adapter'); - $this->shouldHaveType('Gaufrette\Adapter\MetadataSupporter'); - } - - /** - * @param \Gaufrette\Adapter $adapter - */ - function it_should_delegate_read($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_should_delegate_rename_and_update_acl($adapter, $service) - { - $service - ->set_object_acl('bucketName', 'filename2', \AmazonS3::ACL_PRIVATE) - ->shouldBeCalled() - ->willReturn(new \CFResponse(array(), '', 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_should_not_rename_when_cannot_update_acl($adapter, $service) - { - $service - ->set_object_acl('bucketName', 'filename2', \AmazonS3::ACL_PRIVATE) - ->shouldBeCalled() - ->willReturn(new \CFResponse(array(), '', 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_should_update_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)); - $adapter - ->rename('filename', 'filename2') - ->willReturn(true); - - $this->setUsers(array(array('id' => 'someId', 'permission' => 'read'))); - $this->rename('filename', 'filename2')->shouldReturn(true); - } - - /** - * @param \Gaufrette\Adapter $adapter - * @param \AmazonS3 $service - */ - function it_should_delegate_write_and_update_acl($adapter, $service) - { - $service - ->set_object_acl('bucketName', 'filename', \AmazonS3::ACL_PRIVATE) - ->shouldBeCalled() - ->willReturn(new \CFResponse(array(), '', 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_should_not_write_when_cannot_update_acl($adapter, $service) - { - $service - ->set_object_acl('bucketName', 'filename', \AmazonS3::ACL_PRIVATE) - ->shouldBeCalled() - ->willReturn(new \CFResponse(array(), '', 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_should_update_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)); - $adapter - ->write('filename', 'some content') - ->willReturn(12); - - $this->setUsers(array(array('id' => 'someId', 'permission' => 'read'))); - $this->write('filename', 'some content')->shouldReturn(12); - } - - /** - * @param \Gaufrette\Adapter $adapter - */ - function it_should_delegate_exists($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_should_delegate_mtime($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_should_delegate_is_directory_check($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_should_delegate_keys($adapter) - { - $adapter->keys->willReturn(array('filename', 'filename2')); - - $this->keys()->shouldReturn(array('filename', 'filename2')); - } - - /** - * @param \spec\Gaufrette\Adapter\TestDelegateAdapter $extendedAdapter - * @param \AmazonS3 $service - */ - function it_should_delegate_metadata($extendedAdapter, $service) - { - $this->beConstructedWith($extendedAdapter, $service, 'bucketName'); - - $extendedAdapter->setMetadata('filename', array('some'))->shouldBeCalled(); - $extendedAdapter->getMetadata('filename')->shouldBeCalled()->willReturn(array('some2')); - - $this->setMetadata('filename', array('some')); - $this->getMetadata('filename')->shouldReturn(array('some2')); - } - - /** - * @param \Gaufrette\Adapter $adapter - */ - function it_should_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, - \Gaufrette\Adapter\MetadataSupporter -{ -} diff --git a/spec/Gaufrette/Adapter/AmazonS3.php b/spec/Gaufrette/Adapter/AmazonS3.php deleted file mode 100644 index 22a913386..000000000 --- a/spec/Gaufrette/Adapter/AmazonS3.php +++ /dev/null @@ -1,553 +0,0 @@ -beConstructedWith($service, 'bucketName'); - } - - function it_should_be_initializable() - { - $this->shouldHaveType('Gaufrette\Adapter\AmazonS3'); - $this->shouldHaveType('Gaufrette\Adapter'); - $this->shouldHaveType('Gaufrette\Adapter\MetadataSupporter'); - } - - /** - * @param \AmazonS3 $service - */ - function it_should_read_file($service) - { - $options = array( - 'range' => 12, - 'response' => array( - '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_should_return_false_when_cannot_read($service) - { - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true); - $service - ->get_object( - 'bucketName', - 'filename', - array() - ) - ->shouldBeCalled() - ->willReturn(new \CFResponse('header', 'some content', 500)); - - $this->read('filename')->shouldReturn(false); - } - - /** - * @param \AmazonS3 $service - */ - function it_should_not_mask_exception_when_read($service) - { - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true); - $service - ->get_object( - 'bucketName', - 'filename', - array() - ) - ->willThrow(new \RuntimeException('read')); - - $this->shouldThrow(new \RuntimeException('read'))->duringRead('filename'); - } - - /** - * @param \AmazonS3 $service - */ - function it_should_rename_file($service) - { - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true); - $service - ->copy_object( - array( - 'bucket' => 'bucketName', - 'filename' => 'filename1', - ), - array( - 'bucket' => 'bucketName', - 'filename' => 'filename2' - ), - array('acl' => \AmazonS3::ACL_OWNER_READ) - ) - ->shouldBeCalled() - ->willReturn(new \CFResponse('header', 'some content', 200)); - $service - ->delete_object( - 'bucketName', - 'filename1', - ANY_ARGUMENT - ) - ->shouldBeCalled() - ->willReturn(new \CFResponse(array(), 'some', 200)); - - $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) - { - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true); - $service - ->copy_object(ANY_ARGUMENT, ANY_ARGUMENT, ANY_ARGUMENT) - ->willThrow(new \RuntimeException('rename')); - - $this->shouldThrow(new \RuntimeException('rename'))->duringRename('filename', 'filename1'); - } - - /** - * @param \AmazonS3 $service - */ - function it_should_return_false_when_cannot_rename($service) - { - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true); - $service - ->copy_object( - array( - 'bucket' => 'bucketName', - 'filename' => 'filename1', - ), - array( - 'bucket' => 'bucketName', - 'filename' => 'filename2' - ), - array() - ) - ->shouldBeCalled() - ->willReturn(new \CFResponse('header', 'some content', 500)); - - $this->rename('filename1', 'filename2')->shouldReturn(false); - } - - /** - * @param \AmazonS3 $service - */ - function it_should_write_file($service) - { - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true); - $service - ->create_object( - 'bucketName', - 'filename', - array( - 'acl' => \AmazonS3::ACL_PRIVATE, - 'body' => 'some content' - ) - ) - ->shouldBeCalled() - ->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); - } - - /** - * @param \AmazonS3 $service - */ - function it_should_return_false_when_cannot_write($service) - { - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true); - $service - ->create_object( - 'bucketName', - 'filename', - array( - 'acl' => \AmazonS3::ACL_PUBLIC, - 'body' => 'some content' - ) - ) - ->shouldBeCalled() - ->willReturn(new \CFResponse(array('x-aws-requestheaders' => array('Content-Length' => 12)), 'some content', 500)); - - $this->write('filename', 'some content')->shouldReturn(false); - } - - /** - * @param \AmazonS3 $service - */ - function it_should_not_mask_exception_when_write($service) - { - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true); - $service - ->create_object(ANY_ARGUMENT, ANY_ARGUMENT, ANY_ARGUMENT) - ->willThrow(new \RuntimeException('write')); - - $this->shouldThrow(new \RuntimeException('write'))->duringWrite('filename', 'some content'); - } - - /** - * @param \AmazonS3 $service - */ - function it_should_check_if_file_exists($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_should_not_mask_exception_when_check_if_file_exists($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($service) - { - $metadata = array('acl' => \AmazonS3::ACL_PUBLIC); - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true); - - $service - ->get_object_metadata( - 'bucketName', - 'filename', - $metadata - ) - ->shouldBeCalled() - ->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')); - } - - /** - * @param \AmazonS3 $service - */ - function it_should_return_false_when_cannot_mtime($service) - { - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true); - - $service - ->get_object_metadata( - 'bucketName', - 'filename', - array() - ) - ->shouldBeCalled() - ->willReturn(array('Headers' => array())); - - $this->mtime('filename')->shouldReturn(false); - } - - /** - * @param \AmazonS3 $service - */ - function it_should_not_mask_exception_when_get_mtime($service) - { - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true); - $service - ->get_object_metadata('bucketName', 'filename', ANY_ARGUMENT) - ->willThrow(new \RuntimeException('mtime')); - - $this->shouldThrow(new \RuntimeException('mtime'))->duringMtime('filename'); - } - - /** - * @param \AmazonS3 $service - */ - function it_should_delete_file($service) - { - $metadata = array('acl' => \AmazonS3::ACL_PRIVATE); - - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true); - $service - ->delete_object( - 'bucketName', - 'filename', - $metadata - ) - ->willReturn(new \CFResponse(array(), 'some', 200)); - - $this->setMetadata('filename', $metadata); - $this->delete('filename')->shouldReturn(true); - } - - /** - * @param \AmazonS3 $service - */ - function it_should_not_mask_exception_when_delete($service) - { - $service - ->if_bucket_exists('bucketName') - ->willReturn(true); - $service - ->delete_object( - 'bucketName', - 'filename', - ANY_ARGUMENT - ) - ->willThrow(new \RuntimeException('delete')); - - $this->shouldThrow(new \RuntimeException('delete'))->duringDelete('filename'); - } - - /** - * @param \AmazonS3 $service - */ - function it_should_return_false_when_cannot_delete_file($service) - { - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true); - $service - ->delete_object( - 'bucketName', - 'filename', - array() - ) - ->willReturn(new \CFResponse(array(), 'some', 500)); - - $this->delete('filename')->shouldReturn(false); - } - - /** - * @param \AmazonS3 $service - */ - function it_should_get_keys($service) - { - $service - ->if_bucket_exists('bucketName') - ->shouldBeCalled() - ->willReturn(true); - $service - ->get_object_list('bucketName') - ->shouldBeCalled() - ->willReturn(array('filename2', 'aaa/filename', 'filename1')); - - $this->keys()->shouldReturn(array('aaa', 'aaa/filename', 'filename1', 'filename2')); - } - - /** - * @param \AmazonS3 $service - */ - function it_should_not_mask_exception_when_get_keys($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($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($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_should_create_bucket_if_create_mode_is_enabled($service) - { - $service - ->if_bucket_exists('bucketName') - ->willReturn(false); - $service - ->create_bucket('bucketName', \AmazonS3::REGION_US_E1) - ->shouldBeCalled() - ->willReturn(new \CFResponse(array(), 'created', 201)); - $service - ->if_object_exists('bucketName', 'filename') - ->willReturn(false); - - $this->beConstructedWith($service, 'bucketName', array('create' => true)); - $this->exists('filename'); - } - - /** - * @param \AmazonS3 $service - */ - function it_allows_to_configure_region_when_creating_bucket($service) - { - $service - ->if_bucket_exists('bucketName') - ->willReturn(false); - $service - ->create_bucket('bucketName', \AmazonS3::REGION_EU_W1) - ->shouldBeCalled() - ->willReturn(new \CFResponse(array(), 'created', 201)); - $service - ->if_object_exists('bucketName', 'filename') - ->willReturn(false); - - $this->beConstructedWith($service, 'bucketName', array('create' => true, 'region' => \AmazonS3::REGION_EU_W1)); - $this->exists('filename'); - } - - /** - * @param \AmazonS3 $service - */ - function it_should_fail_when_cannot_create_bucket($service) - { - $service - ->if_bucket_exists('bucketName') - ->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_should_set_region_when_region_option_is_set($service) - { - $service - ->set_region(\AmazonS3::REGION_EU_W1) - ->shouldBeCalled(); - - $this->beConstructedWith($service, 'bucketName', array('region' => \AmazonS3::REGION_EU_W1)); - $this->exists('filename'); - } -} diff --git a/spec/Gaufrette/Adapter/Apc.php b/spec/Gaufrette/Adapter/Apc.php deleted file mode 100644 index 30f85e284..000000000 --- a/spec/Gaufrette/Adapter/Apc.php +++ /dev/null @@ -1,68 +0,0 @@ -beConstructedWith('prefix-apc-test/'); - } - - function letgo() - { - global $extensionLoaded; - $extensionLoaded = null; - } - - function it_should_be_initializable() - { - $this->shouldHaveType('Gaufrette\Adapter\Apc'); - $this->shouldHaveType('Gaufrette\Adapter'); - } - - function it_should_read_file() - { - $this->read('filename')->shouldReturn('prefix-apc-test/filename content'); - $this->read('filename2')->shouldReturn('prefix-apc-test/filename2 content'); - } - - function it_should_write_file() - { - $this->write('filename', 'some content')->shouldReturn(12); - $this->write('invalid', 'some content')->shouldReturn(false); - } - - function it_should_delete_file() - { - $this->delete('filename')->shouldReturn(true); - $this->delete('invalid')->shouldReturn(false); - } - - function it_should_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() - { - $this->exists('filename')->shouldReturn(true); - $this->exists('invalid')->shouldReturn(false); - } - - function it_should_not_handles_directory() - { - $this->isDirectory('filename')->shouldReturn(false); - $this->isDirectory('invalid')->shouldReturn(false); - } -} 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/spec/Gaufrette/Adapter/AwsS3Spec.php b/spec/Gaufrette/Adapter/AwsS3Spec.php new file mode 100644 index 000000000..7dd38a8f9 --- /dev/null +++ b/spec/Gaufrette/Adapter/AwsS3Spec.php @@ -0,0 +1,43 @@ +beConstructedWith($service, 'bucketName'); + } + + function it_is_initializable() + { + $this->shouldHaveType('Gaufrette\Adapter\AwsS3'); + } + + 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/spec/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php b/spec/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php new file mode 100644 index 000000000..88b9f50c9 --- /dev/null +++ b/spec/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php @@ -0,0 +1,22 @@ +beConstructedWith($connectionString); + } + + function it_should_be_initializable() + { + $this->shouldHaveType('Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactory'); + $this->shouldHaveType('Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface'); + } +} diff --git a/spec/Gaufrette/Adapter/AzureBlobStorageSpec.php b/spec/Gaufrette/Adapter/AzureBlobStorageSpec.php new file mode 100644 index 000000000..977cdcf56 --- /dev/null +++ b/spec/Gaufrette/Adapter/AzureBlobStorageSpec.php @@ -0,0 +1,505 @@ +beConstructedWith($blobProxyFactory, 'containerName'); + } + + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + 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 + */ + function it_should_get_keys($blobProxyFactory, $blobProxy, $listBlobResult) + { + $fileNames = ['aaa', 'aaa/filename', 'filename1', 'filename2']; + $blobs = []; + 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(['aaa', 'aaa/filename', 'filename1', 'filename2']); + } + + /** + * @param \Gaufrette\Adapter\AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory + * @param \WindowsAzure\Blob\Internal\IBlob $blobProxy + */ + 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 + */ + 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 + */ + 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 + */ + 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/Cache.php b/spec/Gaufrette/Adapter/Cache.php deleted file mode 100644 index 593b2b302..000000000 --- a/spec/Gaufrette/Adapter/Cache.php +++ /dev/null @@ -1,165 +0,0 @@ -beConstructedWith($source, $cache); - } - - function it_should_be_initializable() - { - $this->shouldHaveType('Gaufrette\Adapter\Cache'); - $this->shouldHaveType('Gaufrette\Adapter'); - $this->shouldHaveType('Gaufrette\Adapter\MetadataSupporter'); - } - - /** - * @param \spec\Gaufrette\Adapter\CacheTestExtendedAdapter $extendedSource - * @param \spec\Gaufrette\Adapter\CacheTestExtendedAdapter $extendedCache - */ - function it_should_handle_metadata_when_cached_adapters_supports_metadata($extendedSource, $extendedCache) - { - $extendedSource->setMetadata('filename', array('metadata'))->shouldBeCalled(); - $extendedCache->setMetadata('filename', array('metadata'))->shouldBeCalled(); - $extendedSource->getMetadata('filename')->shouldBeCalled()->willReturn(array('someMetadata')); - $this->beConstructedWith($extendedSource, $extendedCache); - - $this->setMetadata('filename', array('metadata')); - $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) - { - $source->isDirectory('filename')->shouldBeCalled()->willReturn(true); - - $this->isDirectory('filename')->shouldReturn(true); - } - - /** - * @param \Gaufrette\Adapter $source - * @param \Gaufrette\Adapter $cache - */ - function it_should_read_from_cache($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')); - - $this->read('filename')->shouldReturn('some content'); - } - - /** - * @param \Gaufrette\Adapter $source - * @param \Gaufrette\Adapter $cache - */ - function it_should_read_from_source_and_write_to_cache($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')); - - $this->read('filename')->shouldReturn('some other content'); - } - - /** - * @param \Gaufrette\Adapter $source - * @param \Gaufrette\Adapter $cache - */ - function it_should_rename_file_in_source_and_cache($source, $cache) - { - $source->rename('filename', 'filename2')->shouldBeCalled(); - $cache->rename('filename', 'filename2')->shouldBeCalled()->willReturn(true); - - $this->rename('filename', 'filename2')->shouldReturn(true); - } - - /** - * @param \Gaufrette\Adapter $source - * @param \Gaufrette\Adapter $cache - */ - function it_should_write_file_to_source_and_cache($source, $cache) - { - $source->write('filename', 'some content')->shouldBeCalled(); - $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_should_delete_file_from_source_and_cache($source, $cache) - { - $source->delete('filename')->shouldBeCalled()->willReturn(true); - $cache->delete('filename')->shouldBeCalled()->willReturn(true); - - $this->delete('filename')->shouldReturn(true); - } - - /** - * @param \Gaufrette\Adapter $source - */ - function it_should_check_if_exists_in_source($source) - { - $source->exists('filename')->shouldBeCalled()->willReturn(true); - - $this->exists('filename')->shouldReturn(true); - } - - /** - * @param \Gaufrette\Adapter $source - */ - function it_should_get_mtime_from_source($source) - { - $source->mtime('filename')->shouldBeCalled()->willReturn(1234); - - $this->mtime('filename')->shouldReturn(1234); - } - - /** - * @param \Gaufrette\Adapter $source - */ - function it_should_get_keys_from_source($source) - { - $source->keys()->willReturn(array('filename2', 'filename1', 'filename')); - - $this->keys()->shouldReturn(array('filename', 'filename1', 'filename2')); - } -} - -interface CacheTestExtendedAdapter extends \Gaufrette\Adapter, - \Gaufrette\Adapter\ChecksumCalculator, - \Gaufrette\Adapter\MetadataSupporter -{ - -} diff --git a/spec/Gaufrette/Adapter/DoctrineDbal.php b/spec/Gaufrette/Adapter/DoctrineDbal.php deleted file mode 100644 index 66247e41e..000000000 --- a/spec/Gaufrette/Adapter/DoctrineDbal.php +++ /dev/null @@ -1,204 +0,0 @@ -beConstructedWith($connection, 'someTableName'); - } - - function it_should_be_initializable() - { - $this->shouldHaveType('Gaufrette\Adapter\DoctrineDbal'); - $this->shouldHaveType('Gaufrette\Adapter'); - $this->shouldHaveType('Gaufrette\Adapter\ChecksumCalculator'); - } - - function it_should_not_handle_directories() - { - $this->isDirectory('filename')->shouldReturn(false); - } - - /** - * @param \Doctrine\DBAL\Connection $connection - */ - function it_should_check_if_file_exists($connection) - { - $connection - ->quoteIdentifier(ANY_ARGUMENT) - ->willReturnUsing(function ($argument) { return sprintf('"%s"', $argument); }); - - $connection - ->fetchColumn('SELECT COUNT("key") FROM "someTableName" WHERE "key" = :key', array('key' => 'filename')) - ->willReturn(12); - $this->exists('filename')->shouldReturn(true); - - $connection - ->fetchColumn('SELECT COUNT("key") FROM "someTableName" WHERE "key" = :key', array('key' => 'filename')) - ->willReturn(0); - $this->exists('filename')->shouldReturn(false); - } - - /** - * @param \Doctrine\DBAL\Connection $connection - */ - function it_should_write_file($connection) - { - $connection - ->quoteIdentifier(ANY_ARGUMENT) - ->willReturnUsing(function ($argument) { return sprintf('"%s"', $argument); }); - $connection - ->fetchColumn('SELECT COUNT("key") FROM "someTableName" WHERE "key" = :key', array('key' => 'filename')) - ->willReturn(false); - $connection - ->insert( - 'someTableName', - array( - '"content"' => 'some content', - '"mtime"' => strtotime('2012-10-10 23:10:10'), - '"checksum"' => '9893532233caff98cd083a116b013c0b', - '"key"' => 'filename' - )) - ->shouldBeCalled(); - - $this->write('filename', 'some content')->shouldReturn(12); - } - - /** - * @param \Doctrine\DBAL\Connection $connection - */ - function it_should_update_file($connection) - { - $connection - ->quoteIdentifier(ANY_ARGUMENT) - ->willReturnUsing(function ($argument) { return sprintf('"%s"', $argument); }); - $connection - ->fetchColumn('SELECT COUNT("key") FROM "someTableName" WHERE "key" = :key', array('key' => 'filename')) - ->willReturn(true); - $connection - ->update( - 'someTableName', - array( - '"content"' => 'some content', - '"mtime"' => strtotime('2012-10-10 23:10:10'), - '"checksum"' => '9893532233caff98cd083a116b013c0b', - ), - array( - '"key"' => 'filename' - )) - ->shouldBeCalled(); - - $this->write('filename', 'some content')->shouldReturn(12); - } - - /** - * @param \Doctrine\DBAL\Connection $connection - */ - function it_should_read_file($connection) - { - $connection - ->quoteIdentifier(ANY_ARGUMENT) - ->willReturnUsing(function ($argument) { return sprintf('"%s"', $argument); }); - $connection - ->fetchColumn('SELECT "content" FROM "someTableName" WHERE "key" = :key', array('key' => 'filename')) - ->willReturn('some content'); - - $this->read('filename')->shouldReturn('some content'); - } - - /** - * @param \Doctrine\DBAL\Connection $connection - */ - function it_should_calculate_checksum($connection) - { - $connection - ->quoteIdentifier(ANY_ARGUMENT) - ->willReturnUsing(function ($argument) { return sprintf('"%s"', $argument); }); - $connection - ->fetchColumn('SELECT "checksum" FROM "someTableName" WHERE "key" = :key', array('key' => 'filename')) - ->willReturn(1234); - - $this->checksum('filename')->shouldReturn(1234); - } - - /** - * @param \Doctrine\DBAL\Connection $connection - */ - function it_should_get_mtime($connection) - { - $connection - ->quoteIdentifier(ANY_ARGUMENT) - ->willReturnUsing(function ($argument) { return sprintf('"%s"', $argument); }); - $connection - ->fetchColumn('SELECT "mtime" FROM "someTableName" WHERE "key" = :key', array('key' => 'filename')) - ->willReturn(1234); - - $this->mtime('filename')->shouldReturn(1234); - } - - /** - * @param \Doctrine\DBAL\Connection $connection - */ - function it_should_rename_file($connection) - { - $connection - ->quoteIdentifier(ANY_ARGUMENT) - ->willReturnUsing(function ($argument) { return sprintf('"%s"', $argument); }); - $connection - ->update( - 'someTableName', - array( - '"key"' => 'newFile', - ), - array( - '"key"' => 'filename' - )) - ->shouldBeCalled() - ->willReturn(1); - - $this->rename('filename', 'newFile')->shouldReturn(true); - } - - /** - * @param \Doctrine\DBAL\Connection $connection - * @param \Doctrine\DBAL\Statement $stmt - */ - function it_should_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); }); - $connection - ->executeQuery('SELECT "key" FROM "someTableName"') - ->willReturn($stmt); - - $this->keys()->shouldReturn(array('filename', 'filename1', 'filename2')); - } - - /** - * @param \Doctrine\DBAL\Connection $connection - */ - function it_should_delete_file($connection) - { - $connection - ->quoteIdentifier(ANY_ARGUMENT) - ->willReturnUsing(function ($argument) { return sprintf('"%s"', $argument); }); - $connection - ->delete('someTableName', array('"key"' => 'filename')) - ->shouldBeCalled() - ->willReturn(1); - - $this->delete('filename')->shouldReturn(true); - } -} diff --git a/spec/Gaufrette/Adapter/DoctrineDbalSpec.php b/spec/Gaufrette/Adapter/DoctrineDbalSpec.php new file mode 100644 index 000000000..4e723e148 --- /dev/null +++ b/spec/Gaufrette/Adapter/DoctrineDbalSpec.php @@ -0,0 +1,270 @@ +beConstructedWith($connection, 'someTableName'); + } + + function it_is_adapter() + { + $this->shouldHaveType('Gaufrette\Adapter'); + } + + function it_is_checksum_calculator() + { + $this->shouldHaveType('Gaufrette\Adapter\ChecksumCalculator'); + } + + function it_does_not_handle_directories() + { + $this->isDirectory('filename')->shouldReturn(false); + } + + /** + * @param \Doctrine\DBAL\Connection $connection + */ + function it_checks_if_file_exists(Connection $connection) + { + $connection + ->quoteIdentifier(Argument::any()) + ->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 + ->$method('SELECT COUNT("key") FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) + ->willReturn(12); + $this->exists('filename')->shouldReturn(true); + + $connection + ->$method('SELECT COUNT("key") FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) + ->willReturn(0); + $this->exists('filename')->shouldReturn(false); + } + + /** + * @param \Doctrine\DBAL\Connection $connection + */ + function it_writes_to_new_file(Connection $connection) + { + $connection + ->quoteIdentifier(Argument::any()) + ->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 + ->$method('SELECT COUNT("key") FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) + ->willReturn(false); + $connection + ->insert( + 'someTableName', + [ + '"content"' => 'some content', + '"mtime"' => strtotime('2012-10-10 23:10:10'), + '"checksum"' => '9893532233caff98cd083a116b013c0b', + '"key"' => 'filename', + ] + ) + ->shouldBeCalled(); + + $this->write('filename', 'some content')->shouldReturn(12); + } + + /** + * @param \Doctrine\DBAL\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 + ->$method('SELECT COUNT("key") FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) + ->willReturn(true); + $connection + ->update( + 'someTableName', + [ + '"content"' => 'some content', + '"mtime"' => strtotime('2012-10-10 23:10:10'), + '"checksum"' => '9893532233caff98cd083a116b013c0b', + ], + [ + '"key"' => 'filename', + ] + ) + ->shouldBeCalled(); + + $this->write('filename', 'some content')->shouldReturn(12); + } + + /** + * @param \Doctrine\DBAL\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 + ->$method('SELECT "content" FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) + ->willReturn('some content'); + + $this->read('filename')->shouldReturn('some content'); + } + + /** + * @param \Doctrine\DBAL\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 + ->$method('SELECT "checksum" FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) + ->willReturn(1234); + + $this->checksum('filename')->shouldReturn(1234); + } + + /** + * @param \Doctrine\DBAL\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 + ->$method('SELECT "mtime" FROM "someTableName" WHERE "key" = :key', ['key' => 'filename']) + ->willReturn(1234); + + $this->mtime('filename')->shouldReturn(1234); + } + + /** + * @param \Doctrine\DBAL\Connection $connection + */ + function it_renames_file(Connection $connection) + { + $connection + ->quoteIdentifier(Argument::any()) + ->will(function ($argument) { + return sprintf('"%s"', $argument[0]); + }); + $connection + ->update( + 'someTableName', + [ + '"key"' => 'newFile', + ], + [ + '"key"' => 'filename', + ] + ) + ->shouldBeCalled() + ->willReturn(1); + + $this->rename('filename', 'newFile')->shouldReturn(true); + } + + /** + * @param \Doctrine\DBAL\Connection $connection + */ + function it_get_keys(Connection $connection, $result) + { + 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) { + return sprintf('"%s"', $argument[0]); + }); + $connection + ->executeQuery('SELECT "key" FROM "someTableName"') + ->willReturn($result); + + $this->keys()->shouldReturn(['filename', 'filename1', 'filename2']); + } + + /** + * @param \Doctrine\DBAL\Connection $connection + */ + function it_deletes_file(Connection $connection) + { + $connection + ->quoteIdentifier(Argument::any()) + ->will(function ($argument) { + return sprintf('"%s"', $argument[0]); + }); + $connection + ->delete('someTableName', ['"key"' => 'filename']) + ->shouldBeCalled() + ->willReturn(1); + + $this->delete('filename')->shouldReturn(true); + } +} diff --git a/spec/Gaufrette/Adapter/Dropbox.php b/spec/Gaufrette/Adapter/Dropbox.php deleted file mode 100644 index e4ac1b7a4..000000000 --- a/spec/Gaufrette/Adapter/Dropbox.php +++ /dev/null @@ -1,275 +0,0 @@ -beConstructedWith($dropbox); - } - - function it_should_be_initializable() - { - $this->shouldHaveType('Gaufrette\Adapter\Dropbox'); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_should_read_file($dropbox) - { - $dropbox->getFile('filename')->willReturn('some content'); - - $this->read('filename')->shouldReturn('some content'); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_should_not_mask_exception_from_client_during_read($dropbox) - { - $dropbox->getFile('filename')->willThrow(new \RuntimeException('read')); - - $this->shouldThrow(new \RuntimeException('read'))->duringRead('filename'); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_should_not_read_file($dropbox) - { - $dropbox - ->getFile('filename') - ->willThrow(new \Dropbox_Exception_NotFound()); - - $this->read('filename')->shouldReturn(false); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_should_check_if_file_exists($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 - )); - - $this->exists('filename')->shouldReturn(true); - - $dropbox - ->getMetaData('filename', false) - ->willThrow(new \Dropbox_Exception_NotFound); - - $this->exists('filename')->shouldReturn(false); - - $dropbox - ->getMetaData('filename', false) - ->willReturn(array("is_deleted" => true)); - - $this->exists('filename')->shouldReturn(false); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_should_not_mask_exception_from_client_during_check_if_exists($dropbox) - { - $dropbox - ->getMetaData('filename', false) - ->willThrow(new \RuntimeException('exists')); - - $this->shouldThrow(new \RuntimeException('exists'))->duringExists('filename'); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_should_get_keys($dropbox) - { - $dropbox - ->getMetaData('/', true) - ->willReturn(array( - 'contents' => array( - array('path' => '/filename'), - array('path' => '/aaa/filename') - ) - )); - - $this->keys()->shouldReturn(array('aaa', 'aaa/filename', 'filename')); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_should_not_mask_exception_from_client_during_getting_keys($dropbox) - { - $dropbox - ->getMetaData('/', true) - ->willThrow(new \RuntimeException('keys')); - - $this->shouldThrow(new \RuntimeException('keys'))->duringKeys(); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_should_check_is_directory($dropbox) - { - $dropbox - ->getMetaData('filename', false) - ->willReturn(array( - "is_dir" => true - )); - - $this->isDirectory('filename')->shouldReturn(true); - - $dropbox - ->getMetaData('filename', false) - ->willReturn(array( - "is_dir" => false - )); - - $this->isDirectory('filename')->shouldReturn(false); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_should_write_file($dropbox) - { - $dropbox - ->putFile('filename', ANY_ARGUMENT) - ->shouldBeCalled(); - - $this->write('filename', 'some content')->shouldReturn(12); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_should_not_mask_exception_from_client_during_write($dropbox) - { - $dropbox - ->putFile('filename', ANY_ARGUMENT) - ->willThrow(new \RuntimeException('write')); - - $this->shouldThrow(new \RuntimeException('write'))->duringWrite('filename', 'some content'); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_should_delete_file($dropbox) - { - $dropbox - ->delete('filename') - ->shouldBeCalled(); - - $this->delete('filename')->shouldReturn(true); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_should_not_delete_file($dropbox) - { - $dropbox - ->delete('filename') - ->willThrow(new \Dropbox_Exception_NotFound()); - - $this->delete('filename')->shouldReturn(false); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_should_rename_file($dropbox) - { - $dropbox - ->move('filename', 'filename2') - ->shouldBeCalled(); - - $this->rename('filename', 'filename2')->shouldReturn(true); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_should_not_rename_file($dropbox) - { - $dropbox - ->move('filename', 'filename2') - ->willThrow(new \Dropbox_Exception_NotFound()); - - $this->rename('filename', 'filename2')->shouldReturn(false); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_should_get_mtime($dropbox) - { - $dropbox - ->getMetaData('filename', false) - ->willReturn(array( - "modified" => "Tue, 19 Jul 2011 21:55:38 +0000", - )); - - $this->mtime('filename')->shouldReturn(1311112538); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_should_not_fetch_mtime_when_file_not_found($dropbox) - { - $dropbox - ->getMetaData('filename', false) - ->willThrow(new \Dropbox_Exception_NotFound()); - - $this->mtime('filename')->shouldReturn(false); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_should_not_check_if_key_is_dir_when_file_not_found($dropbox) - { - $dropbox - ->getMetaData('filename', false) - ->willThrow(new \Dropbox_Exception_NotFound()); - - $this->isDirectory('filename')->shouldReturn(false); - } - - /** - * @param \Dropbox_API $dropbox - */ - function it_should_fail_checking_if_key_is_dir_when_dropbox_throws_exception($dropbox) - { - $dropbox - ->getMetaData('filename', false) - ->willThrow(new \RuntimeException('some exception')); - - $this->shouldThrow(new \RuntimeException('some exception'))->duringIsDirectory('filename'); - } -} diff --git a/spec/Gaufrette/Adapter/FlysystemSpec.php b/spec/Gaufrette/Adapter/FlysystemSpec.php new file mode 100644 index 000000000..48f9934da --- /dev/null +++ b/spec/Gaufrette/Adapter/FlysystemSpec.php @@ -0,0 +1,106 @@ +beConstructedWith($adapter, $config); + } + + 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.']); + + $this->read('filename')->shouldReturn('Hello.'); + } + + function it_writes_file(AdapterInterface $adapter, Config $config) + { + $adapter->write('filename', 'Hello.', $config)->willReturn([]); + + $this->write('filename', 'Hello.')->shouldReturn([]); + } + + function it_checks_if_file_exists(AdapterInterface $adapter) + { + $adapter->has('filename')->willReturn(true); + + $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([[ + 'path' => 'folder', + 'timestamp' => 1457104978, + 'size' => 22, + 'type' => 'dir', + ]]); + + $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); + + $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); + } + + function it_does_not_support_is_directory(AdapterInterface $adapter) + { + $this->shouldThrow('Gaufrette\Exception\UnsupportedAdapterMethodException')->duringisDirectory('folder'); + } +} diff --git a/spec/Gaufrette/Adapter/Ftp.php b/spec/Gaufrette/Adapter/Ftp.php deleted file mode 100644 index 48c8333fd..000000000 --- a/spec/Gaufrette/Adapter/Ftp.php +++ /dev/null @@ -1,156 +0,0 @@ -beConstructedWith('/home/l3l0', 'localhost'); - } - - function it_should_be_initializable() - { - $this->shouldHaveType('Gaufrette\Adapter\Ftp'); - $this->shouldHaveType('Gaufrette\Adapter'); - } - - function it_should_check_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() - { - $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_should_read_file() - { - $this->read('filename')->shouldReturn('some content'); - } - - function it_should_not_read_file() - { - $this->read('filename2')->shouldReturn(false); - } - - function it_should_write_file() - { - $this->write('filename', 'some content')->shouldReturn(12); - } - - function it_should_not_write_file() - { - $this->write('filename2', 'some content')->shouldReturn(false); - } - - function it_should_rename_file() - { - $this->rename('filename', 'filename2')->shouldReturn(true); - } - - function it_should_not_rename_file() - { - $this->rename('filename', 'invalid')->shouldReturn(false); - } - - function it_should_fetch_keys_without_directories_dots() - { - $this->keys()->shouldReturn(array('filename', 'filename.exe', '.htaccess', 'aaa', 'aaa/filename')); - } - - function it_should_get_mtime() - { - $this->mtime('filename')->shouldReturn(strtotime('2010-10-10 23:10:10')); - } - - function it_should_throw_excention_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() - { - $this->delete('filename')->shouldReturn(true); - } - - function it_should_not_delete_file() - { - $this->delete('invalid')->shouldReturn(false); - } - - /** - * @param \Gaufrette\Filesystem $filesystem - */ - function it_should_create_file($filesystem) - { - $this->createFile('filename', $filesystem)->shouldReturnAnInstanceOf('\Gaufrette\File'); - } - - /** - * @param \Gaufrette\Filesystem $filesystem - */ - function it_should_create_file_in_not_existing_directory($filesystem) - { - $this->createFile('bb/cc/dd/filename', $filesystem)->shouldReturnAnInstanceOf('\Gaufrette\File'); - } - - function it_should_check_is_directory() - { - $this->isDirectory('aaa')->shouldReturn(true); - $this->isDirectory('filename')->shouldReturn(false); - } - - function it_should_fetch_keys_with_hidden_files() - { - $this->beConstructedWith('/home/l3l1', 'localhost'); - - $this->keys()->shouldReturn(array('filename', '.htaccess')); - } - - function it_should_check_if_hidden_file_exists() - { - $this->beConstructedWith('/home/l3l1', 'localhost'); - - $this->exists('.htaccess')->shouldReturn(true); - } - - function it_should_create_base_directory_without_warning() - { - global $createdDirectory; - $createdDirectory = ''; - - $this->beConstructedWith('/home/l3l0/new', 'localhost', array('create' => true)); - - $this->listDirectory()->shouldReturn(array('keys' => array(), 'dirs' => array())); - } - - function it_should_not_create_base_directory_and_should_throw_exception() - { - global $createdDirectory; - $createdDirectory = ''; - - $this->beConstructedWith('/home/l3l0/new', 'localhost', array('create' => false)); - - $this->shouldThrow(new \RuntimeException("The directory '/home/l3l0/new' does not exist."))->during('listDirectory', array()); - } - - function it_should_fetch_keys_for_windows() - { - $this->beConstructedWith('C:\Ftp', 'localhost'); - - $this->keys()->shouldReturn(array('archive', 'file1.zip', 'file2.zip')); - } -} diff --git a/spec/Gaufrette/Adapter/GoogleCloudStorageSpec.php b/spec/Gaufrette/Adapter/GoogleCloudStorageSpec.php new file mode 100644 index 000000000..6fd449fa9 --- /dev/null +++ b/spec/Gaufrette/Adapter/GoogleCloudStorageSpec.php @@ -0,0 +1,28 @@ +beConstructedWith($service, 'bucketName'); + } + + function it_is_adapter() + { + $this->shouldHaveType('Gaufrette\Adapter'); + } + + function it_supports_metadata() + { + $this->shouldHaveType('Gaufrette\Adapter\MetadataSupporter'); + } + + function it_is_list_keys_aware() + { + $this->shouldHaveType('Gaufrette\Adapter\ListKeysAware'); + } +} diff --git a/spec/Gaufrette/Adapter/GridFS.php b/spec/Gaufrette/Adapter/GridFS.php deleted file mode 100644 index 926b6e712..000000000 --- a/spec/Gaufrette/Adapter/GridFS.php +++ /dev/null @@ -1,270 +0,0 @@ -setInternalClassMethodParamMap("MongoCollection", "aggregate", array('$pipeline', '$op = null', '$some = null')); - -class GridFS extends ObjectBehavior -{ - /** - * @param \MongoGridFS $gridFs - */ - function let($gridFs) - { - $this->beConstructedWith($gridFs); - } - - function it_should_be_initializable() - { - $this->shouldHaveType('Gaufrette\Adapter\GridFS'); - $this->shouldHaveType('Gaufrette\Adapter'); - } - - /** - * @param \MongoGridFS $gridFs - * @param \MongoGridFSFile $file - */ - function it_should_read_file($gridFs, $file) - { - $file - ->getBytes() - ->willReturn('some content'); - $gridFs - ->findOne('filename', array()) - ->shouldBeCalled() - ->willReturn($file); - - $this->read('filename')->shouldReturn('some content'); - } - - /** - * @param \MongoGridFS $gridFs - */ - function it_should_not_fail_when_cannot_read($gridFs) - { - $gridFs - ->findOne('filename', array()) - ->shouldBeCalled() - ->willReturn(null); - - $this->read('filename')->shouldReturn(false); - } - - /** - * @param \MongoGridFS $gridFs - * @param \MongoGridFSFile $file - */ - function it_should_check_if_exists($gridFs, $file) - { - $gridFs - ->findOne('filename', array()) - ->willReturn($file); - $gridFs - ->findOne('filename2', array()) - ->willReturn(null); - - $this->exists('filename')->shouldReturn(true); - $this->exists('filename2')->shouldReturn(false); - } - - /** - * @param \MongoGridFS $gridFs - */ - function it_should_delete_file($gridFs) - { - $file = new \stdClass; - $file->file = array('_id' => 123); - $gridFs - ->findOne('filename', array('_id')) - ->willReturn($file); - $gridFs - ->delete(123) - ->willReturn(true); - - $this->delete('filename')->shouldReturn(true); - } - - /** - * @param \MongoGridFS $gridFs - */ - function it_should_not_delete_file($gridFs) - { - $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); - - $gridFs - ->findOne('filename', array('_id')) - ->willReturn(null); - $this->delete('filename')->shouldReturn(false); - } - - /** - * @param \MongoGridFS $gridFs - * @param \MongoGridFSFile $file - */ - function it_should_write_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_should_update_file($gridFs, $file) - { - $someFile = new \stdClass; - $someFile->file = array('_id' => 123); - $gridFs - ->findOne('filename', array('_id')) - ->shouldBeCalled() - ->willReturn($someFile); - $gridFs - ->delete(123) - ->shouldBeCalled() - ->willReturn(true); - - $file - ->getSize() - ->willReturn(12); - $gridFs - ->findOne('filename', array()) - ->willReturn($file); - $gridFs - ->storeBytes(ANY_ARGUMENT, ANY_ARGUMENT) - ->willReturn('someId'); - $gridFs - ->findOne(array('_id' => 'someId')) - ->willReturn($file); - - $this - ->write('filename', 'some content') - ->shouldReturn(12); - } - - /** - * @param \MongoGridFS $gridFs - * @param \MongoGridFSFile $file - */ - function it_should_rename_file($gridFs, $file) - { - $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'); - - $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); - - $this->setMetadata('otherFilename', array('date' => 1234)); - $this->rename('filename', 'otherFilename')->shouldReturn(true); - } - - /** - * @param \MongoGridFS $gridFs - */ - function it_should_get_keys($gridFs, $file, $otherFile) - { - $gridFs - ->find(array(), array('filename')) - ->willReturn(array(new File('filename'), new File('otherFilename'))); - - $this->keys()->shouldReturn(array('filename', 'otherFilename')); - } - - /** - * @param \MongoGridFS $gridFs - */ - function it_should_get_mtime($gridFs) - { - $time = new \stdClass; - $time->sec = 12345; - - $someFile = new \stdClass; - $someFile->file = array('date' => $time); - $gridFs - ->findOne('filename', array('date')) - ->willReturn($someFile); - - $this->mtime('filename')->shouldReturn(12345); - } - - /** - * @param \MongoGridFS $gridFs - */ - function it_should_calculate_checksum($gridFs) - { - $someFile = new \stdClass; - $someFile->file = array('md5' => 'md5123'); - $gridFs - ->findOne('filename', array('md5')) - ->willReturn($someFile); - - $this->checksum('filename')->shouldReturn('md5123'); - } -} - -class File -{ - private $filename; - - public function __construct($filename) - { - $this->filename = $filename; - } - - public function getFilename() - { - return $this->filename; - } -} diff --git a/spec/Gaufrette/Adapter/GridFSSpec.php b/spec/Gaufrette/Adapter/GridFSSpec.php new file mode 100644 index 000000000..87ec43f17 --- /dev/null +++ b/spec/Gaufrette/Adapter/GridFSSpec.php @@ -0,0 +1,169 @@ +beConstructedWith($bucket); + } + + function letGo() + { + array_map(function ($res) { + if (is_resource($res)) { + @fclose($res); + } + }, $this->resources); + } + + function it_is_adapter() + { + $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'); + } + + function it_reads_file($bucket) + { + $this->resources[] = $readable = fopen('php://memory', 'rw'); + fwrite($readable, 'some content'); + fseek($readable, 0); + + $bucket + ->openDownloadStreamByName('filename') + ->shouldBeCalled() + ->willReturn($readable) + ; + + $this->read('filename')->shouldReturn('some content'); + } + + function it_does_not_fail_when_cannot_read($bucket) + { + $bucket->openDownloadStreamByName('filename')->willThrow(FileNotFoundException::class); + + $this->read('filename')->shouldReturn(false); + } + + function it_checks_if_file_exists($bucket, BSONDocument $file) + { + $bucket + ->findOne(['filename' => 'filename']) + ->willReturn($file) + ; + $bucket + ->findOne(['filename' => 'filename2']) + ->willReturn(null) + ; + + $this->exists('filename')->shouldReturn(true); + $this->exists('filename2')->shouldReturn(false); + } + + function it_deletes_file($bucket) + { + $bucket + ->findOne(['filename' => 'filename'], ['projection' => ['_id' => 1]]) + ->willReturn($file = new BSONDocument(['_id' => 123])) + ; + $bucket->delete(123)->shouldBeCalled(); + + $this->delete('filename')->shouldReturn(true); + } + + function it_does_not_delete_file($bucket) + { + $bucket->findOne(['filename' => 'filename'], ['projection' => ['_id' => 1]])->willReturn(null); + + $this->delete('filename')->shouldReturn(false); + } + + function it_writes_file($bucket) + { + $this->resources[] = $writable = fopen('php://memory', 'rw'); + + $bucket + ->openUploadStream('filename', ['metadata' => ['someother' => 'metadata']]) + ->willReturn($writable) + ; + + $this->setMetadata('filename', ['someother' => 'metadata']); + $this + ->write('filename', 'some content') + ->shouldReturn(12) + ; + } + + function it_renames_file($bucket) + { + $this->resources[] = $writable = fopen('php://memory', 'rw'); + $this->resources[] = $readable = fopen('php://memory', 'rw'); + fwrite($readable, 'some content'); + fseek($readable, 0); + + $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('filename', ['some' => 'metadata']); + $this->rename('filename', 'otherFilename')->shouldReturn(true); + } + + function it_fetches_keys($bucket) + { + $bucket + ->find([], ['projection' => ['filename' => 1]]) + ->willReturn([new BSONDocument(['filename' => 'filename']), new BSONDocument(['filename' => 'otherFilename'])]) + ; + + $this->keys()->shouldReturn(['filename', 'otherFilename']); + } + + function it_fetches_mtime($bucket) + { + $bucket + ->findOne(['filename' => 'filename'], ['projection' => ['uploadDate' => 1]]) + ->willReturn(new BSONDocument(['uploadDate' => new UTCDateTime(12345000)])) + ; + + $this->mtime('filename')->shouldReturn(12345); + } + + function it_calculates_checksum($bucket) + { + $bucket + ->findOne(['filename' => 'filename'], ['projection' => ['md5' => 1]]) + ->willReturn(new BSONDocument(['md5' => 'md5123'])) + ; + + $this->checksum('filename')->shouldReturn('md5123'); + } +} diff --git a/spec/Gaufrette/Adapter/InMemory.php b/spec/Gaufrette/Adapter/InMemory.php deleted file mode 100644 index d2c67e810..000000000 --- a/spec/Gaufrette/Adapter/InMemory.php +++ /dev/null @@ -1,66 +0,0 @@ -beConstructedWith(array( - 'filename' => array('mtime' => 12345, 'content' => 'content'), - 'filename2' => 'other content' - )); - } - - function it_should_be_initializable() - { - $this->shouldHaveType('Gaufrette\Adapter\InMemory'); - } - - function it_should_read_file() - { - $this->read('filename')->shouldReturn('content'); - } - - function it_should_write_file() - { - $this->write('filename', 'some content')->shouldReturn(12); - } - - function it_should_rename_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() - { - $this->exists('filename')->shouldReturn(true); - $this->exists('filenameTest')->shouldReturn(false); - } - - function it_should_get_keys() - { - $this->keys()->shouldReturn(array('filename', 'filename2')); - } - - function it_should_get_mtime() - { - $this->mtime('filename')->shouldReturn(12345); - } - - function it_should_delete_file() - { - $this->delete('filename')->shouldReturn(true); - $this->exists('filename')->shouldReturn(false); - } - - function it_should_not_handle_dirs() - { - $this->isDirectory('filename')->shouldReturn(false); - $this->isDirectory('filename2')->shouldReturn(false); - } -} diff --git a/spec/Gaufrette/Adapter/InMemorySpec.php b/spec/Gaufrette/Adapter/InMemorySpec.php new file mode 100644 index 000000000..89728cb78 --- /dev/null +++ b/spec/Gaufrette/Adapter/InMemorySpec.php @@ -0,0 +1,76 @@ +beConstructedWith([ + 'filename' => ['mtime' => 12345, 'content' => 'content'], + 'filename2' => 'other content', + ]); + } + + 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'); + } + + function it_writes_file() + { + $this->write('filename', 'some content')->shouldReturn(12); + } + + function it_renames_file() + { + $this->rename('filename', 'aaa/filename2')->shouldReturn(true); + $this->exists('filename')->shouldReturn(false); + $this->exists('aaa/filename2')->shouldReturn(true); + } + + function it_checks_if_file_exists() + { + $this->exists('filename')->shouldReturn(true); + $this->exists('filenameTest')->shouldReturn(false); + } + + function it_fetches_keys() + { + $this->keys()->shouldReturn(['filename', 'filename2']); + } + + function it_fetches_mtime() + { + $this->mtime('filename')->shouldReturn(12345); + } + + function it_deletes_file() + { + $this->delete('filename')->shouldReturn(true); + $this->exists('filename')->shouldReturn(false); + } + + 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/LazyRackspaceCloudfiles.php deleted file mode 100644 index 87ce815de..000000000 --- a/spec/Gaufrette/Adapter/LazyRackspaceCloudfiles.php +++ /dev/null @@ -1,132 +0,0 @@ -create()->willReturn($connection); - $this->beConstructedWith($connectionFactory, 'my_container'); - } - - function it_should_be_initializable() - { - $this->shouldHaveType('\Gaufrette\Adapter\LazyRackspaceCloudfiles'); - $this->shouldHaveType('\Gaufrette\Adapter\RackspaceCloudfiles'); - } - - /** - * @param \CF_Container $container - * @param \CF_Object $object - */ - function it_should_lazily_fetch_container_before_read($connection, $container, $object) - { - $connection->get_container('my_container')->willReturn($container)->shouldBeCalled(); - $connection->create_container(ANY_ARGUMENTS)->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(); - $this->read('filename1')->shouldReturn('some content'); - } - - /** - * @param \CF_Container $container - * @param \CF_Object $object - */ - function it_should_lazily_create_container_before_read($connectionFactory, $connection, $container, $object) - { - $this->beConstructedWith($connectionFactory, 'my_container', true); - - $connection->get_container(ANY_ARGUMENT)->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_should_lazily_fetch_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_should_lazily_fetch_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_should_lazily_fetch_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_should_lazily_fetch_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_should_lazily_fetch_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/Local.php b/spec/Gaufrette/Adapter/Local.php deleted file mode 100644 index 86779ffae..000000000 --- a/spec/Gaufrette/Adapter/Local.php +++ /dev/null @@ -1,143 +0,0 @@ -beConstructedWith('/home/somedir'); - } - - function letgo() - { - global $iteratorToArray; - $iteratorToArray = array(); - } - - function it_should_be_initializable() - { - $this->shouldHaveType('Gaufrette\Adapter\Local'); - } - - function it_should_read_file() - { - $this->read('filename')->shouldReturn('/home/somedir/filename content'); - } - - function it_should_write_file() - { - $this->write('filename', 'some content')->shouldReturn(12); - } - - function it_should_rename_file() - { - $this->rename('filename', 'aaa/filename2')->shouldReturn('/home/somedir/filename to /home/somedir/aaa/filename2'); - } - - function it_should_check_if_file_exists() - { - $this->exists('filename')->shouldReturn(true); - $this->exists('filename1')->shouldReturn(false); - } - - function it_should_get_keys() - { - global $iteratorToArray; - $iteratorToArray = array('/home/somedir/filename', '/home/somedir/filename1', '/home/somedir/aaa/filename'); - - $this->keys()->shouldReturn(array('aaa', 'aaa/filename', 'filename', 'filename1')); - } - - function it_should_get_mtime() - { - $this->mtime('filename')->shouldReturn(12345); - } - - function it_should_delete_file() - { - $this->delete('filename')->shouldReturn(true); - $this->delete('filename1')->shouldReturn(false); - } - - function it_should_check_if_key_is_dir() - { - $this->isDirectory('dir')->shouldReturn(true); - $this->isDirectory('filename')->shouldReturn(false); - } - - function it_should_create_local_stream() - { - $this->createStream('filename')->shouldReturnAnInstanceOf('Gaufrette\Stream\Local'); - } - - function it_should_accepts_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() - { - $this - ->shouldThrow(new \OutOfBoundsException('The path "/home/filename" is out of the filesystem.')) - ->duringRead('../filename'); - $this - ->shouldThrow(new \OutOfBoundsException('The path "/home/filename" is out of the filesystem.')) - ->duringExists('../filename'); - } - - function it_should_fail_when_directory_does_not_exists() - { - $this->beConstructedWith('/home/other'); - - $this - ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) - ->duringRead('filename'); - $this - ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) - ->duringWrite('filename', 'some content'); - $this - ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) - ->duringRename('filename', 'otherFilename'); - $this - ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) - ->duringExists('filename'); - $this - ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) - ->duringKeys(); - $this - ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) - ->duringMtime('filename'); - $this - ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) - ->duringDelete('filename'); - $this - ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) - ->duringIsDirectory('filename'); - $this - ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) - ->duringCreateStream('filename'); - $this - ->shouldThrow(new \RuntimeException('The directory "/home/other" does not exist.')) - ->duringChecksum('filename'); - } - - function it_should_create_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/LocalSpec.php b/spec/Gaufrette/Adapter/LocalSpec.php new file mode 100644 index 000000000..b76cce0a4 --- /dev/null +++ b/spec/Gaufrette/Adapter/LocalSpec.php @@ -0,0 +1,166 @@ +beConstructedWith(vfsStream::url('test')); + } + + function it_is_adapter() + { + $this->shouldHaveType('Gaufrette\Adapter'); + } + + 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'); + } + + function it_reads_file() + { + $this->read('filename')->shouldReturn("content\n"); + } + + function it_writes_file() + { + $this->write('filename', 'some content')->shouldReturn(12); + } + + function it_renames_file() + { + $this->rename('filename', 'aaa/filename2')->shouldReturn(true); + } + + function it_checks_if_file_exists() + { + $this->exists('filename')->shouldReturn(true); + $this->exists('filename1')->shouldReturn(false); + } + + function it_fetches_keys() + { + $expectedKeys = ['filename', 'dir', 'dir/file']; + sort($expectedKeys); + $this->keys()->shouldReturn($expectedKeys); + } + + function it_fetches_mtime() + { + $mtime = filemtime(vfsStream::url('test/filename')); + $this->mtime('filename')->shouldReturn($mtime); + } + + function it_deletes_file() + { + $this->delete('filename')->shouldReturn(true); + $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); + $this->isDirectory('filename')->shouldReturn(false); + } + + function it_creates_local_stream() + { + $this->createStream('filename')->shouldReturnAnInstanceOf('Gaufrette\Stream\Local'); + } + + function it_does_not_allow_to_read_path_above_main_file_directory() + { + $this + ->shouldThrow(new \OutOfBoundsException(sprintf('The path "%s" is out of the filesystem.', vfsStream::url('filename')))) + ->duringRead('../filename') + ; + $this + ->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(vfsStream::url('other')); + + $this + ->shouldThrow(new \RuntimeException(sprintf('The directory "%s" does not exist.', vfsStream::url('other')))) + ->duringRead('filename') + ; + $this + ->shouldThrow(new \RuntimeException(sprintf('The directory "%s" does not exist.', vfsStream::url('other')))) + ->duringWrite('filename', 'some content') + ; + $this + ->shouldThrow(new \RuntimeException(sprintf('The directory "%s" does not exist.', vfsStream::url('other')))) + ->duringRename('filename', 'otherFilename') + ; + $this + ->shouldThrow(new \RuntimeException(sprintf('The directory "%s" does not exist.', vfsStream::url('other')))) + ->duringExists('filename') + ; + $this + ->shouldThrow(new \RuntimeException(sprintf('The directory "%s" does not exist.', vfsStream::url('other')))) + ->duringKeys() + ; + $this + ->shouldThrow(new \RuntimeException(sprintf('The directory "%s" does not exist.', vfsStream::url('other')))) + ->duringMtime('filename') + ; + $this + ->shouldThrow(new \RuntimeException(sprintf('The directory "%s" does not exist.', vfsStream::url('other')))) + ->duringDelete('filename') + ; + $this + ->shouldThrow(new \RuntimeException(sprintf('The directory "%s" does not exist.', vfsStream::url('other')))) + ->duringIsDirectory('filename') + ; + $this + ->shouldThrow(new \RuntimeException(sprintf('The directory "%s" does not exist.', vfsStream::url('other')))) + ->duringCreateStream('filename') + ; + $this + ->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() + { + $this->beConstructedWith(vfsStream::url('test/other'), true); + + $this->isDirectory('/')->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/MogileFS.php b/spec/Gaufrette/Adapter/MogileFS.php deleted file mode 100644 index 641a80966..000000000 --- a/spec/Gaufrette/Adapter/MogileFS.php +++ /dev/null @@ -1,25 +0,0 @@ -beConstructedWith('http://domain.com', array('localhost')); - } - - function it_should_be_initializable() - { - $this->shouldHaveType('Gaufrette\Adapter\MogileFS'); - $this->shouldHaveType('Gaufrette\Adapter'); - } - - function it_should_not_handle_mtime() - { - $this->mtime('filename')->shouldReturn(false); - $this->mtime('filename2')->shouldReturn(false); - } -} diff --git a/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php b/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php new file mode 100644 index 000000000..7f6703cdd --- /dev/null +++ b/spec/Gaufrette/Adapter/PhpseclibSftpSpec.php @@ -0,0 +1,157 @@ +beConstructedWith($sftp, '/home/l3l0', false, 'l3lo', 'password'); + } + + function it_is_adapter() + { + $this->shouldHaveType('Gaufrette\Adapter'); + } + + function it_is_file_factory() + { + $this->shouldHaveType('Gaufrette\Adapter\FileFactory'); + } + + function it_supports_native_list_keys() + { + $this->shouldHaveType('Gaufrette\Adapter\ListKeysAware'); + } + + /** + * @param \spec\Gaufrette\Adapter\SFTP $sftp + */ + function it_fetches_keys(SFTP $sftp) + { + $sftp + ->file_exists('/home/l3l0/') + ->willReturn(true); + $sftp + ->rawlist('/home/l3l0/') + ->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([ + 'filename' => ['type' => NET_SFTP_TYPE_REGULAR], + ]); + + $this->keys()->shouldReturn(['filename', 'filename1', 'aaa', 'aaa/filename']); + } + + /** + * @param \spec\Gaufrette\Adapter\SFTP $sftp + */ + function it_reads_file(SFTP $sftp) + { + $sftp->get('/home/l3l0/filename')->willReturn('some content'); + + $this->read('filename')->shouldReturn('some content'); + } + + /** + * @param \spec\Gaufrette\Adapter\SFTP $sftp + */ + function it_creates_and_writes_file(SFTP $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); + + $this->write('filename', 'some content')->shouldReturn(12); + } + + /** + * @param \spec\Gaufrette\Adapter\SFTP $sftp + */ + function it_renames_file(SFTP $sftp) + { + $sftp->pwd()->willReturn('/home/l3l0'); + $sftp->chdir('/home/l3l0')->willReturn(true); + $sftp + ->rename('/home/l3l0/filename', '/home/l3l0/filename1') + ->willReturn(true) + ; + + $this->rename('filename', 'filename1')->shouldReturn(true); + } + + /** + * @param \spec\Gaufrette\Adapter\SFTP $sftp + */ + 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([ + '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\SFTP $sftp + */ + function it_should_check_is_directory(SFTP $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\SFTP $sftp + * @param \Gaufrette\Filesystem $filesystem + */ + function it_should_create_file(SFTP $sftp, Filesystem $filesystem) + { + $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/RackspaceCloudfiles.php b/spec/Gaufrette/Adapter/RackspaceCloudfiles.php deleted file mode 100644 index a066ea83a..000000000 --- a/spec/Gaufrette/Adapter/RackspaceCloudfiles.php +++ /dev/null @@ -1,263 +0,0 @@ -beConstructedWith($container); - } - - function it_should_be_initializable() - { - $this->shouldHaveType('Gaufrette\Adapter\RackspaceCloudfiles'); - $this->shouldHaveType('Gaufrette\Adapter'); - $this->shouldHaveType('Gaufrette\Adapter\ChecksumCalculator'); - } - - /** - * @param \CF_Container $container - * @param \CF_Object $object - */ - function it_should_read_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_should_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_should_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_should_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_should_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_should_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_should_create_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_should_rename_file($container, $fromObject, $toObject) - { - $fromObject - ->read() - ->willReturn('some content'); - $toObject - ->write('some content') - ->willReturn(true); - $container - ->get_object('filename') - ->willReturn($fromObject); - $container - ->get_object('filename1') - ->willReturn($toObject); - - $this->rename('filename', 'filename1')->shouldReturn(true); - } - - /** - * @param \CF_Container $container - */ - function it_should_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_should_get_keys($container) - { - $container->list_objects(0, null, null)->willReturn(array('filename2', 'filename1')); - - $this->keys()->shouldReturn(array('filename1', 'filename2')); - } - - /** - * @param \CF_Container $container - */ - function it_should_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() - { - $this->mtime('filename')->shouldBe(false); - $this->mtime('filename2')->shouldBe(false); - } - - /** - * @param \CF_Container $container - * @param \CF_Object $object - */ - function it_should_calculate_checksum($container, $object) - { - $object->getETag()->willReturn('123m5'); - $container->get_object('filename')->willReturn($object); - - $this->checksum('filename')->shouldReturn('123m5'); - } - - /** - * @param \CF_Container $container - */ - function it_should_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_should_delete_object($container) - { - $container->delete_object('filename')->shouldBeCalled(); - - $this->delete('filename')->shouldReturn(true); - } - - /** - * @param \CF_Container $container - */ - function it_should_not_delete_object($container) - { - $container->delete_object('filename')->willThrow(new \NoSuchObjectException); - - $this->delete('filename')->shouldReturn(false); - } - - /** - * @param \CF_Container $container - */ - function it_should_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/spec/Gaufrette/Adapter/RackspaceCloudfiles/AuthenticationConnectionFactory.php b/spec/Gaufrette/Adapter/RackspaceCloudfiles/AuthenticationConnectionFactory.php deleted file mode 100644 index 893f2e6d5..000000000 --- a/spec/Gaufrette/Adapter/RackspaceCloudfiles/AuthenticationConnectionFactory.php +++ /dev/null @@ -1,36 +0,0 @@ -beConstructedWith($authentication); - } - - function it_should_be_initializable() - { - $this->shouldHaveType('Gaufrette\Adapter\RackspaceCloudfiles\AuthenticationConnectionFactory'); - $this->shouldHaveType('Gaufrette\Adapter\RackspaceCloudfiles\ConnectionFactoryInterface'); - } - - function it_should_create_cf_connection() - { - $this->create()->shouldReturnAnInstanceOf('\CF_Connection'); - } - - function it_should_authenticate_when_not_authenticated($authentication) - { - $authentication->authenticated()->willReturn(false); - $authentication->authenticate()->shouldBeCalled(); - $authentication->authenticated()->willReturn(true); - - $this->create()->shouldReturnAnInstanceOf('\CF_Connection'); - } -} diff --git a/spec/Gaufrette/Adapter/SafeLocal.php b/spec/Gaufrette/Adapter/SafeLocal.php deleted file mode 100644 index 3207dbe81..000000000 --- a/spec/Gaufrette/Adapter/SafeLocal.php +++ /dev/null @@ -1,35 +0,0 @@ -beConstructedWith('/home/l3l0'); - } - - function it_should_be_initializable() - { - $this->shouldHaveType('Gaufrette\Adapter\SafeLocal'); - $this->shouldHaveType('Gaufrette\Adapter'); - } - - function it_should_compute_path_using_base64() - { - $this->read('filename')->shouldReturn('/home/l3l0/'.base64_encode('filename').' content'); - } - - function it_should_compute_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')); - } -} diff --git a/spec/Gaufrette/Adapter/SafeLocalSpec.php b/spec/Gaufrette/Adapter/SafeLocalSpec.php new file mode 100644 index 000000000..0badbe4c6 --- /dev/null +++ b/spec/Gaufrette/Adapter/SafeLocalSpec.php @@ -0,0 +1,32 @@ +beConstructedWith(vfsStream::url('test')); + } + + function it_is_local_adapter() + { + $this->shouldHaveType('Gaufrette\Adapter\Local'); + } + + function it_computes_path_using_base64() + { + 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([base64_decode('dir'), base64_decode('dir/file'), base64_decode('filename')]); + } +} diff --git a/spec/Gaufrette/Adapter/Sftp.php b/spec/Gaufrette/Adapter/Sftp.php deleted file mode 100644 index 8229c93e3..000000000 --- a/spec/Gaufrette/Adapter/Sftp.php +++ /dev/null @@ -1,98 +0,0 @@ -beConstructedWith($sftp, '/home/l3l0'); - } - - function it_should_be_initializable() - { - $this->shouldHaveType('Gaufrette\Adapter\Sftp'); - $this->shouldHaveType('Gaufrette\Adapter'); - $this->shouldHaveType('Gaufrette\Adapter\ChecksumCalculator'); - } - - /** - * @param \Ssh\Sftp $sftp - */ - function it_should_get_keys($sftp) - { - $sftp - ->listDirectory('/home/l3l0', true) - ->willReturn(array('files' => array('/home/l3l0/filename', '/home/l3l0/filename1', '/home/l3l0/aaa/filename'))); - - $this->keys()->shouldReturn(array('aaa', 'aaa/filename', 'filename', 'filename1')); - } - - /** - * @param \Ssh\Sftp $sftp - */ - function it_should_read_file($sftp) - { - $sftp - ->read('/home/l3l0/filename') - ->shouldBeCalled() - ->willReturn('some content'); - - $this->read('filename')->shouldReturn('some content'); - } - - /** - * @param \Ssh\Sftp $sftp - */ - function it_should_write_file($sftp) - { - $sftp - ->write('/home/l3l0/filename', 'some content') - ->shouldBeCalled() - ->willReturn(12); - - $this->write('filename', 'some content')->shouldReturn(12); - } - - /** - * @param \Ssh\Sftp $sftp - */ - function it_should_rename_file($sftp) - { - $sftp - ->rename('/home/l3l0/filename', '/home/l3l0/filename1') - ->shouldBeCalled() - ->willReturn(true); - - $this->rename('filename', 'filename1')->shouldReturn(true); - } - - /** - * @param \Ssh\Sftp $sftp - */ - function it_should_check_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/spec/Gaufrette/Adapter/Zip.php b/spec/Gaufrette/Adapter/Zip.php deleted file mode 100644 index 893e4e0ee..000000000 --- a/spec/Gaufrette/Adapter/Zip.php +++ /dev/null @@ -1,19 +0,0 @@ -beConstructedWith('somefile'); - } - - function it_should_be_initializable() - { - $this->shouldHaveType('Gaufrette\Adapter\Zip'); - $this->shouldHaveType('Gaufrette\Adapter'); - } -} diff --git a/spec/Gaufrette/Adapter/functions.php b/spec/Gaufrette/Adapter/functions.php index 6ab7b215b..177bf120f 100644 --- a/spec/Gaufrette/Adapter/functions.php +++ b/spec/Gaufrette/Adapter/functions.php @@ -4,208 +4,21 @@ 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, array('/home/l3l0', '/home/l3l0/aaa', '/home/l3l0/relative', '/home/l3l0/relative/some', '/home/l3l1', '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, array('/home/l3l0/new'))) { - global $createdDirectory; - $createdDirectory = $dirname; - - return true; - } - - 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) { - return false; - } - - return fopen('php://temp', 'r'); -} - -function ftp_close($connection) -{ - return fclose($connection); -} - -function ftp_rawlist($connection, $directory, $recursive = false) -{ - $arguments = explode(' ', $directory); - 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" - ); - } - - if ('/home/l3l0/aaa' === end($arguments)) - { - return array( - "-rwxr-x--- 15 vincent vincent 4096 Nov 3 21:31 filename" - ); - } - - 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", - ); - } - - 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", - ); - } - - // 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", - ); - } - - return array(); -} - -function ftp_login($connection, $username, $password) -{ - if ('invalid' === $username) { - return false; - } - - 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, ['/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; @@ -222,36 +35,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/File.php b/spec/Gaufrette/FileSpec.php similarity index 65% rename from spec/Gaufrette/File.php rename to spec/Gaufrette/FileSpec.php index 2c86a096a..80576bbda 100644 --- a/spec/Gaufrette/File.php +++ b/spec/Gaufrette/FileSpec.php @@ -2,24 +2,30 @@ namespace spec\Gaufrette; -use PHPSpec2\ObjectBehavior; +use Gaufrette\Filesystem; +use PhpSpec\ObjectBehavior; -class File extends 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); } - 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 +33,7 @@ function it_should_gives_access_to_key() /** * @param \Gaufrette\Filesystem $filesystem */ - function it_should_get_content($filesystem) + function it_gets_content(Filesystem $filesystem) { $filesystem->read('filename')->shouldBeCalled()->willReturn('Some content'); @@ -37,7 +43,7 @@ function it_should_get_content($filesystem) /** * @param \Gaufrette\Filesystem $filesystem */ - function it_should_get_mtime($filesystem) + function it_gets_mtime(Filesystem $filesystem) { $filesystem->mtime('filename')->shouldBeCalled()->willReturn(1358797854); @@ -48,10 +54,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 $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); $this->setContent('some content', $metadata); @@ -61,10 +68,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 $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); $this->getContent($metadata); @@ -74,35 +82,23 @@ 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 $filesystem, MetadataAdapter $adapter) { - $metadata = array('id' => '123'); + $metadata = ['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 $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); @@ -110,12 +106,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 +120,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) { $filesystem->write('filename', 'some content', true)->shouldBeCalled()->willReturn(21); @@ -135,9 +131,9 @@ 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_filesystem(Filesystem $filesystem) { - $filesystem->read('filename')->shouldBeCalled()->willReturn('some content'); + $filesystem->size('filename')->shouldBeCalled()->willReturn(12); $this->getSize()->shouldReturn(12); } @@ -145,7 +141,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) { $filesystem->read('filename')->shouldNotBeCalled(); @@ -156,9 +152,9 @@ 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) { - $filesystem->read('filename')->willThrow(new \Gaufrette\Exception\FileNotFound('filename')); + $filesystem->size('filename')->willThrow(new \Gaufrette\Exception\FileNotFound('filename')); $this->getSize()->shouldReturn(0); } @@ -166,7 +162,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) { $filesystem->has('filename')->willReturn(true); $this->exists()->shouldReturn(true); @@ -178,13 +174,18 @@ 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) { $filesystem->delete('filename')->shouldBeCalled()->willReturn(true); $this->delete()->shouldReturn(true); } -} -interface MetadataAdapter extends \Gaufrette\Adapter, - \Gaufrette\Adapter\MetadataSupporter -{} + /** + * @param \Gaufrette\Filesystem $filesystem + */ + function it_renames_file_from_filesystem(Filesystem $filesystem) + { + $filesystem->rename('filename', 'newname')->shouldBeCalled(); + $this->rename('newname'); + } +} diff --git a/spec/Gaufrette/FilesystemMap.php b/spec/Gaufrette/FilesystemMapSpec.php similarity index 62% rename from spec/Gaufrette/FilesystemMap.php rename to spec/Gaufrette/FilesystemMapSpec.php index a13cacc78..358b94241 100644 --- a/spec/Gaufrette/FilesystemMap.php +++ b/spec/Gaufrette/FilesystemMapSpec.php @@ -2,11 +2,12 @@ namespace spec\Gaufrette; -use PHPSpec2\ObjectBehavior; +use Gaufrette\Filesystem; +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 +15,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 $filesystem) { $this->set('some', $filesystem); $this->has('some')->shouldReturn(true); @@ -24,23 +25,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 $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'); + ->shouldThrow(new \InvalidArgumentException('There is no filesystem defined having "some" name.')) + ->duringGet('some') + ; } /** * @param Gaufrette\Filesystem $filesystem */ - function it_should_remove_mapped_filesystem($filesystem) + function it_removes_mapped_filesystem(Filesystem $filesystem) { $this->set('some', $filesystem); $this->remove('some'); @@ -48,17 +50,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 $filesystem) { $this->set('some', $filesystem); $this->set('other', $filesystem); @@ -66,6 +69,6 @@ function it_should_remove_all_filesystems($filesystem) $this->has('some')->shouldReturn(false); $this->has('other')->shouldReturn(false); - $this->all()->shouldReturn(array()); + $this->all()->shouldReturn([]); } } diff --git a/spec/Gaufrette/Filesystem.php b/spec/Gaufrette/FilesystemSpec.php similarity index 61% rename from spec/Gaufrette/Filesystem.php rename to spec/Gaufrette/FilesystemSpec.php index f2e90c6a7..ad8d66405 100644 --- a/spec/Gaufrette/Filesystem.php +++ b/spec/Gaufrette/FilesystemSpec.php @@ -2,27 +2,40 @@ namespace spec\Gaufrette; -use PHPSpec2\ObjectBehavior; +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 Filesystem extends ObjectBehavior +class FilesystemSpec extends ObjectBehavior { /** * @param \Gaufrette\Adapter $adapter */ - function let($adapter) + function let(Adapter $adapter) { $this->beConstructedWith($adapter); } - function it_should_be_initializable() + function it_is_initializable() { $this->shouldBeAnInstanceOf('Gaufrette\Filesystem'); + $this->shouldBeAnInstanceOf('Gaufrette\FilesystemInterface'); } /** * @param \Gaufrette\Adapter $adapter */ - function it_gives_access_to_adapter($adapter) + function it_gives_access_to_adapter(Adapter $adapter) { $this->getAdapter()->shouldBe($adapter); } @@ -30,7 +43,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) { $adapter->exists('filename')->willReturn(true); $adapter->exists('otherFilename')->willReturn(false); @@ -42,7 +55,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) { $adapter->exists('filename')->shouldBeCalled()->willReturn(true); $adapter->exists('otherFilename')->shouldBeCalled()->willReturn(false); @@ -54,32 +67,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) { $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) { $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) { $adapter->exists('filename')->willReturn(true); $adapter->exists('otherFilename')->willReturn(false); @@ -87,13 +102,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) { $adapter->exists('filename')->willReturn(true); @@ -103,19 +119,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) { $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) { $adapter->exists('filename')->willReturn(false); @@ -123,10 +140,10 @@ function it_should_get_file_object_when_file_does_not_exist_but_can_be_created($ } /** - * @param \spec\Gaufrette\Adapter $extendedAdapter + * @param \spec\Gaufrette\ExtendedAdapter $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 $extendedAdapter, File $file) { $this->beConstructedWith($extendedAdapter); $extendedAdapter->exists('filename')->willReturn(true); @@ -138,7 +155,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) { $adapter->exists('filename')->shouldBeCalled()->willReturn(false); $adapter->write('filename', 'some content to write')->shouldBeCalled()->willReturn(21); @@ -149,7 +166,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) { $adapter->write('filename', 'some content to write')->shouldBeCalled()->willReturn(21); @@ -159,33 +176,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) { $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) { $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) { $adapter->exists('filename')->shouldBeCalled()->willReturn(true); $adapter->read('filename')->shouldBeCalled()->willReturn('Some content'); @@ -196,7 +215,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) { $adapter->exists('filename')->willReturn(false); @@ -208,20 +227,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) { $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) { $adapter->exists('filename')->shouldBeCalled()->willReturn(true); $adapter->delete('filename')->shouldBeCalled()->willReturn(true); @@ -232,34 +252,36 @@ 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) { $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) { $adapter->exists('filename')->willReturn(true); $adapter->delete('filename')->willReturn(false); $this ->shouldThrow(new \RuntimeException('Could not remove the "filename" key.')) - ->duringDelete('filename'); + ->duringDelete('filename') + ; } /** * @param \Gaufrette\Adapter $adapter */ - function it_should_get_all_keys($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); @@ -268,39 +290,39 @@ 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 $adapter) { - $keys = array('filename', 'filename1', 'filename2', 'testKey', 'KeyTest', 'testkey'); + $keys = ['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( - '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('testKey', 'KeyTest'), - 'dirs' => array() - ) + [ + 'keys' => ['KeyTest'], + 'dirs' => [], + ] ); } /** * @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 $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); @@ -312,29 +334,29 @@ function it_should_decide_which_key_is_dir_using_adapter($adapter) $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('testKey') - ) + [ + 'keys' => ['KeyTest'], + 'dirs' => [], + ] ); } /** * @param \Gaufrette\Adapter $adapter */ - function it_should_get_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); @@ -345,19 +367,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) { $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) { $adapter->exists('filename')->shouldBeCalled()->willReturn(true); $adapter->read('filename')->willReturn('some content'); @@ -368,7 +391,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) { $adapter->exists('filename')->shouldBeCalled()->willReturn(false); @@ -378,9 +401,9 @@ function it_should_not_calculate_checksum_of_file_which_does_not_exist($adapter) } /** - * @param \spec\Gaufrette\Adapter $extendedAdapter + * @param \spec\Gaufrette\ExtendedAdapter $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 $extendedAdapter) { $this->beConstructedWith($extendedAdapter); $extendedAdapter->exists('filename')->shouldBeCalled()->willReturn(true); @@ -389,11 +412,24 @@ function it_should_delegate_checksum_calculation_to_adapter_when_adapter_is_chec $this->checksum('filename')->shouldReturn(12); } -} -interface Adapter extends \Gaufrette\Adapter, - \Gaufrette\Adapter\FileFactory, - \Gaufrette\Adapter\StreamFactory, - \Gaufrette\Adapter\ChecksumCalculator, - \Gaufrette\Adapter\MetadataSupporter -{} + /** + * @param \spec\Gaufrette\ExtendedAdapter $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); + $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) + { + $adapter->exists('filename')->willReturn(true); + $this + ->shouldThrow(new \LogicException(sprintf('Adapter "%s" cannot provide MIME type', get_class($adapter->getWrappedObject())))) + ->duringMimeType('filename'); + } +} diff --git a/spec/Gaufrette/Stream/LocalSpec.php b/spec/Gaufrette/Stream/LocalSpec.php new file mode 100644 index 000000000..872045193 --- /dev/null +++ b/spec/Gaufrette/Stream/LocalSpec.php @@ -0,0 +1,29 @@ +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')); + + restore_error_handler(); + } +} 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 59% rename from spec/Gaufrette/StreamWrapper.php rename to spec/Gaufrette/StreamWrapperSpec.php index 7a61f4db2..00eafce40 100644 --- a/spec/Gaufrette/StreamWrapper.php +++ b/spec/Gaufrette/StreamWrapperSpec.php @@ -2,23 +2,27 @@ namespace spec\Gaufrette; -use PHPSpec2\ObjectBehavior; +use Gaufrette\FilesystemMap; +use Gaufrette\Filesystem; +use Gaufrette\Stream; +use PhpSpec\ObjectBehavior; +use Prophecy\Argument; -class StreamWrapper extends ObjectBehavior +class StreamWrapperSpec extends ObjectBehavior { /** * @param \Gaufrette\FilesystemMap $map * @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); $this->setFilesystemMap($map); } - function it_should_be_initializable() + function it_is_initializable() { $this->shouldHaveType('Gaufrette\StreamWrapper'); } @@ -26,28 +30,29 @@ function it_should_be_initializable() /** * @param \Gaufrette\Stream $stream */ - function it_should_open_stream($stream) + function it_opens_stream(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 +60,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) { + $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 +77,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) { + $stream->open(Argument::any())->willReturn(true); $stream->write('some content')->shouldBeCalled()->willReturn(12); $this->stream_open('gaufrette://some/filename', 'w+'); @@ -82,7 +89,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 +98,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) { + $stream->open(Argument::any())->willReturn(true); $stream->close()->shouldBeCalled(); $this->stream_open('gaufrette://some/filename', 'w+'); $this->stream_close(); @@ -101,7 +109,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) { $stream->flush()->shouldNotBeCalled(); $this->stream_flush(); @@ -110,8 +118,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) { + $stream->open(Argument::any())->willReturn(true); $stream->flush()->shouldBeCalled(); $this->stream_open('gaufrette://some/filename', 'w+'); $this->stream_flush(); @@ -120,7 +129,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) { $stream->seek(12, SEEK_SET)->shouldNotBeCalled(); $this->stream_seek(12, SEEK_SET); @@ -129,8 +138,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) { + $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 +149,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) { $stream->tell()->shouldNotBeCalled(); $this->stream_tell(); @@ -148,8 +158,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) { + $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 +169,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) { $stream->eof()->shouldNotBeCalled(); $this->stream_eof(); @@ -167,8 +178,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) { + $stream->open(Argument::any())->willReturn(true); $this->stream_open('gaufrette://some/filename', 'w+'); $stream->eof()->willReturn(false); @@ -178,7 +190,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,23 +198,24 @@ 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 $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); $this->stream_open('gaufrette://some/filename', 'w+'); @@ -212,57 +225,62 @@ function it_should_stat_file_from_stream($stream) /** * @param \Gaufrette\Stream $stream */ - function it_should_stat_from_url($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); $this->url_stat('gaufrette://some/filename', STREAM_URL_STAT_LINK)->shouldReturn($stat); } /** + * @param \Gaufrette\Filesystem $stream * @param \Gaufrette\Stream $stream */ - function it_should_not_stat_when_cannot_open($stream) + function it_stats_even_if_it_cannot_be_open(Filesystem $filesystem, Stream $stream) { - $stream->open(ANY_ARGUMENT)->willThrow(new \RuntimeException); - $this->url_stat('gaufrette://some/filename', STREAM_URL_STAT_LINK)->shouldReturn(false); + $filesystem->createStream('dir/')->willReturn($stream); + $stream->open(Argument::any())->willThrow(new \RuntimeException); + $stream->stat(Argument::any())->willReturn(['mode' => 16893]); + $this->url_stat('gaufrette://some/dir/', STREAM_URL_STAT_LINK)->shouldReturn(['mode' => 16893]); } /** * @param \Gaufrette\Stream $stream */ - function it_should_not_unlink_when_cannot_open($stream) + function it_does_not_unlink_when_cannot_open(Stream $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) { + $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 +288,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) { + $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/Checksum.php deleted file mode 100644 index 27a66e0a1..000000000 --- a/spec/Gaufrette/Util/Checksum.php +++ /dev/null @@ -1,31 +0,0 @@ -fromContent('some content')->shouldReturn(md5('some content')); - } - - function it_should_calculate_checksum_from_filepath() - { - $path = __DIR__.DIRECTORY_SEPARATOR.'testFile'; - $this->fromFile($path)->shouldReturn(md5('some other content')); - } - - function letgo() - { - $path = __DIR__.DIRECTORY_SEPARATOR.'testFile'; - @unlink(__DIR__.DIRECTORY_SEPARATOR.'testFile'); - } -} diff --git a/spec/Gaufrette/Util/ChecksumSpec.php b/spec/Gaufrette/Util/ChecksumSpec.php new file mode 100644 index 000000000..dc4546a13 --- /dev/null +++ b/spec/Gaufrette/Util/ChecksumSpec.php @@ -0,0 +1,37 @@ +getTestFilePath(), 'some other content'); + } + + function letGo() + { + @unlink($this->getTestFilePath()); + } + + function it_calculates_checksum_from_content() + { + $this->fromContent('some content') + ->shouldReturn(md5('some content')) + ; + } + + function it_calculates_checksum_from_filepath() + { + $this->fromFile($this->getTestFilePath()) + ->shouldReturn(md5('some other content')) + ; + } + + private function getTestFilePath(): string + { + return __DIR__ . DIRECTORY_SEPARATOR . 'testFile'; + } +} diff --git a/spec/Gaufrette/Util/Path.php b/spec/Gaufrette/Util/PathSpec.php similarity index 58% rename from spec/Gaufrette/Util/Path.php rename to spec/Gaufrette/Util/PathSpec.php index 46430424e..f47164017 100644 --- a/spec/Gaufrette/Util/Path.php +++ b/spec/Gaufrette/Util/PathSpec.php @@ -2,23 +2,30 @@ 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); + $this->isAbsolute('protocol://home/path')->shouldBe(true); } - 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'); $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'); + } + + function it_returns_unix_style_dirname() + { + $this->dirname('a/test/path')->shouldReturn('a/test'); } } 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.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 deleted file mode 100644 index 2873edfd6..000000000 --- a/src/Gaufrette/Adapter/AclAwareAmazonS3.php +++ /dev/null @@ -1,189 +0,0 @@ - - */ -class AclAwareAmazonS3 implements Adapter, - MetadataSupporter -{ - protected $delegate; - protected $s3; - protected $bucketName; - protected $aclConstant = AmazonClient::ACL_PRIVATE; - protected $users = array(); - - 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 = array(); - - 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 array(); - } - - /** - * {@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 cfd062ad2..000000000 --- a/src/Gaufrette/Adapter/AmazonS3.php +++ /dev/null @@ -1,282 +0,0 @@ - - * @author Leszek Prabucki - */ -class AmazonS3 implements Adapter, - MetadataSupporter -{ - protected $service; - protected $bucket; - protected $ensureBucket = false; - protected $metadata; - protected $options; - - 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), - $options - ); - } - - /** - * 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] : array(); - } - - /** - * {@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( - array( // source - 'bucket' => $this->bucket, - 'filename' => $this->computePath($sourceKey) - ), - array( // 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( - array('acl' => AmazonClient::ACL_PUBLIC), - $this->getMetadata($key), - array('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 = array(); - foreach ($list as $file) { - if ('.' !== dirname($file)) { - $keys[] = dirname($file); - } - $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 4dd9d4e21..000000000 --- a/src/Gaufrette/Adapter/Apc.php +++ /dev/null @@ -1,149 +0,0 @@ - - * @author Antoine Hérault - * @author Leszek Prabucki - */ -class Apc implements Adapter -{ - protected $prefix; - protected $ttl; - - /** - * Constructor - * - * @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 array(); - } - - $keys = array(); - 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 integer $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/AsyncAwsS3.php b/src/Gaufrette/Adapter/AsyncAwsS3.php new file mode 100644 index 000000000..0ee94bb6d --- /dev/null +++ b/src/Gaufrette/Adapter/AsyncAwsS3.php @@ -0,0 +1,342 @@ + + * @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 $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, + 'CreateBucketConfiguration' => [ + 'LocationConstraint' => $this->service->getConfiguration()->get(Configuration::OPTION_REGION), + ], + ]); + $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/src/Gaufrette/Adapter/AwsS3.php b/src/Gaufrette/Adapter/AwsS3.php new file mode 100644 index 000000000..2d335a1c6 --- /dev/null +++ b/src/Gaufrette/Adapter/AwsS3.php @@ -0,0 +1,346 @@ + + */ +class AwsS3 implements Adapter, MetadataSupporter, ListKeysAware, SizeCalculator, MimeTypeProvider +{ + /** @var S3Client */ + protected $service; + /** @var string */ + protected $bucket; + /** @var array */ + protected $options; + /** @var bool */ + protected $bucketExists; + /** @var array */ + protected $metadata = []; + /** @var bool */ + protected $detectContentType; + + /** + * @param S3Client $service + * @param string $bucket + * @param array $options + * @param bool $detectContentType + */ + 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( + [ + 'create' => false, + 'directory' => '', + 'acl' => 'private', + ], + $options + ); + + $this->detectContentType = $detectContentType; + } + + /** + * {@inheritdoc} + */ + public function setMetadata($key, $metadata) + { + // BC with AmazonS3 adapter + if (isset($metadata['contentType'])) { + $metadata['ContentType'] = $metadata['contentType']; + unset($metadata['contentType']); + } + + $this->metadata[$key] = $metadata; + } + + /** + * {@inheritdoc} + */ + public function getMetadata($key) + { + return $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->get('ContentType'); + + return (string) $object->get('Body'); + } 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} + */ + public function write($key, $content) + { + $this->ensureBucketExists(); + $options = $this->getOptions($key, ['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']) && $this->detectContentType) { + $options['ContentType'] = $this->guessContentType($content); + } + + try { + $this->service->putObject($options); + + if (is_resource($content)) { + return Util\Size::fromResource($content); + } + + return Util\Size::fromContent($content); + } catch (\Exception $e) { + return false; + } + } + + /** + * {@inheritdoc} + */ + public function exists($key) + { + return $this->service->doesObjectExist($this->bucket, $this->computePath($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 size($key) + { + try { + $result = $this->service->headObject($this->getOptions($key)); + + return $result['ContentLength']; + } catch (\Exception $e) { + return false; + } + } + + /** + * {@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 = []; + $iter = $this->service->getIterator('ListObjects', $options); + foreach ($iter as $file) { + $keys[] = $this->computeKey($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([ + 'Bucket' => $this->bucket, + '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 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->doesBucketExist($this->bucket)) { + 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, + 'LocationConstraint' => $this->service->getRegion(), + ]); + $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 $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); + } + + 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 new file mode 100644 index 000000000..c153f9c01 --- /dev/null +++ b/src/Gaufrette/Adapter/AzureBlobStorage.php @@ -0,0 +1,598 @@ + + * @author Paweł Czyżewski + */ +class AzureBlobStorage implements Adapter, MetadataSupporter, SizeCalculator, ChecksumCalculator, MimeTypeProvider +{ + /** + * Error constants. + */ + const ERROR_CONTAINER_ALREADY_EXISTS = 'ContainerAlreadyExists'; + const ERROR_CONTAINER_NOT_FOUND = 'ContainerNotFound'; + + /** + * @var AzureBlobStorage\BlobProxyFactoryInterface + */ + protected $blobProxyFactory; + + /** + * @var string + */ + protected $containerName; + + /** + * @var bool + */ + protected $detectContentType; + + /** + * @var \MicrosoftAzure\Storage\Blob\Internal\IBlob + */ + protected $blobProxy; + + /** + * @var bool + */ + protected $multiContainerMode = false; + + /** + * @var CreateContainerOptions + */ + protected $createContainerOptions; + + /** + * @param AzureBlobStorage\BlobProxyFactoryInterface $blobProxyFactory + * @param string|null $containerName + * @param bool $create + * @param bool $detectContentType + * + * @throws \RuntimeException + */ + public function __construct(BlobProxyFactoryInterface $blobProxyFactory, $containerName = null, $create = false, $detectContentType = true) + { + $this->blobProxyFactory = $blobProxyFactory; + $this->containerName = $containerName; + $this->detectContentType = $detectContentType; + 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. + * + * @param string $containerName + * @param \MicrosoftAzure\Storage\Blob\Models\CreateContainerOptions $options + * + * @throws \RuntimeException if cannot create the container + */ + public function createContainer($containerName, CreateContainerOptions $options = null) + { + $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) { + throw new \RuntimeException(sprintf( + 'Failed to create the configured container "%s": %s (%s).', + $containerName, + $e->getErrorText(), + $errorCode + )); + } + } + } + + /** + * Deletes a container. + * + * @param string $containerName + * @param BlobServiceOptions $options + * + * @throws \RuntimeException if cannot delete the container + */ + public function deleteContainer($containerName, BlobServiceOptions $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} + * @throws \RuntimeException + * @throws \InvalidArgumentException + */ + public function read($key) + { + $this->init(); + list($containerName, $key) = $this->tokenizeKey($key); + + try { + $blob = $this->blobProxy->getBlob($containerName, $key); + + return stream_get_contents($blob->getContentStream()); + } catch (ServiceException $e) { + $this->failIfContainerNotFound($e, sprintf('read key "%s"', $key), $containerName); + + return false; + } + } + + /** + * {@inheritdoc} + * @throws \RuntimeException + * @throws \InvalidArgumentException + */ + public function write($key, $content) + { + $this->init(); + list($containerName, $key) = $this->tokenizeKey($key); + + 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); + + $options->setContentType($contentType); + } + + $size = is_resource($content) + ? Util\Size::fromResource($content) + : Util\Size::fromContent($content) + ; + + try { + if ($this->multiContainerMode) { + $this->createContainer($containerName); + } + + $this->blobProxy->createBlockBlob($containerName, $key, $content, $options); + } catch (ServiceException $e) { + $this->failIfContainerNotFound($e, sprintf('write content for key "%s"', $key), $containerName); + + return false; + } + + return $size; + } + + /** + * {@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($containerName, $listBlobsOptions); + + foreach ($blobsList->getBlobs() as $blob) { + if ($key === $blob->getName()) { + return true; + } + } + } catch (ServiceException $e) { + $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, + $containerName, + $e->getErrorText(), + $errorCode + ), $e->getCode()); + } + + return false; + } + + /** + * {@inheritdoc} + * @throws \RuntimeException + */ + public function keys() + { + $this->init(); + + try { + 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->containerName); + $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} + * @throws \RuntimeException + * @throws \InvalidArgumentException + */ + public function mtime($key) + { + $this->init(); + list($containerName, $key) = $this->tokenizeKey($key); + + try { + $properties = $this->blobProxy->getBlobProperties($containerName, $key); + + return $properties->getProperties()->getLastModified()->getTimestamp(); + } catch (ServiceException $e) { + $this->failIfContainerNotFound($e, sprintf('read mtime for key "%s"', $key), $containerName); + + return false; + } + } + + /** + * {@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} + */ + 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} + */ + 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 + * @throws \InvalidArgumentException + */ + public function delete($key) + { + $this->init(); + list($containerName, $key) = $this->tokenizeKey($key); + + try { + $this->blobProxy->deleteBlob($containerName, $key); + + return true; + } catch (ServiceException $e) { + $this->failIfContainerNotFound($e, sprintf('delete key "%s"', $key), $containerName); + + return false; + } + } + + /** + * {@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 { + 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), $sourceContainerName); + + return false; + } + } + + /** + * {@inheritdoc} + */ + public function isDirectory($key) + { + // Windows Azure Blob Storage does not support directories + return false; + } + + /** + * {@inheritdoc} + * @throws \RuntimeException + * @throws \InvalidArgumentException + */ + public function setMetadata($key, $content) + { + $this->init(); + list($containerName, $key) = $this->tokenizeKey($key); + + try { + $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, + $containerName, + $e->getErrorText(), + $errorCode + ), $e->getCode()); + } + } + + /** + * {@inheritdoc} + * @throws \RuntimeException + * @throws \InvalidArgumentException + */ + public function getMetadata($key) + { + $this->init(); + list($containerName, $key) = $this->tokenizeKey($key); + + try { + $properties = $this->blobProxy->getBlobProperties($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, + $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 = $this->blobProxyFactory->create(); + } + } + + /** + * Throws a runtime exception if a give ServiceException derived from a "container not found" error. + * + * @param ServiceException $exception + * @param string $action + * @param string $containerName + * + * @throws \RuntimeException + */ + protected function failIfContainerNotFound(ServiceException $exception, $action, $containerName) + { + $errorCode = $this->getErrorCodeFromServiceException($exception); + + if ($errorCode === self::ERROR_CONTAINER_NOT_FOUND) { + throw new \RuntimeException(sprintf( + 'Failed to %s: container "%s" not found.', + $action, + $containerName + ), $exception->getCode()); + } + } + + /** + * Extracts the error code from a service exception. + * + * @param ServiceException $exception + * + * @return string + */ + protected function getErrorCodeFromServiceException(ServiceException $exception) + { + $xml = @simplexml_load_string($exception->getResponse()->getBody()); + + if ($xml && isset($xml->Code)) { + return (string) $xml->Code; + } + + return $exception->getErrorText(); + } + + /** + * @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); + } + + /** + * @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/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php b/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php new file mode 100644 index 000000000..441efae4e --- /dev/null +++ b/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactory.php @@ -0,0 +1,43 @@ + + */ +class BlobProxyFactory implements BlobProxyFactoryInterface +{ + /** + * @var string + */ + protected $connectionString; + + /** + * @param string $connectionString + */ + public function __construct($connectionString) + { + 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; + } + + /** + * {@inheritdoc} + */ + public function create() + { + if (class_exists(ServicesBuilder::class)) { + // for microsoft/azure-storage < 1.0 + return ServicesBuilder::getInstance()->createBlobService($this->connectionString); + } + + return BlobRestProxy::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..852f75ff4 --- /dev/null +++ b/src/Gaufrette/Adapter/AzureBlobStorage/BlobProxyFactoryInterface.php @@ -0,0 +1,18 @@ + + */ +interface BlobProxyFactoryInterface +{ + /** + * Creates a new instance of the Blob proxy. + * + * @return \MicrosoftAzure\Storage\Blob\Internal\IBlob + */ + public function create(); +} diff --git a/src/Gaufrette/Adapter/Cache.php b/src/Gaufrette/Adapter/Cache.php deleted file mode 100644 index 67384655b..000000000 --- a/src/Gaufrette/Adapter/Cache.php +++ /dev/null @@ -1,230 +0,0 @@ - - */ -class Cache implements Adapter, - MetadataSupporter -{ - /** - * @var Adapter - */ - protected $source; - - /** - * @var Adapter - */ - protected $cache; - - /** - * @var integer - */ - protected $ttl; - - /** - * @var 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 - * @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 integer $ttl - */ - public function getTtl() - { - return $this->ttl; - } - - /** - * Defines the time to live of the cache - * - * @param integer $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) - { - $this->source->rename($key, $new); - - return $this->cache->rename($key, $new); - } - - /** - * {@inheritDoc} - */ - public function write($key, $content, array $metadata = null) - { - $this->source->write($key, $content); - - return $this->cache->write($key, $content); - } - - /** - * {@inheritDoc} - */ - public function exists($key) - { - return $this->source->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); - - if (time() - $this->ttl >= $dateCache) { - $dateSource = $this->source->mtime($key); - $needsReload = $dateCache < $dateSource; - } else { - $needsReload = false; - } - } 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/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 27664f944..77066d2fb 100644 --- a/src/Gaufrette/Adapter/DoctrineDbal.php +++ b/src/Gaufrette/Adapter/DoctrineDbal.php @@ -2,74 +2,80 @@ namespace Gaufrette\Adapter; +use Doctrine\DBAL\Result; use Gaufrette\Adapter; use Gaufrette\Util; - use Doctrine\DBAL\Connection; /** - * Doctrine DBAL adapter + * Doctrine DBAL adapter. * * @author Markus Bachmann * @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( - 'key' => 'key', - 'content' => 'content', - 'mtime' => 'mtime', + protected $columns = [ + 'key' => 'key', + 'content' => 'content', + 'mtime' => 'mtime', 'checksum' => 'checksum', - ); + ]; /** - * Constructor - * * @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 = []) { + 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); + $this->table = $table; + $this->columns = array_replace($this->columns, $columns); } /** - * {@inheritDoc} + * {@inheritdoc} */ public function keys() { - $keys = array(); + $keys = []; $stmt = $this->connection->executeQuery(sprintf( 'SELECT %s FROM %s', $this->getQuotedColumn('key'), $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); } /** - * {@inheritDoc} + * {@inheritdoc} */ 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] ); } /** - * {@inheritDoc} + * {@inheritdoc} */ public function mtime($key) { @@ -77,7 +83,7 @@ public function mtime($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function checksum($key) { @@ -85,23 +91,28 @@ public function checksum($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ 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'), $this->getQuotedTable(), $this->getQuotedColumn('key') ), - array('key' => $key) + ['key' => $key] ); } /** - * {@inheritDoc} + * {@inheritdoc} */ public function read($key) { @@ -109,32 +120,32 @@ public function read($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function delete($key) { return (boolean) $this->connection->delete( $this->table, - array($this->getQuotedColumn('key') => $key) + [$this->getQuotedColumn('key') => $key] ); } /** - * {@inheritDoc} + * {@inheritdoc} */ public function write($key, $content) { - $values = array( - $this->getQuotedColumn('content') => $content, - $this->getQuotedColumn('mtime') => time(), + $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; @@ -145,7 +156,7 @@ public function write($key, $content) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function isDirectory($key) { @@ -154,43 +165,55 @@ 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), $this->getQuotedTable(), $this->getQuotedColumn('key') ), - array('key' => $key) + ['key' => $key] ); return $value; } /** - * {@inheritDoc} + * {@inheritdoc} */ 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'), $this->getQuotedTable(), $this->getQuotedColumn('key') ), - array('pattern' => sprintf('%s%%', $prefix)) + ['pattern' => sprintf('%s%%', $prefix)] ); - return array( - 'dirs' => array(), - 'keys' => array_map(function ($value) { + return [ + 'dirs' => [], + 'keys' => array_map( + function ($value) { return $value['_key']; }, - $keys) - ); + $keys + ), + ]; } private function getQuotedTable() diff --git a/src/Gaufrette/Adapter/Dropbox.php b/src/Gaufrette/Adapter/Dropbox.php deleted file mode 100644 index 4acd52944..000000000 --- a/src/Gaufrette/Adapter/Dropbox.php +++ /dev/null @@ -1,180 +0,0 @@ - - * @author Antoine Hérault - * @author Leszek Prabucki - */ -class Dropbox implements Adapter -{ - protected $client; - - /** - * Constructor - * - * @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 array(); - } - - $keys = array(); - foreach ($metadata['contents'] as $value) { - $file = ltrim($value['path'], '/'); - $keys[] = $file; - if ('.' !== dirname($file)) { - $keys[] = dirname($file); - } - } - 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/FileFactory.php b/src/Gaufrette/Adapter/FileFactory.php index 0eb93492e..f71be89f0 100644 --- a/src/Gaufrette/Adapter/FileFactory.php +++ b/src/Gaufrette/Adapter/FileFactory.php @@ -2,17 +2,18 @@ namespace Gaufrette\Adapter; +use Gaufrette\File; 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/Flysystem.php b/src/Gaufrette/Adapter/Flysystem.php new file mode 100644 index 000000000..b4038e5ce --- /dev/null +++ b/src/Gaufrette/Adapter/Flysystem.php @@ -0,0 +1,125 @@ +adapter = $adapter; + $this->config = Util::ensureConfig($config); + } + + /** + * {@inheritdoc} + */ + public function read($key) + { + return $this->adapter->read($key)['contents']; + } + + /** + * {@inheritdoc} + */ + public function write($key, $content) + { + return $this->adapter->write($key, $content, $this->config); + } + + /** + * {@inheritdoc} + */ + public function exists($key) + { + return (bool) $this->adapter->has($key); + } + + /** + * {@inheritdoc} + */ + public function keys() + { + return array_map(function ($content) { + return $content['path']; + }, $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} + */ + 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) + { + throw new UnsupportedAdapterMethodException('isDirectory is not supported by this adapter.'); + } +} diff --git a/src/Gaufrette/Adapter/Ftp.php b/src/Gaufrette/Adapter/Ftp.php index 90e418cac..8ea3bb141 100644 --- a/src/Gaufrette/Adapter/Ftp.php +++ b/src/Gaufrette/Adapter/Ftp.php @@ -5,17 +5,15 @@ 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, - FileFactory +class Ftp implements Adapter, FileFactory, ListKeysAware, SizeCalculator { + /** @var null|resource|\FTP\Connection */ protected $connection = null; protected $directory; protected $host; @@ -25,29 +23,37 @@ class Ftp implements Adapter, protected $passive; protected $create; protected $mode; - protected $fileData = array(); + protected $ssl; + protected $timeout; + protected $fileData = []; + protected $utf8; /** - * 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 */ - 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.'); + } + $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->host = $host; + $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; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function read($key) { @@ -67,14 +73,14 @@ public function read($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function write($key, $content) { $this->ensureDirectoryExists($this->directory, $this->create); $path = $this->computePath($key); - $directory = dirname($path); + $directory = \Gaufrette\Util\Path::dirname($path); $this->ensureDirectoryExists($directory, true); @@ -94,7 +100,7 @@ public function write($key, $content) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function rename($sourceKey, $targetKey) { @@ -103,36 +109,78 @@ 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 ftp_rename($this->getConnection(), $sourcePath, $targetPath); } /** - * {@inheritDoc} + * {@inheritdoc} */ public function exists($key) { $this->ensureDirectoryExists($this->directory, $this->create); - $file = $this->computePath($key); - $items = ftp_nlist($this->getConnection(), dirname($file)); + $file = $this->computePath($key); + $lines = ftp_rawlist($this->getConnection(), '-al ' . \Gaufrette\Util\Path::dirname($file)); + + if (false === $lines) { + return false; + } - return $items && (in_array($file, $items) || in_array(basename($file), $items)); + $pattern = '{(?) ' . preg_quote(basename($file)) . '( -> |$)}m'; + foreach ($lines as $line) { + if (preg_match($pattern, $line)) { + return true; + } + } + + return false; } /** - * {@inheritDoc} + * {@inheritdoc} */ 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 = []; + foreach (['keys', 'dirs'] as $hash) { + $filteredKeys[$hash] = []; + foreach ($keys[$hash] as $key) { + if (0 === strpos($key, $prefix)) { + $filteredKeys[$hash][] = $key; + } + } + } + + return $filteredKeys; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function mtime($key) { @@ -149,7 +197,7 @@ public function mtime($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function delete($key) { @@ -163,7 +211,7 @@ public function delete($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function isDirectory($key) { @@ -187,22 +235,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( - 'name' => $itemData['name'], - 'path' => trim(($directory ? $directory . '/' : '') . $itemData['name'], '/'), - 'time' => $itemData['time'], - 'size' => $itemData['size'], - ); + $item = [ + 'name' => $itemData['name'], + 'path' => trim(($directory ? $directory . '/' : '') . $itemData['name'], '/'), + 'time' => $itemData['time'], + 'size' => $itemData['size'], + ]; if ('-' === substr($itemData['perms'], 0, 1)) { $fileData[$item['path']] = $item; @@ -213,14 +260,14 @@ public function listDirectory($directory = '') $this->fileData = array_merge($fileData, $this->fileData); - return array( - 'keys' => array_keys($fileData), - 'dirs' => $dirs - ); + return [ + 'keys' => array_keys($fileData), + 'dirs' => $dirs, + ]; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function createFile($key, Filesystem $filesystem) { @@ -229,7 +276,8 @@ public function createFile($key, Filesystem $filesystem) $file = new File($key, $filesystem); if (!array_key_exists($key, $this->fileData)) { - $directory = dirname($key) == '.' ? '' : dirname($key); + $dirname = \Gaufrette\Util\Path::dirname($key); + $directory = $dirname == '.' ? '' : $dirname; $this->listDirectory($directory); } @@ -243,13 +291,31 @@ 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 + * 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 @@ -266,7 +332,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 * @@ -275,7 +341,7 @@ protected function ensureDirectoryExists($directory, $create = false) protected function createDirectory($directory) { // create parent directory if needed - $parent = dirname($directory); + $parent = \Gaufrette\Util\Path::dirname($directory); if (!$this->isDir($parent)) { $this->createDirectory($parent); } @@ -288,8 +354,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) { @@ -307,25 +374,68 @@ 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); - $keys = $items['dirs']; - foreach ($items['dirs'] as $dir) { - $keys = array_merge($keys, $this->fetchKeys($dir)); + if (false === $lines) { + return ['keys' => [], 'dirs' => []]; } - return array_merge($items['keys'], $keys); + $regexDir = '/' . preg_quote($this->directory . $directory, '/') . '\/?(.+):$/u'; + $regexItem = '/^(?:([d\-\d])\S+)\s+\S+(?:(?:\s+\S+){5})?\s+(\S+)\s+(.+?)$/'; + + $prevLine = null; + $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 = [ + 'keys' => array_merge($keys['keys'], $keys['dirs']), + 'dirs' => [], + ]; + } + } 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 = [ + 'keys' => array_merge($keys['keys'], $keys['dirs']), + 'dirs' => [], + ]; + } + + foreach (array_keys($directories) as $directory) { + $keys = array_merge_recursive($keys, $this->fetchKeys($directory, $onlyKeys)); + } + + return $keys; } /** - * Parses the given raw list + * Parses the given raw list. * * @param array $rawlist * @@ -333,30 +443,30 @@ private function fetchKeys($directory = '') */ 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]), - 'name' => $infos[8] - ); + 'num' => $infos[1], + 'size' => $infos[4], + 'time' => strtotime($infos[5] . ' ' . $infos[6] . '. ' . $infos[7]), + 'name' => $infos[8], + ]; } - } else { + } 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]), - 'name' => $infos[3] - ); + 'num' => '', + 'size' => $isDir ? '' : $infos[2], + 'time' => strtotime($infos[0] . ' ' . $infos[1]), + 'name' => $infos[3], + ]; } } @@ -364,7 +474,7 @@ private function parseRawlist(array $rawlist) } /** - * Computes the path for the given key + * Computes the path for the given key. * * @param string $key */ @@ -374,20 +484,24 @@ private function computePath($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() { + if (class_exists('\FTP\Connection')) { + return $this->connection instanceof \FTP\Connection; + } + return is_resource($this->connection); } /** * 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 + * @return resource|\FTP\Connection The ftp connection */ private function getConnection() { @@ -399,74 +513,82 @@ private function getConnection() } /** - * Opens the adapter's ftp connection + * Opens the adapter's ftp connection. * * @throws RuntimeException if could not connect */ private function connect() { + if ($this->ssl && !function_exists('ftp_ssl_connect')) { + throw new \RuntimeException('This Server Has No SSL-FTP Available.'); + } + // open ftp connection - $this->connection = ftp_connect($this->host, $this->port); + if (!$this->ssl) { + $this->connection = ftp_connect($this->host, $this->port, $this->timeout); + } else { + $this->connection = ftp_ssl_connect($this->host, $this->port, $this->timeout); + } + if (!$this->connection) { throw new \RuntimeException(sprintf('Could not connect to \'%s\' (port: %s).', $this->host, $this->port)); } - $username = $this->username ? : 'anonymous'; - $password = $this->password ? : ''; + if (defined('FTP_USEPASVADDRESS')) { + ftp_set_option($this->connection, FTP_USEPASVADDRESS, false); + } + + $username = $this->username ?: 'anonymous'; + $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)); } // 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'); + } + // ensure the adapter's directory exists if ('/' !== $this->directory) { try { $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)); } } } /** - * Closes the adapter's ftp connection + * Closes the adapter's ftp connection. */ - private function close() + public function close() { if ($this->isConnected()) { ftp_close($this->connection); } } - /** - * 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; diff --git a/src/Gaufrette/Adapter/GoogleCloudStorage.php b/src/Gaufrette/Adapter/GoogleCloudStorage.php new file mode 100644 index 000000000..295dcad14 --- /dev/null +++ b/src/Gaufrette/Adapter/GoogleCloudStorage.php @@ -0,0 +1,447 @@ + + */ +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 = [ + self::OPTION_CREATE_BUCKET_IF_NOT_EXISTS => false, + self::OPTION_STORAGE_CLASS => 'STANDARD', + 'directory' => '', + 'acl' => 'private', + ]; + protected $bucketExists; + protected $metadata = []; + protected $detectContentType; + + /** + * @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( + Storage $service, + $bucket, + array $options = [], + $detectContentType = false + ) { + 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( + $this->options, + $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->bucketExists = null; + $this->bucket = $bucket; + } + + /** + * {@inheritdoc} + */ + public function read($key) + { + $this->ensureBucketExists(); + $path = $this->computePath($key); + + $object = $this->getObjectData($path); + if ($object === false) { + return false; + } + + 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()); + + 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; + } + + /** + * {@inheritdoc} + */ + public function write($key, $content) + { + $this->ensureBucketExists(); + $path = $this->computePath($key); + + $metadata = $this->getMetadata($key); + $options = [ + '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) { + $options['mimeType'] = $this->guessContentType($content); + unset($metadata['ContentType']); + } elseif (isset($metadata['ContentType'])) { + $options['mimeType'] = $metadata['ContentType']; + unset($metadata['ContentType']); + } + + $object = new 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 { + $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 (ServiceException $e) { + return false; + } + } + + /** + * {@inheritdoc} + */ + public function exists($key) + { + $this->ensureBucketExists(); + $path = $this->computePath($key); + + try { + $this->service->objects->get($this->bucket, $path); + } catch (ServiceException $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 (ServiceException $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->service->objects->delete($this->bucket, $sourcePath); + } catch (ServiceException $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 = []; + 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 = []; + + // 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 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 $this->metadata[$path] ?? []; + } + + /** + * 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 (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( + 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|StorageObject + */ + private function getObjectData($path, $options = []) + { + try { + return $this->service->objects->get($this->bucket, $path, $options); + } catch (ServiceException $e) { + 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); + } +} diff --git a/src/Gaufrette/Adapter/GridFS.php b/src/Gaufrette/Adapter/GridFS.php index e8e8c3532..de9bee6d0 100644 --- a/src/Gaufrette/Adapter/GridFS.php +++ b/src/Gaufrette/Adapter/GridFS.php @@ -3,62 +3,72 @@ 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 + * Adapter for the GridFS filesystem on MongoDB database. * * @author Tomi Saarinen * @author Antoine Hérault * @author Leszek Prabucki */ -class GridFS implements Adapter, - ChecksumCalculator, - MetadataSupporter, - ListKeysAware +class GridFS implements Adapter, ChecksumCalculator, MetadataSupporter, ListKeysAware, SizeCalculator { - private $metadata = array(); - protected $gridFS = null; + /** @var array */ + private $metadata = []; + + /** @var Bucket */ + private $bucket; /** - * Constructor - * - * @param \MongoGridFS $gridFS + * @param Bucket $bucket */ - public function __construct(MongoGridFs $gridFS) + public function __construct(Bucket $bucket) { - $this->gridFS = $gridFS; + if (!class_exists(Bucket::class)) { + throw new \LogicException('You need to install package "mongodb/mongodb" to use this adapter'); + } + $this->bucket = $bucket; } /** - * {@inheritDoc} + * {@inheritdoc} */ 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); + } } /** - * {@inheritDoc} + * {@inheritdoc} */ 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; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function isDirectory($key) { @@ -66,71 +76,85 @@ public function isDirectory($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ 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; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function exists($key) { - return (boolean) $this->find($key); + return (boolean) $this->bucket->findOne(['filename' => $key]); } /** - * {@inheritDoc} + * {@inheritdoc} */ 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; } /** - * {@inheritDoc} + * {@inheritdoc} */ 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; } /** - * {@inheritDoc} + * {@inheritdoc} */ 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; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function delete($key) { - $file = $this->find($key, array('_id')); + if (null === $file = $this->bucket->findOne(['filename' => $key], ['projection' => ['_id' => 1]])) { + return false; + } - return $file && $this->gridFS->delete($file->file['_id']); + $this->bucket->delete($file['_id']); + + return true; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function setMetadata($key, $metadata) { @@ -138,45 +162,62 @@ public function setMetadata($key, $metadata) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function getMetadata($key) { - return isset($this->metadata[$key]) ? $this->metadata[$key] : array(); - } + if (isset($this->metadata[$key])) { + return $this->metadata[$key]; + } + $meta = $this->bucket->findOne(['filename' => $key], ['projection' => ['metadata' => 1,'_id' => 0]]); - private function find($key, array $fields = array()) - { - return $this->gridFS->findOne($key, $fields); + if ($meta === null || !isset($meta['metadata'])) { + return []; + } + + $this->metadata[$key] = iterator_to_array($meta['metadata']); + + return $this->metadata[$key]; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function listKeys($prefix = '') { $prefix = trim($prefix); - if ('' == $prefix) { - return array( - 'dirs' => array(), - 'keys' => $this->keys() - ); + 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; } + + 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/src/Gaufrette/Adapter/InMemory.php b/src/Gaufrette/Adapter/InMemory.php index e232b745d..b58cdcb42 100644 --- a/src/Gaufrette/Adapter/InMemory.php +++ b/src/Gaufrette/Adapter/InMemory.php @@ -6,55 +6,52 @@ 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 +class InMemory implements Adapter, MimeTypeProvider { - protected $files = array(); + protected $files = []; /** - * Constructor - * * @param array $files An array of files */ - public function __construct(array $files = array()) + public function __construct(array $files = []) { $this->setFiles($files); } /** - * Defines the files + * Defines the files. * * @param array $files An array of files */ 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( - 'content' => null, - 'mtime' => null, - ), $file); + $file = array_merge([ + 'content' => null, + 'mtime' => null, + ], $file); $this->setFile($key, $file['content'], $file['mtime']); } } /** - * 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) { @@ -62,14 +59,14 @@ public function setFile($key, $content = null, $mtime = null) $mtime = time(); } - $this->files[$key] = array( - 'content' => (string) $content, - 'mtime' => (integer) $mtime - ); + $this->files[$key] = [ + 'content' => (string) $content, + 'mtime' => (integer) $mtime, + ]; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function read($key) { @@ -77,7 +74,7 @@ public function read($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function rename($sourceKey, $targetKey) { @@ -88,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) { @@ -107,7 +104,7 @@ public function exists($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function keys() { @@ -115,15 +112,15 @@ public function keys() } /** - * {@inheritDoc} + * {@inheritdoc} */ public function mtime($key) { - return isset($this->files[$key]['mtime']) ? $this->files[$key]['mtime'] : false; + return $this->files[$key]['mtime'] ?? false; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function delete($key) { @@ -134,10 +131,20 @@ public function delete($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function isDirectory($path) { return false; } + + /** + * {@inheritdoc} + */ + public function mimeType($key) + { + $fileInfo = new \finfo(FILEINFO_MIME_TYPE); + + return $fileInfo->buffer($this->files[$key]['content']); + } } 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/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 2de7f1eb2..81b253f8a 100644 --- a/src/Gaufrette/Adapter/Local.php +++ b/src/Gaufrette/Adapter/Local.php @@ -5,33 +5,29 @@ 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 */ -class Local implements Adapter, - StreamFactory, - ChecksumCalculator +class Local implements Adapter, StreamFactory, ChecksumCalculator, SizeCalculator, MimeTypeProvider { protected $directory; private $create; + private $mode; /** - * Constructor + * @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 * - * @param string $directory Directory where the filesystem is located - * @param boolean $create Whether to create the directory if it does not - * exist (default FALSE) - * - * @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) + public function __construct($directory, $create = false, $mode = 0777) { $this->directory = Util\Path::normalize($directory); @@ -40,71 +36,89 @@ public function __construct($directory, $create = false) } $this->create = $create; + $this->mode = $mode; } /** - * {@inheritDoc} + * {@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) { + if ($this->isDirectory($key)) { + return false; + } + return file_get_contents($this->computePath($key)); } /** - * {@inheritDoc} + * {@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) { $path = $this->computePath($key); - $this->ensureDirectoryExists(dirname($path), true); + $this->ensureDirectoryExists(\Gaufrette\Util\Path::dirname($path), true); return file_put_contents($path, $content); } /** - * {@inheritDoc} + * {@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) { $targetPath = $this->computePath($targetKey); - $this->ensureDirectoryExists(dirname($targetPath), true); + $this->ensureDirectoryExists(\Gaufrette\Util\Path::dirname($targetPath), true); return rename($this->computePath($sourceKey), $targetPath); } /** - * {@inheritDoc} + * {@inheritdoc} */ public function exists($key) { - return file_exists($this->computePath($key)); + return is_file($this->computePath($key)); } /** - * {@inheritDoc} + * {@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() { $this->ensureDirectoryExists($this->directory, $this->create); try { - $iterator = new \RecursiveIteratorIterator( + $files = new \RecursiveIteratorIterator( new \RecursiveDirectoryIterator( $this->directory, \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS - ) + ), + \RecursiveIteratorIterator::CHILD_FIRST ); } catch (\Exception $e) { - $iterator = new \EmptyIterator; + $files = new \EmptyIterator(); } - $files = iterator_to_array($iterator); - $keys = array(); + $keys = []; foreach ($files as $file) { - $keys[] = $key = $this->computeKey($file); - if ('.' !== dirname($key)) { - $keys[] = dirname($key); - } + $keys[] = $this->computeKey($file); } sort($keys); @@ -112,7 +126,11 @@ public function keys() } /** - * {@inheritDoc} + * {@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,20 +138,30 @@ public function mtime($key) } /** - * {@inheritDoc} + * {@inheritdoc} + * + * 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; } /** - * @param string $key - * @return boolean + * @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) { @@ -141,24 +169,64 @@ public function isDirectory($key) } /** - * {@inheritDoc} + * {@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) { - return new Stream\Local($this->computePath($key)); + return new Stream\Local($this->computePath($key), $this->mode); } + /** + * {@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) { return Util\Checksum::fromFile($this->computePath($key)); } /** - * Computes the key from the specified path + * {@inheritdoc} * - * @param string $path + * @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) + { + return Util\Size::fromFile($this->computePath($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) + { + $fileInfo = new \finfo(FILEINFO_MIME_TYPE); + + return $fileInfo->file($this->computePath($key)); + } + + /** + * Computes the key from the specified path. * - * return string + * @param $path + * @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) { @@ -168,15 +236,15 @@ 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 * * @return string A path * - * @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) { @@ -186,11 +254,13 @@ protected function computePath($key) } /** - * Normalizes the given path + * Normalizes the given path. * * @param string $path * * @return string + * @throws \OutOfBoundsException If the computed path is out of the + * directory */ protected function normalizePath($path) { @@ -204,13 +274,14 @@ 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 + * @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) @@ -225,21 +296,58 @@ 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 * - * @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) { - $umask = umask(0); - $created = mkdir($directory, 0777, true); - umask($umask); - - if (!$created) { + if (!@mkdir($directory, $this->mode, true) && !is_dir($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/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 new file mode 100644 index 000000000..16b1fc88a --- /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/Adapter/MogileFS.php b/src/Gaufrette/Adapter/MogileFS.php deleted file mode 100644 index 46506b368..000000000 --- a/src/Gaufrette/Adapter/MogileFS.php +++ /dev/null @@ -1,357 +0,0 @@ - - * @author Antoine Hérault - * - * 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; - - /** - * Constructor - * - * @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) { - foreach ($paths as $path) { - $fh = fopen($path, 'r'); - - if (!$fh) { - continue; - } - - while (!feof($fh)) { - $data .= fread($fh, 8192); - } - - fclose($fh); - } - } - - return $data ?: false; - } - - /** - * {@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'])); - - 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 = array("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', array('key' => $key)); - - return true; - } - - /** - * {@inheritDoc} - */ - public function rename($sourceKey, $targetKey) - { - $this->doRequest('RENAME', array( - '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 array(); - } - - 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 = array(); - - 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']; - } - - $domains[] = array('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 boolean - */ - 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 = array()) - { - 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", array("key" => $key)); - unset($res['paths']); - - return $res; - } - - /** - * Sends file to MogileFS tracker - * - * @param path Save path at server - * @param data Data to save - * @return boolean - */ - 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; - } -} diff --git a/src/Gaufrette/Adapter/OpenCloud.php b/src/Gaufrette/Adapter/OpenCloud.php deleted file mode 100644 index e3e604200..000000000 --- a/src/Gaufrette/Adapter/OpenCloud.php +++ /dev/null @@ -1,225 +0,0 @@ - - */ -class OpenCloud implements Adapter, - ChecksumCalculator -{ - /** - * @var ObjectStore - */ - protected $objectStore; - /** - * @var string - */ - protected $containerName; - /** - * @var bool - */ - protected $createContainer; - /** - * @var bool - */ - protected $detectContentType; - /** - * @var Container - */ - protected $container; - - public function __construct(ObjectStore $objectStore, $containerName, $createContainer = false, $detectContentType = true) - { - $this->objectStore = $objectStore; - $this->containerName = $containerName; - $this->createContainer = $createContainer; - $this->detectContentType = $detectContentType; - } - - private function initialize() - { - 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); - } - $this->container = $container; - } - } - - /** - * Reads the content of the file - * - * @param string $key - * - * @return string|boolean if cannot read content - */ - public function read($key) - { - $this->initialize(); - return $this->tryGetObject($key)->SaveToString(); - } - - /** - * 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 - */ - 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; - }catch(CreateUpdateError $updateError){ - return false; - } - } - - /** - * Indicates whether the file exists - * - * @param string $key - * - * @return boolean - */ - public function exists($key) - { - $this->initialize(); - return $this->tryGetObject($key)->bytes > 0; - } - - /** - * Returns an array of all keys (files and directories) - * - * @return array - */ - public function keys() - { - $this->initialize(); - $objectList = $this->container->ObjectList(); - $keys = array(); - while($object = $objectList->Next()) { - $keys[] = $object->name; - } - sort($keys); - return $keys; - } - - /** - * Returns the last modified time - * - * @param string $key - * - * @return integer|boolean An UNIX like timestamp or false - */ - public function mtime($key) - { - $this->initialize(); - $lastModified = $this->tryGetObject($key)->last_modified; - return $lastModified; - } - - /** - * Deletes the file - * - * @param string $key - * - * @return boolean - */ - public function delete($key) - { - $this->initialize(); - try{ - $this->tryGetObject($key)->Delete(); - }catch (ObjectStore\DeleteError $deleteError){ - return false; - } - return true; - } - - /** - * Renames a file - * - * @param string $sourceKey - * @param string $targetKey - * - * @return boolean - */ - public function rename($sourceKey, $targetKey) - { - $this->initialize(); - $this->write($targetKey, $this->read($sourceKey)); - $this->delete($sourceKey); - } - - /** - * Check if key is directory - * - * @param string $key - * - * @return boolean - */ - public function isDirectory($key) - { - return false; - } - - /** - * Returns the checksum of the specified key - * - * @param string $key - * - * @return string - */ - public function checksum($key) - { - $this->initialize(); - return $this->tryGetObject($key)->getETag(); - } - - /** - * @param $key - * @return \OpenCloud\ObjectStore\DataObject - */ - protected function tryGetObject($key) - { - try{ - return $this->container->DataObject($key); - }catch (ObjFetchError $objFetchError){ - return false; - } - } -} \ No newline at end of file diff --git a/src/Gaufrette/Adapter/PhpseclibSftp.php b/src/Gaufrette/Adapter/PhpseclibSftp.php new file mode 100644 index 000000000..2e71d625c --- /dev/null +++ b/src/Gaufrette/Adapter/PhpseclibSftp.php @@ -0,0 +1,242 @@ +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(\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); + 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 = []; + foreach (['keys', 'dirs'] as $hash) { + $filteredKeys[$hash] = []; + 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 $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) + { + $pwd = $this->sftp->pwd(); + if ($this->sftp->chdir($directory)) { + $this->sftp->chdir($pwd); + } 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())); + } + } 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 = ['keys' => [], 'dirs' => []]; + $computedPath = $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; + } + + $path = ltrim($directory . '/' . $filename, '/'); + if (isset($stat['type']) && $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'] = []; + } + + foreach ($dirs as $dir) { + $keys = array_merge_recursive($keys, $this->fetchKeys($dir, $onlyKeys)); + } + + return $keys; + } +} diff --git a/src/Gaufrette/Adapter/RackspaceCloudfiles.php b/src/Gaufrette/Adapter/RackspaceCloudfiles.php deleted file mode 100644 index a1f89c4c5..000000000 --- a/src/Gaufrette/Adapter/RackspaceCloudfiles.php +++ /dev/null @@ -1,150 +0,0 @@ - - */ -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 @@ - */ 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 deleted file mode 100644 index 73cb76d50..000000000 --- a/src/Gaufrette/Adapter/Sftp.php +++ /dev/null @@ -1,223 +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(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); - $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(array($this, 'computeKey'), $results['files']); - - $dirs = array(); - foreach ($files as $file) { - if ('.' !== dirname($file)) { - $dirs[] = dirname($file); - } - } - - $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 boolean $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); - - if (false === @opendir($url) && (!$create || !$this->createDirectory($directory))) { - throw new \RuntimeException(sprintf('The directory \'%s\' does not exist and could not be created.', $directory)); - } - } - - /** - * Creates the specified directory and its parents - * - * @param string $directory The directory to create - * - * @return boolean TRUE on success, or FALSE on failure - */ - protected function createDirectory($directory) - { - return mkdir($this->sftp->getUrl($directory), 0777, true); - } -} diff --git a/src/Gaufrette/Adapter/SizeCalculator.php b/src/Gaufrette/Adapter/SizeCalculator.php new file mode 100644 index 000000000..512537a5b --- /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 int + */ + public function size($key); +} diff --git a/src/Gaufrette/Adapter/StreamFactory.php b/src/Gaufrette/Adapter/StreamFactory.php index cec1d90d6..bbf8f21bd 100644 --- a/src/Gaufrette/Adapter/StreamFactory.php +++ b/src/Gaufrette/Adapter/StreamFactory.php @@ -3,18 +3,18 @@ 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 * - * @return Gaufrette\Stream + * @return \Gaufrette\Stream */ public function createStream($key); } diff --git a/src/Gaufrette/Adapter/Zip.php b/src/Gaufrette/Adapter/Zip.php index 0eeccb720..4f261f261 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 @@ -28,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; @@ -39,7 +35,7 @@ public function __construct($zipFile) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function read($key) { @@ -51,7 +47,7 @@ public function read($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function write($key, $content) { @@ -67,7 +63,7 @@ public function write($key, $content) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function exists($key) { @@ -75,11 +71,11 @@ public function exists($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function keys() { - $keys = array(); + $keys = []; for ($i = 0; $i < $this->zipArchive->numFiles; ++$i) { $keys[$i] = $this->zipArchive->getNameIndex($i); @@ -91,7 +87,7 @@ public function keys() /** * @todo implement * - * {@inheritDoc} + * {@inheritdoc} */ public function isDirectory($key) { @@ -99,17 +95,17 @@ public function isDirectory($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function mtime($key) { $stat = $this->getStat($key); - return isset($stat['mtime']) ? $stat['mtime'] : false; + return $stat['mtime'] ?? false; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function delete($key) { @@ -121,7 +117,7 @@ public function delete($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function rename($sourceKey, $targetKey) { @@ -134,16 +130,17 @@ 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) { $stat = $this->zipArchive->statName($key); if (false === $stat) { - return array(); + return []; } return $stat; @@ -155,7 +152,6 @@ public function __destruct() try { $this->zipArchive->close(); } catch (\Exception $e) { - } unset($this->zipArchive); } @@ -167,36 +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.'; - 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; - $errMsg = 'Seek error.'; - break; - default: - $errMsg = 'Unknown error.'; - break; + 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: + $errMsg = 'Seek error.'; + + break; + default: + $errMsg = 'Unknown error.'; + + break; } throw new \RuntimeException(sprintf('%s', $errMsg)); @@ -206,7 +212,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/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 @@ + */ @@ -16,8 +16,9 @@ class File protected $filesystem; /** - * Content variable is lazy. It will not be read from filesystem until it's requested first time - * @var content + * Content variable is lazy. It will not be read from filesystem until it's requested first time. + * + * @var mixed content */ protected $content = null; @@ -27,30 +28,31 @@ 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; /** - * Constructor - * * @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; @@ -58,7 +60,7 @@ public function __construct($key, Filesystem $filesystem) } /** - * Returns the key + * Returns the key. * * @return string */ @@ -68,14 +70,15 @@ public function getKey() } /** - * Returns the content + * Returns the content. * - * @throws Gaufrette\Exception\FileNotFound + * @throws FileNotFound + * + * @param array $metadata optional metadata which should be set when read * - * @param array $metadata optional metadata which should be send when read * @return string */ - public function getContent($metadata = array()) + public function getContent($metadata = []) { if (isset($this->content)) { return $this->content; @@ -103,7 +106,7 @@ public function getSize() } try { - return $this->size = Util\Size::fromContent($this->getContent()); + return $this->size = $this->filesystem->size($this->getKey()); } catch (FileNotFound $exception) { } @@ -111,17 +114,17 @@ public function getSize() } /** - * Returns the file modified time + * Returns the file modified time. * * @return int - */ + */ public function getMtime() { return $this->mtime = $this->filesystem->mtime($this->key); } /** - * @param int size of the file + * @param int $size size of the file */ public function setSize($size) { @@ -129,15 +132,15 @@ 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()) + public function setContent($content, $metadata = []) { $this->content = $content; $this->setMetadata($metadata); @@ -146,7 +149,7 @@ public function setContent($content, $metadata = array()) } /** - * @param string name of the file + * @param string $name name of the file */ public function setName($name) { @@ -154,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() { @@ -164,14 +167,16 @@ 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 * - * @throws Gaufrette\Exception\FileNotFound - * @throws \RuntimeException when cannot delete file - * @param array $metadata optional metadata which should be send when write - * @return boolean TRUE on success + * @return bool TRUE on success */ - public function delete($metadata = array()) + public function delete($metadata = []) { $this->setMetadata($metadata); @@ -179,9 +184,9 @@ public function delete($metadata = array()) } /** - * Creates a new file stream instance of the file + * Creates a new file stream instance of the file. * - * @return FileStream + * @return Stream */ public function createStream() { @@ -189,10 +194,23 @@ public function createStream() } /** - * Sets the metadata array to be stored in adapters that can support it + * 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. + * + * @param array $metadata * - * @param array $metadata - * @return boolean + * @return bool */ protected function setMetadata(array $metadata) { @@ -206,7 +224,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 8c53ca942..e08d1921d 100644 --- a/src/Gaufrette/Filesystem.php +++ b/src/Gaufrette/Filesystem.php @@ -5,18 +5,23 @@ 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 */ -class Filesystem +class Filesystem implements FilesystemInterface { protected $adapter; /** - * Constructor + * Contains File objects created with $this->createFile() method. * + * @var array + */ + protected $fileRegister = []; + + /** * @param Adapter $adapter A configured Adapter instance */ public function __construct(Adapter $adapter) @@ -25,7 +30,7 @@ public function __construct(Adapter $adapter) } /** - * Returns the adapter + * Returns the adapter. * * @return Adapter */ @@ -35,54 +40,48 @@ public function getAdapter() } /** - * Indicates whether the file matching the specified key exists - * - * @param string $key - * - * @return boolean TRUE if the file exists, FALSE otherwise + * {@inheritdoc} */ public function has($key) { + self::assertValidKey($key); + return $this->adapter->exists($key); } /** - * Renames a file - * - * @param string $sourceKey - * @param string $targetKey - * - * @return boolean 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 + * {@inheritdoc} */ public function rename($sourceKey, $targetKey) { + self::assertValidKey($sourceKey); + self::assertValidKey($targetKey); + $this->assertHasFile($sourceKey); if ($this->has($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)) { + $this->fileRegister[$targetKey] = $this->fileRegister[$sourceKey]; + unset($this->fileRegister[$sourceKey]); + } + return true; } /** - * 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 - * - * @throws Gaufrette\Exception\FileNotFound - * @return File + * {@inheritdoc} */ public function get($key, $create = false) { + self::assertValidKey($key); + if (!$create) { $this->assertHasFile($key); } @@ -91,18 +90,12 @@ 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 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 + * {@inheritdoc} */ public function write($key, $content, $overwrite = false) { + self::assertValidKey($key); + if (!$overwrite && $this->has($key)) { throw new Exception\FileAlreadyExists($key); } @@ -117,16 +110,12 @@ 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 - * - * @return string + * {@inheritdoc} */ public function read($key) { + self::assertValidKey($key); + $this->assertHasFile($key); $content = $this->adapter->read($key); @@ -139,18 +128,17 @@ public function read($key) } /** - * Deletes the file matching the specified key - * - * @param string $key - * @throws \RuntimeException when cannot read file - * - * @return boolean + * {@inheritdoc} */ public function delete($key) { + self::assertValidKey($key); + $this->assertHasFile($key); if ($this->adapter->delete($key)) { + $this->removeFromRegister($key); + return true; } @@ -158,9 +146,7 @@ public function delete($key) } /** - * Returns an array of all keys - * - * @return array + * {@inheritdoc} */ public function keys() { @@ -168,14 +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 = '') { @@ -183,11 +162,11 @@ public function listKeys($prefix = '') return $this->adapter->listKeys($prefix); } - $dirs = array(); - $keys = array(); + $dirs = []; + $keys = []; 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 { @@ -196,35 +175,31 @@ public function listKeys($prefix = '') } } - return array( + return [ 'keys' => $keys, - 'dirs' => $dirs - ); + 'dirs' => $dirs, + ]; } /** - * Returns the last modified time of the specified file - * - * @param string $key - * - * @return integer An UNIX like timestamp + * {@inheritdoc} */ public function mtime($key) { + self::assertValidKey($key); + $this->assertHasFile($key); return $this->adapter->mtime($key); } /** - * Returns the checksum of the specified file's content - * - * @param string $key - * - * @return string A MD5 hash + * {@inheritdoc} */ public function checksum($key) { + self::assertValidKey($key); + $this->assertHasFile($key); if ($this->adapter instanceof Adapter\ChecksumCalculator) { @@ -235,10 +210,28 @@ public function checksum($key) } /** - * {@inheritDoc} + * {@inheritdoc} + */ + public function size($key) + { + self::assertValidKey($key); + + $this->assertHasFile($key); + + if ($this->adapter instanceof Adapter\SizeCalculator) { + return $this->adapter->size($key); + } + + return Util\Size::fromContent($this->read($key)); + } + + /** + * {@inheritdoc} */ public function createStream($key) { + self::assertValidKey($key); + if ($this->adapter instanceof Adapter\StreamFactory) { return $this->adapter->createStream($key); } @@ -247,25 +240,108 @@ public function createStream($key) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function createFile($key) { - if ($this->adapter instanceof Adapter\FileFactory) { - return $this->adapter->createFile($key, $this); + self::assertValidKey($key); + + if (false === $this->isFileInRegister($key)) { + 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]; + } + + /** + * {@inheritdoc} + */ + public function mimeType($key) + { + self::assertValidKey($key); + + $this->assertHasFile($key); + + if ($this->adapter instanceof Adapter\MimeTypeProvider) { + return $this->adapter->mimeType($key); } - return new File($key, $this); + throw new \LogicException(sprintf( + 'Adapter "%s" cannot provide MIME type', + get_class($this->adapter) + )); } /** - * @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 (!$this->has($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); + } + + /** + * Clear files register. + */ + public function clearFileRegister() + { + $this->fileRegister = []; + } + + /** + * Removes File object from register. + * + * @param string $key + */ + public function removeFromRegister($key) + { + if ($this->isFileInRegister($key)) { + unset($this->fileRegister[$key]); + } + } + + /** + * {@inheritdoc} + */ + 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.'); + } + } } diff --git a/src/Gaufrette/FilesystemInterface.php b/src/Gaufrette/FilesystemInterface.php new file mode 100644 index 000000000..9a686ab8f --- /dev/null +++ b/src/Gaufrette/FilesystemInterface.php @@ -0,0 +1,182 @@ + */ -class FilesystemMap +class FilesystemMap implements FilesystemMapInterface { - private $filesystems = array(); + private $filesystems = []; /** * 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,87 +23,71 @@ public function all() } /** - * Register the given filesystem for the specified domain + * Register the given filesystem for the specified name. * - * @param string $domain - * @param Filesystem $filesystem + * @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, Filesystem $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 Boolean + * {@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 Filesystem - * - * @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 - * - * @param string $domain + * Removes the filesystem registered for the specified name. * - * @return void + * @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]); } /** - * Clears all the registered filesystems - * - * @return void + * Clears all the registered filesystems. */ public function clear() { - $this->filesystems = array(); + $this->filesystems = []; } } 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 @@ + */ 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 57e9fe555..30ac9be62 100644 --- a/src/Gaufrette/Stream/InMemoryBuffer.php +++ b/src/Gaufrette/Stream/InMemoryBuffer.php @@ -18,19 +18,17 @@ class InMemoryBuffer implements Stream private $synchronized; /** - * Constructor - * * @param Filesystem $filesystem The filesystem managing the file to stream * @param string $key The file key */ public function __construct(Filesystem $filesystem, $key) { $this->filesystem = $filesystem; - $this->key = $key; + $this->key = $key; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function open(StreamMode $mode) { @@ -66,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; } @@ -79,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; @@ -103,7 +101,7 @@ public function write($data) public function close() { - if (! $this->synchronized) { + if (!$this->synchronized) { $this->flush(); } } @@ -113,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; @@ -153,28 +154,29 @@ 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); - $stats = array( - 'dev' => 1, - 'ino' => 0, - 'mode' => 33204, + $stats = [ + 'dev' => 1, + 'ino' => 0, + 'mode' => $isDirectory ? 16893 : 33204, 'nlink' => 1, - 'uid' => 0, - 'gid' => 0, - 'rdev' => 0, - 'size' => 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); } @@ -183,7 +185,7 @@ public function stat() } /** - * {@inheritDoc} + * {@inheritdoc} */ public function cast($castAst) { @@ -191,7 +193,7 @@ public function cast($castAst) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function unlink() { @@ -203,7 +205,7 @@ public function unlink() } /** - * @return Boolean + * @return bool */ protected function hasNewContentAtFurtherPosition() { @@ -211,8 +213,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 c9bfe5af6..306119c5e 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 */ @@ -15,23 +15,33 @@ class Local implements Stream private $path; private $mode; private $fileHandle; + private $mkdirMode; /** - * Constructor - * * @param string $path + * @param int $mkdirMode */ - public function __construct($path) + public function __construct($path, $mkdirMode = 0755) { $this->path = $path; + $this->mkdirMode = $mkdirMode; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function open(StreamMode $mode) { - $fileHandle = @fopen($this->path, $mode->getMode()); + $baseDirPath = \Gaufrette\Util\Path::dirname($this->path); + if ($mode->allowsWrite() && !is_dir($baseDirPath)) { + @mkdir($baseDirPath, $this->mkdirMode, true); + } + + 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)); @@ -44,11 +54,11 @@ public function open(StreamMode $mode) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function read($count) { - if (! $this->fileHandle) { + if (!$this->fileHandle) { return false; } @@ -60,11 +70,11 @@ public function read($count) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function write($data) { - if (! $this->fileHandle) { + if (!$this->fileHandle) { return false; } @@ -76,11 +86,11 @@ public function write($data) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function close() { - if (! $this->fileHandle) { + if (!$this->fileHandle) { return false; } @@ -95,7 +105,7 @@ public function close() } /** - * {@inheritDoc} + * {@inheritdoc} */ public function flush() { @@ -107,7 +117,7 @@ public function flush() } /** - * {@inheritDoc} + * {@inheritdoc} */ public function seek($offset, $whence = SEEK_SET) { @@ -119,7 +129,7 @@ public function seek($offset, $whence = SEEK_SET) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function tell() { @@ -131,7 +141,7 @@ public function tell() } /** - * {@inheritDoc} + * {@inheritdoc} */ public function eof() { @@ -143,19 +153,21 @@ public function eof() } /** - * {@inheritDoc} + * {@inheritdoc} */ 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; } /** - * {@inheritDoc} + * {@inheritdoc} */ public function cast($castAs) { @@ -167,7 +179,7 @@ public function cast($castAs) } /** - * {@inheritDoc} + * {@inheritdoc} */ public function unlink() { diff --git a/src/Gaufrette/StreamMode.php b/src/Gaufrette/StreamMode.php index 581a29890..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 */ @@ -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) @@ -32,7 +30,7 @@ public function __construct($mode) } /** - * Returns the underlying mode + * Returns the underlying mode. * * @return string */ @@ -42,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() { @@ -56,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() { @@ -70,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() { @@ -80,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() { @@ -91,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() { @@ -102,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() { @@ -113,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() { @@ -123,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() { @@ -133,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 cf508095e..4497af370 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,39 +15,39 @@ class StreamWrapper private $stream; /** - * Defines the filesystem map + * Defines the filesystem map. * * @param FilesystemMap $map */ public static function setFilesystemMap(FilesystemMap $map) { - static::$filesystemMap = $map; + self::$filesystemMap = $map; } /** - * Returns the filesystem map + * Returns the filesystem map. * * @return 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; } /** - * Registers the stream wrapper to handle the specified scheme + * Registers the stream wrapper to handle the specified scheme. * * @param string $scheme Default is gaufrette */ 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__, @@ -69,12 +69,16 @@ 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); + } } /** * @param string $scheme - protocol scheme * @param string $className + * + * @return bool */ protected static function streamWrapperRegister($scheme, $className) { @@ -89,7 +93,8 @@ public function stream_open($path, $mode) } /** - * @param int $bytes + * @param int $bytes + * * @return mixed */ public function stream_read($bytes) @@ -102,7 +107,8 @@ public function stream_read($bytes) } /** - * @param string $data + * @param string $data + * * @return int */ public function stream_write($data) @@ -122,7 +128,7 @@ public function stream_close() } /** - * @return boolean + * @return bool */ public function stream_flush() { @@ -134,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) { @@ -160,7 +167,7 @@ public function stream_tell() } /** - * @return boolean + * @return bool */ public function stream_eof() { @@ -184,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) @@ -196,14 +205,14 @@ public function url_stat($path, $flags) try { $stream->open($this->createStreamMode('r+')); } catch (\RuntimeException $e) { - return false; } return $stream->stat(); } /** - * @param string $path + * @param string $path + * * @return mixed */ public function unlink($path) @@ -234,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() + [ + 'scheme' => null, + 'host' => null, + 'path' => null, + 'query' => null, + 'fragment' => null, + ], + parse_url($path) ?: [] ); $domain = $parts['host']; - $key = substr($parts['path'], 1); + $key = !empty($parts['path']) ? 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)) { @@ -262,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) 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 b92f556a2..4dec5c50a 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,22 +18,23 @@ 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'); - $tokens = array(); + $path = substr($path, strlen($prefix)); + $parts = array_filter(explode('/', $path), 'strlen'); + $tokens = []; 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; @@ -45,11 +45,11 @@ public static function normalize($path) } /** - * 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 +57,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 * @@ -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 ''; @@ -73,4 +73,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)); + } } diff --git a/src/Gaufrette/Util/Size.php b/src/Gaufrette/Util/Size.php index 635c01459..7dd53aba3 100644 --- a/src/Gaufrette/Util/Size.php +++ b/src/Gaufrette/Util/Size.php @@ -3,23 +3,51 @@ 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 */ 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'); + } + + /** + * Returns the size in bytes from the given file. + * + * @param string $filename + * + * @return int + */ + 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; } } 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 (!ini_get('apc.enabled') || !ini_get('apc.enable_cli')) { - return $this->markTestSkipped('The APC extension is available, but not enabled.'); - } - - apc_clear_cache('user'); - - $this->filesystem = new Filesystem(new Apc('gaufrette-test.')); - } - - public function tearDown() - { - parent::tearDown(); - if (extension_loaded('apc')) { - apc_clear_cache('user'); - } - } -} diff --git a/tests/Gaufrette/Functional/Adapter/AsyncAwsS3Test.php b/tests/Gaufrette/Functional/Adapter/AsyncAwsS3Test.php new file mode 100644 index 000000000..559010188 --- /dev/null +++ b/tests/Gaufrette/Functional/Adapter/AsyncAwsS3Test.php @@ -0,0 +1,152 @@ +markTestSkipped('You need to install async-aws/simple-s3 to run this test.'); + } + $key = getenv('AWS_KEY'); + $secret = getenv('AWS_SECRET'); + + if (empty($key) || empty($secret)) { + $this->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(): 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) { + } + } + + private function createFilesystem(array $adapterOptions = []) + { + $this->filesystem = new Filesystem(new AsyncAwsS3($this->client, $this->bucket, $adapterOptions)); + } + + /** + * @test + */ + public function shouldThrowExceptionIfBucketMissingAndNotCreating(): void + { + $this->expectException(\RuntimeException::class); + $this->createFilesystem(); + $this->filesystem->read('foo'); + } + + /** + * @test + */ + public function shouldWriteObjects(): void + { + $this->assertEquals(7, $this->filesystem->write('foo', 'testing')); + } + + /** + * @test + */ + public function shouldCheckForObjectExistence(): void + { + $this->filesystem->write('foo', ''); + $this->assertTrue($this->filesystem->has('foo')); + } + + /** + * @test + */ + public function shouldCheckForObjectExistenceWithDirectory(): void + { + $this->createFilesystem(['directory' => 'bar', 'create' => true]); + $this->filesystem->write('foo', ''); + + $this->assertTrue($this->filesystem->has('foo')); + } + + /** + * @test + */ + public function shouldListKeysWithoutDirectory(): void + { + $this->assertEquals([], $this->filesystem->listKeys()); + $this->filesystem->write('test.txt', 'some content'); + $this->assertEquals(['test.txt'], $this->filesystem->listKeys()); + } + + /** + * @test + */ + public function shouldListKeysWithDirectory(): void + { + $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(): void + { + $this->filesystem->write('test.txt', 'some content'); + $this->assertEquals(['test.txt'], $this->filesystem->keys()); + } + + /** + * @test + */ + public function shouldGetKeysWithDirectory(): void + { + $this->createFilesystem(['create' => true, 'directory' => 'root/']); + $this->filesystem->write('test.txt', 'some content'); + $this->assertEquals(['test.txt'], $this->filesystem->keys()); + } + + /** + * @test + */ + public function shouldUploadWithGivenContentType(): void + { + /** @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')); + } +} diff --git a/tests/Gaufrette/Functional/Adapter/AwsS3Test.php b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php new file mode 100644 index 000000000..037ba72a3 --- /dev/null +++ b/tests/Gaufrette/Functional/Adapter/AwsS3Test.php @@ -0,0 +1,174 @@ +markTestSkipped('Either AWS_KEY and/or AWS_SECRET env variables are not defined.'); + } + + 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', + 'credentials' => [ + 'key' => $key, + 'secret' => $secret, + ], + ]); + } else { + $this->client = S3Client::factory([ + 'region' => 'eu-west-1', + 'version' => '2006-03-01', + 'key' => $key, + 'secret' => $secret, + ]); + } + + $this->createFilesystem(['create' => true]); + } + + protected function tearDown(): void + { + if ($this->client === null || !$this->client->doesBucketExist($this->bucket)) { + return; + } + + $result = $this->client->listObjects(['Bucket' => $this->bucket]); + + if (!$result->hasKey('Contents')) { + $this->client->deleteBucket(['Bucket' => $this->bucket]); + + return; + } + + foreach ($result->get('Contents') as $staleObject) { + $this->client->deleteObject(['Bucket' => $this->bucket, 'Key' => $staleObject['Key']]); + } + + $this->client->deleteBucket(['Bucket' => $this->bucket]); + } + + private function createFilesystem(array $adapterOptions = []) + { + $this->filesystem = new Filesystem(new AwsS3($this->client, $this->bucket, $adapterOptions)); + } + + /** + * @test + */ + public function shouldThrowExceptionIfBucketMissingAndNotCreating(): void + { + $this->expectException(\RuntimeException::class); + $this->createFilesystem(); + $this->filesystem->read('foo'); + } + + /** + * @test + */ + public function shouldWriteObjects(): void + { + $this->assertEquals(7, $this->filesystem->write('foo', 'testing')); + } + + /** + * @test + */ + public function shouldCheckForObjectExistence(): void + { + $this->filesystem->write('foo', ''); + $this->assertTrue($this->filesystem->has('foo')); + } + + /** + * @test + */ + public function shouldCheckForObjectExistenceWithDirectory(): void + { + $this->createFilesystem(['directory' => 'bar', 'create' => true]); + $this->filesystem->write('foo', ''); + + $this->assertTrue($this->filesystem->has('foo')); + } + + /** + * @test + */ + public function shouldListKeysWithoutDirectory(): void + { + $this->assertEquals([], $this->filesystem->listKeys()); + $this->filesystem->write('test.txt', 'some content'); + $this->assertEquals(['test.txt'], $this->filesystem->listKeys()); + } + + /** + * @test + */ + public function shouldListKeysWithDirectory(): void + { + $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(): void + { + $this->filesystem->write('test.txt', 'some content'); + $this->assertEquals(['test.txt'], $this->filesystem->keys()); + } + + /** + * @test + */ + public function shouldGetKeysWithDirectory(): void + { + $this->createFilesystem(['create' => true, 'directory' => 'root/']); + $this->filesystem->write('test.txt', 'some content'); + $this->assertEquals(['test.txt'], $this->filesystem->keys()); + } + + /** + * @test + */ + public function shouldUploadWithGivenContentType(): void + { + /** @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')); + } +} diff --git a/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php b/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php new file mode 100644 index 000000000..88fda4301 --- /dev/null +++ b/tests/Gaufrette/Functional/Adapter/AzureBlobStorageTest.php @@ -0,0 +1,59 @@ +markTestSkipped('Either AZURE_ACCOUNT, AZURE_KEY and/or AZURE_CONTAINER env variables are not defined.'); + } + + $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); + $this->filesystem = new Filesystem($this->adapter); + } + + /** + * @test + * @group functional + */ + public function shouldGetContentType(): void + { + $path = '/somefile'; + $content = 'Some content'; + $this->filesystem->write($path, $content); + + $this->assertEquals('text/plain', $this->filesystem->mimeType($path)); + } + + protected function tearDown(): void + { + if ($this->adapter === null) { + return; + } + + $this->adapter->deleteContainer($this->container); + } +} diff --git a/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php new file mode 100644 index 000000000..f01989aae --- /dev/null +++ b/tests/Gaufrette/Functional/Adapter/AzureMultiContainerBlobStorageTest.php @@ -0,0 +1,270 @@ +markTestSkipped(__CLASS__ . ' is flaky.'); + + $account = getenv('AZURE_ACCOUNT'); + $key = getenv('AZURE_KEY'); + if (empty($account) || empty($key)) { + $this->markTestSkipped('Either AZURE_ACCOUNT and/or AZURE_KEY env variables are not defined.'); + } + + $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); + } + + /** + * @test + * @group functional + */ + public function shouldWriteAndRead(): void + { + $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($path1)); + $this->assertEquals('Some content1', $this->filesystem->read($path2)); + } + + /** + * @test + * @group functional + */ + public function shouldUpdateFileContent(): void + { + $path = $this->createUniqueContainerName('container') . '/foo'; + + $this->filesystem->write($path, 'Some content'); + $this->filesystem->write($path, 'Some content updated', true); + + $this->assertEquals('Some content updated', $this->filesystem->read($path)); + } + + /** + * @test + * @group functional + */ + public function shouldCheckIfFileExists(): void + { + $path1 = $this->createUniqueContainerName('container') . '/foo'; + $path2 = $this->createUniqueContainerName('test') . '/somefile'; + + $this->assertFalse($this->filesystem->has($path1)); + + $this->filesystem->write($path1, 'Some content'); + + $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)); + } + + /** + * @test + * @group functional + */ + public function shouldGetMtime(): void + { + $path = $this->createUniqueContainerName('container') . '/foo'; + + $this->filesystem->write($path, 'Some content'); + + $this->assertGreaterThan(0, $this->filesystem->mtime($path)); + } + + /** + * @test + * @group functional + */ + public function shouldGetSize(): void + { + $path = $this->createUniqueContainerName('container') . '/foo'; + + $contentSize = $this->filesystem->write($path, 'Some content'); + + $this->assertEquals($contentSize, $this->filesystem->size($path)); + } + + /** + * @test + * @group functional + */ + public function shouldGetMd5Hash(): void + { + $path = $this->createUniqueContainerName('container') . '/foo'; + + $content = 'Some content'; + $this->filesystem->write($path, $content); + + $this->assertEquals(\md5($content), $this->filesystem->checksum($path)); + } + + /** + * @test + * @group functional + */ + public function shouldGetContentType(): void + { + $path = $this->createUniqueContainerName('container') . '/foo'; + + $content = 'Some content'; + $this->filesystem->write($path, $content); + + $this->assertEquals('text/plain', $this->filesystem->mimeType($path)); + } + + /** + * @test + * @group functional + * @expectedException \RuntimeException + * @expectedMessage Could not get mtime for the "foo" key + */ + public function shouldFailWhenTryMtimeForKeyWhichDoesNotExist(): void + { + $this->assertFalse($this->filesystem->mtime('container5/foo')); + } + + /** + * @test + * @group functional + */ + public function shouldRenameFile(): void + { + $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($path1)); + $this->assertEquals('Some content', $this->filesystem->read($path2)); + + $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($path3)); + } + + /** + * @test + * @group functional + */ + public function shouldDeleteFile(): void + { + $path = $this->createUniqueContainerName('container') . '/foo'; + + $this->filesystem->write($path, 'Some content'); + + $this->assertTrue($this->filesystem->has($path)); + + $this->filesystem->delete($path); + + $this->assertFalse($this->filesystem->has($path)); + } + + /** + * @test + * @group functional + */ + public function shouldFetchKeys(): void + { + $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 ([$path1, $path2, $path3] as $key) { + $this->assertContains($key, $actualKeys); + } + } + + /** + * @test + * @group functional + */ + public function shouldWorkWithHiddenFiles(): void + { + $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)); + } + + /** + * @test + * @group functional + */ + public function shouldKeepFileObjectInRegister(): void + { + $path = $this->createUniqueContainerName('container') . '/somefile'; + + $FileObjectA = $this->filesystem->createFile($path); + $FileObjectB = $this->filesystem->createFile($path); + + $this->assertSame($FileObjectB, $FileObjectA); + } + + /** + * @test + * @group functional + */ + public function shouldWriteToSameFile(): void + { + $path = $this->createUniqueContainerName('container') . '/somefile'; + + $FileObjectA = $this->filesystem->createFile($path); + $FileObjectA->setContent('ABC'); + + $FileObjectB = $this->filesystem->createFile($path); + $FileObjectB->setContent('DEF'); + + $this->assertEquals('DEF', $FileObjectA->getContent()); + } + + private function createUniqueContainerName($prefix) + { + $this->containers[] = $container = uniqid($prefix); + + return $container; + } + + protected function tearDown(): void + { + 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 deleted file mode 100644 index 8c3835b29..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/DoctrineDbalTest.php b/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php index 2684fffd1..aa9d8a39f 100644 --- a/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php +++ b/tests/Gaufrette/Functional/Adapter/DoctrineDbalTest.php @@ -2,12 +2,64 @@ 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 static function setUpBeforeClass(): void + { + if (!class_exists(DriverManager::class)) { + self::markTestSkipped('Package doctrine/dbal is not installed'); + } + parent::setUpBeforeClass(); + } + + protected function setUp(): void + { + $this->connection = DriverManager::getConnection([ + 'driver' => 'pdo_sqlite', + 'memory' => true, + ]); + + $schema = $this->connection->getSchemaManager()->createSchema(); + + $table = $schema->createTable('gaufrette'); + $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]); + + // 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')); + } + + protected function tearDown(): void + { + $schemaManager = $this->connection->getSchemaManager(); + + if (in_array('gaufrette', $schemaManager->listTableNames())) { + $schemaManager->dropTable('gaufrette'); + } + } + /** - * @test - */ - public function shouldListKeys() + * @test + */ + public function shouldListKeys(): void { $this->filesystem->write('foo/foobar/bar.txt', 'data'); $this->filesystem->write('foo/bar/buzz.txt', 'data'); @@ -24,33 +76,33 @@ public function shouldListKeys() //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( - array('foo/foobar/bar.txt'), - $keys['keys'], - '', 0, 10, true); + $this->assertEqualsCanonicalizing( + ['foo/foobar/bar.txt'], + $keys['keys'] + ); $keys = $this->filesystem->listKeys('foo/'); - $this->assertEquals( - array('foo/foobar/bar.txt', 'foo/bar/buzz.txt'), - $keys['keys'], - '', 0, 10, true); + $this->assertEqualsCanonicalizing( + ['foo/foobar/bar.txt', 'foo/bar/buzz.txt'], + $keys['keys'] + ); $keys = $this->filesystem->listKeys('foo'); - $this->assertEquals( - array('foo/foobar/bar.txt', 'foo/bar/buzz.txt', 'foobarbuz.txt', 'foo'), - $keys['keys'], - '', 0, 10, true); + $this->assertEqualsCanonicalizing( + ['foo/foobar/bar.txt', 'foo/bar/buzz.txt', 'foobarbuz.txt', 'foo'], + $keys['keys'] + ); $keys = $this->filesystem->listKeys('fooz'); - $this->assertEquals( - array(), - $keys['keys'], - '', 0, 10, true); + $this->assertEqualsCanonicalizing( + [], + $keys['keys'] + ); } } diff --git a/tests/Gaufrette/Functional/Adapter/DropboxTest.php b/tests/Gaufrette/Functional/Adapter/DropboxTest.php deleted file mode 100644 index ff0a224ec..000000000 --- a/tests/Gaufrette/Functional/Adapter/DropboxTest.php +++ /dev/null @@ -1,9 +0,0 @@ -markTestSkipped('Either FTP_HOST, FTP_USER, FTP_PASSWORD and/or FTP_BASE_DIR env variables are not defined.'); + } + + $adapter = new Ftp($baseDir, $host, ['port' => $port, 'username' => $user, 'password' => $password, 'passive' => true, 'create' => true]); + $this->filesystem = new Filesystem($adapter); + } + + protected function tearDown(): void + { + 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); + } + + $adapter->close(); + } } diff --git a/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php b/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php index f1580afdd..a78aa0edf 100644 --- a/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php +++ b/tests/Gaufrette/Functional/Adapter/FunctionalTestCase.php @@ -2,9 +2,11 @@ namespace Gaufrette\Functional\Adapter; +use Gaufrette\Exception\FileNotFound; use Gaufrette\Filesystem; +use PHPUnit\Framework\TestCase; -abstract class FunctionalTestCase extends \PHPUnit_Framework_TestCase +abstract class FunctionalTestCase extends TestCase { /** * @var Filesystem @@ -15,7 +17,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) )); @@ -24,7 +26,7 @@ public function getAdapterName() return $matches[1]; } - public function setUp() + protected function setUp(): void { $basename = $this->getAdapterName(); $filename = sprintf( @@ -34,7 +36,8 @@ public function setUp() ); if (!file_exists($filename)) { - return $this->markTestSkipped(<<markTestSkipped( + <<filesystem = new Filesystem($adapter); } - public function tearDown() + protected function tearDown(): void { if (null === $this->filesystem) { return; @@ -60,7 +63,7 @@ public 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)); @@ -73,7 +76,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); @@ -85,7 +88,7 @@ public function shouldUpdateFileContent() * @test * @group functional */ - public function shouldCheckIfFileExists() + public function shouldCheckIfFileExists(): void { $this->assertFalse($this->filesystem->has('foo')); @@ -100,7 +103,7 @@ public function shouldCheckIfFileExists() * @test * @group functional */ - public function shouldGetMtime() + public function shouldGetMtime(): void { $this->filesystem->write('foo', 'Some content'); @@ -110,19 +113,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'); @@ -136,14 +139,13 @@ 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'); } /** * @test * @group functional */ - public function shouldDeleteFile() + public function shouldDeleteFile(): void { $this->filesystem->write('foo', 'Some content'); @@ -158,9 +160,9 @@ public function shouldDeleteFile() * @test * @group functional */ - public function shouldFetchKeys() + public function shouldFetchKeys(): void { - $this->assertEquals(array(), $this->filesystem->keys()); + $this->assertEquals([], $this->filesystem->keys()); $this->filesystem->write('foo', 'Some content'); $this->filesystem->write('bar', 'Some content'); @@ -168,8 +170,8 @@ public function shouldFetchKeys() $actualKeys = $this->filesystem->keys(); - $this->assertEquals(3, count($actualKeys)); - foreach (array('foo', 'bar', 'baz') as $key) { + $this->assertCount(3, $actualKeys); + foreach (['foo', 'bar', 'baz'] as $key) { $this->assertContains($key, $actualKeys); } } @@ -178,7 +180,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')); @@ -186,4 +188,31 @@ public function shouldWorkWithHiddenFiles() $this->filesystem->delete('.foo'); $this->assertFalse($this->filesystem->has('.foo')); } + + /** + * @test + * @group functional + */ + public function shouldKeepFileObjectInRegister(): void + { + $FileObjectA = $this->filesystem->createFile('somefile'); + $FileObjectB = $this->filesystem->createFile('somefile'); + + $this->assertSame($FileObjectA, $FileObjectB); + } + + /** + * @test + * @group functional + */ + public function shouldWriteToSameFile(): void + { + $FileObjectA = $this->filesystem->createFile('somefile'); + $FileObjectA->setContent('ABC'); + + $FileObjectB = $this->filesystem->createFile('somefile'); + $FileObjectB->setContent('DEF'); + + $this->assertEquals('DEF', $FileObjectA->getContent()); + } } diff --git a/tests/Gaufrette/Functional/Adapter/GoogleCloudStorageTest.php b/tests/Gaufrette/Functional/Adapter/GoogleCloudStorageTest.php new file mode 100644 index 000000000..8cb6f702c --- /dev/null +++ b/tests/Gaufrette/Functional/Adapter/GoogleCloudStorageTest.php @@ -0,0 +1,88 @@ + + */ +class GoogleCloudStorageTest extends FunctionalTestCase +{ + /** + * @test + * @group functional + */ + public function shouldThrowExceptionIfBucketMissing() + { + $this->expectException(\RuntimeException::class); + /** @var \Gaufrette\Adapter\GoogleCloudStorage $adapter */ + $adapter = $this->filesystem->getAdapter(); + $adapter->setOptions([GoogleCloudStorage::OPTION_CREATE_BUCKET_IF_NOT_EXISTS => false]); + $adapter->setBucket('Gaufrette-' . mt_rand()); + + $adapter->read('foo'); + } + + /** + * @test + * @group functional + */ + public function shouldWriteAndReadWithDirectory() + { + /** @var \Gaufrette\Adapter\GoogleCloudStorage $adapter */ + $adapter = $this->filesystem->getAdapter(); + $oldOptions = $adapter->getOptions(); + $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)); + + $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); + } + + /** + * @test + * @group functional + */ + public function shouldSetMetadataCorrectly() + { + /** @var \Gaufrette\Adapter\GoogleCloudStorage $adapter */ + $adapter = $this->filesystem->getAdapter(); + + $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, ['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([ + 'Colour' => 'Yellow', + ], $metadata->metadata); + + $this->filesystem->delete('metadata.txt'); + } +} diff --git a/tests/Gaufrette/Functional/Adapter/GridFSTest.php b/tests/Gaufrette/Functional/Adapter/GridFSTest.php index 3e2022b08..6b4860556 100644 --- a/tests/Gaufrette/Functional/Adapter/GridFSTest.php +++ b/tests/Gaufrette/Functional/Adapter/GridFSTest.php @@ -3,13 +3,32 @@ namespace Gaufrette\Functional\Adapter; use Gaufrette\Adapter\GridFS; +use Gaufrette\Filesystem; +use MongoDB\Client; class GridFSTest extends FunctionalTestCase { + protected function setUp(): void + { + $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 */ - public function shouldListKeys() + public function shouldListKeys(): void { $this->filesystem->write('foo/foobar/bar.txt', 'data'); $this->filesystem->write('foo/bar/buzz.txt', 'data'); @@ -26,33 +45,82 @@ public function shouldListKeys() //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( - array('foo/foobar/bar.txt'), - $keys['keys'], - '', 0, 10, true); + $this->assertEqualsCanonicalizing( + ['foo/foobar/bar.txt'], + $keys['keys'] + ); $keys = $this->filesystem->listKeys('foo/'); - $this->assertEquals( - array('foo/foobar/bar.txt', 'foo/bar/buzz.txt'), - $keys['keys'], - '', 0, 10, true); + $this->assertEqualsCanonicalizing( + ['foo/foobar/bar.txt', 'foo/bar/buzz.txt'], + $keys['keys'] + ); $keys = $this->filesystem->listKeys('foo'); - $this->assertEquals( - array('foo/foobar/bar.txt', 'foo/bar/buzz.txt', 'foobarbuz.txt', 'foo'), - $keys['keys'], - '', 0, 10, true); + $this->assertEqualsCanonicalizing( + ['foo/foobar/bar.txt', 'foo/bar/buzz.txt', 'foobarbuz.txt', 'foo'], + $keys['keys'] + ); $keys = $this->filesystem->listKeys('fooz'); - $this->assertEquals( - array(), - $keys['keys'], - '', 0, 10, true); + $this->assertEqualsCanonicalizing( + [], + $keys['keys'] + ); + } + + /** + * @test + * Tests metadata written to GridFS can be retrieved after writing + */ + public function shouldRetrieveMetadataAfterWrite(): void + { + //Create local copy of fileadapter + $fileadpt = clone $this->filesystem->getAdapter(); + + $this->filesystem->getAdapter()->setMetadata('metadatatest', ['testing' => true]); + $this->filesystem->write('metadatatest', 'test'); + + $this->assertEquals($this->filesystem->getAdapter()->getMetadata('metadatatest'), $fileadpt->getMetadata('metadatatest')); + } + + /** + * @test + * Test to see if filesize works + */ + public function shouldGetSize(): void + { + $this->filesystem->write('sizetest.txt', 'data'); + $this->assertEquals(4, $this->filesystem->size('sizetest.txt')); + } + + /** + * @test + * Should retrieve empty metadata w/o errors + */ + public function shouldRetrieveEmptyMetadata(): void + { + $this->filesystem->write('no-metadata.txt', 'content'); + + $this->assertEquals([], $this->filesystem->getAdapter()->getMetadata('no-metadata.txt')); + } + + /** + * @test + * @group functional + */ + public function shouldGetMtime(): void + { + if (strtolower(substr(PHP_OS, 0, 3)) === 'win') { + $this->markTestSkipped('Not working on Windows.'); + } else { + parent::shouldGetMtime(); + } } } diff --git a/tests/Gaufrette/Functional/Adapter/LocalTest.php b/tests/Gaufrette/Functional/Adapter/LocalTest.php index c2833b8e8..4b319a7be 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(): void { $this->directory = sprintf('%s/filesystem', str_replace('\\', '/', __DIR__)); @@ -20,34 +20,29 @@ public function setUp() $this->filesystem = new Filesystem(new Local($this->directory)); } - public function tearDown() + protected function tearDown(): void { - $this->filesystem = null; + $adapter = $this->filesystem->getAdapter(); - if (file_exists($this->directory)) { - $iterator = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator( - $this->directory, - \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS - ) - ); - - foreach ($iterator as $item) { - if ($item->isDir()) { - rmdir(strval($item)); - } else { - unlink(strval($item)); - } - } + foreach ($this->filesystem->keys() as $key) { + $adapter->delete($key); } + + $this->filesystem = null; + + rmdir($this->directory); } /** * @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.'); + } + $dirname = sprintf( '%s/adapters/aaa', dirname(__DIR__) @@ -61,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); } @@ -73,16 +69,10 @@ public function shouldWorkWithSyslink() /** * @test * @covers Gaufrette\Adapter\Local + * @group functional */ - public function shouldListingOnlyGivenDirectory() + public function shouldListingOnlyGivenDirectory(): void { - $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'); @@ -98,9 +88,77 @@ 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); + /** + * @test + * @covers Gaufrette\Adapter\Local + * @group functional + */ + public function shouldListingAllKeys(): void + { + $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]); + } + + /** + * @test + * @group functional + */ + public function shouldBeAbleToClearCache(): void + { + $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->removeFromRegister('test.txt'); + $this->assertFalse($fsIsFileInRegister->invoke($this->filesystem, 'test.txt')); + + $this->filesystem->clearFileRegister(); + $fsRegister = $fsReflection->getProperty('fileRegister'); + $fsRegister->setAccessible(true); + $this->assertCount(0, $fsRegister->getValue($this->filesystem)); + } + + /** + * @test + * @group functional + */ + public function shouldDeleteDirectory(): void + { + $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 + */ + public function shouldNotDeleteTheAdapterRootDirectory(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('Impossible to delete the root directory of this Local adapter'); + $this->filesystem->getAdapter()->delete('/'); } } diff --git a/tests/Gaufrette/Functional/Adapter/MogileFSTest.php b/tests/Gaufrette/Functional/Adapter/MogileFSTest.php deleted file mode 100644 index 83fc7f48b..000000000 --- a/tests/Gaufrette/Functional/Adapter/MogileFSTest.php +++ /dev/null @@ -1,24 +0,0 @@ -markTestSkipped('Not supported by the adapter.'); - } - - /** - * @group functional - */ - public function shouldGetMtimeNonExistingFile() - { - $this->markTestSkipped('Not supported by the adapter.'); - } -} diff --git a/tests/Gaufrette/Functional/Adapter/PhpseclibSftpTest.php b/tests/Gaufrette/Functional/Adapter/PhpseclibSftpTest.php new file mode 100644 index 000000000..9a50ab685 --- /dev/null +++ b/tests/Gaufrette/Functional/Adapter/PhpseclibSftpTest.php @@ -0,0 +1,45 @@ +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->filesystem = new Filesystem(new PhpseclibSftp($this->sftp, $this->baseDir, true)); + } + + protected function tearDown(): void + { + if (!isset($this->sftp)) { + return; + } + + $this->sftp->rmdir($this->baseDir); + } +} diff --git a/tests/Gaufrette/Functional/Adapter/SafeLocalTest.php b/tests/Gaufrette/Functional/Adapter/SafeLocalTest.php index be216eb03..00447cc87 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(): void { if (!file_exists($this->getDirectory())) { mkdir($this->getDirectory()); @@ -16,29 +16,18 @@ public function setUp() $this->filesystem = new Filesystem(new SafeLocal($this->getDirectory())); } - public function tearDown() + protected function tearDown(): void { + foreach ($this->filesystem->keys() as $key) { + $this->filesystem->delete($key); + } + $this->filesystem = null; - if (file_exists($this->getDirectory())) { - $iterator = new \RecursiveIteratorIterator( - new \RecursiveDirectoryIterator( - $this->getDirectory(), - \FilesystemIterator::SKIP_DOTS | \FilesystemIterator::UNIX_PATHS - ) - ); - - foreach ($iterator as $item) { - if ($item->isDir()) { - rmdir(strval($item)); - } else { - unlink(strval($item)); - } - } - } + rmdir($this->getDirectory()); } - private function getDirectory() + private function getDirectory(): string { return sprintf('%s/filesystem', __DIR__); } diff --git a/tests/Gaufrette/Functional/Adapter/SftpTest.php b/tests/Gaufrette/Functional/Adapter/SftpTest.php deleted file mode 100644 index 536b87801..000000000 --- a/tests/Gaufrette/Functional/Adapter/SftpTest.php +++ /dev/null @@ -1,9 +0,0 @@ -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.'); } - @touch(__DIR__ . '/test.zip'); - $this->filesystem = new Filesystem(new Zip(__DIR__ . '/test.zip')); } - public function tearDown() + protected function tearDown(): void { parent::tearDown(); @@ -27,25 +27,11 @@ public function tearDown() /** * @test - * @expectedException \RuntimeException * @group functional */ - public function shouldNotAcceptInvalidZipArchive() + public function shouldNotAcceptInvalidZipArchive(): void { + $this->expectException(\RuntimeException::class); new Zip(__FILE__); } - - /** - * @test - * @group functional - */ - public function shouldCreateNewZipArchive() - { - $tmp = tempnam(sys_get_temp_dir(), uniqid()); - $za = new Zip($tmp); - - $this->assertFileExists($tmp); - - return $za; - } } diff --git a/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php b/tests/Gaufrette/Functional/FileStream/FunctionalTestCase.php index 4c1fc5e20..4a041436d 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 +abstract class FunctionalTestCase extends TestCase { protected $filesystem; @@ -26,10 +27,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'); } /** @@ -45,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'); @@ -61,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'); @@ -150,17 +151,50 @@ 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 */ 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'); $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 @@ -168,19 +202,19 @@ public function shouldUnlinkFile() 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() { - 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..50f3fdc1e 100644 --- a/tests/Gaufrette/Functional/FileStream/InMemoryBufferTest.php +++ b/tests/Gaufrette/Functional/FileStream/InMemoryBufferTest.php @@ -7,9 +7,9 @@ class InMemoryBufferTest extends FunctionalTestCase { - public function setUp() + protected function setUp(): void { - $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 60467cb54..49e5a8e60 100644 --- a/tests/Gaufrette/Functional/FileStream/LocalTest.php +++ b/tests/Gaufrette/Functional/FileStream/LocalTest.php @@ -9,23 +9,51 @@ class LocalTest extends FunctionalTestCase { protected $directory; - public function setUp() + protected function setUp(): void { - $this->directory = __DIR__.DIRECTORY_SEPARATOR.'filesystem'; - @mkdir($this->directory); - @chmod($this->directory, 0777); - $this->filesystem = new Filesystem(new LocalAdapter($this->directory, 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)); $this->registerLocalFilesystemInStream(); } - public function tearDown() + /** + * @test + */ + public function shouldChmodDirectory(): void { - if (is_file($file = $this->directory.DIRECTORY_SEPARATOR.'test.txt')) { - @unlink($file); + if (strtolower(substr(PHP_OS, 0, 3)) === 'win') { + $this->markTestSkipped('Chmod and umask are not available on Windows.'); } - if (is_dir($this->directory)) { - @rmdir($this->directory); + + $r = fopen('gaufrette://filestream/foo/bar', 'a+'); + fclose($r); + + $perms = fileperms($this->directory . '/foo/'); + $this->assertEquals('0770', substr(sprintf('%o', $perms), -4)); + } + + protected function tearDown(): void + { + $adapter = $this->filesystem->getAdapter(); + + foreach ($this->filesystem->keys() as $key) { + $adapter->delete($key); } + + $this->filesystem = null; + + rmdir($this->directory); + } + + /** + * @test + */ + public function shouldSupportsDirectory(): void + { + $this->assertFileExists('gaufrette://filestream/subdir'); + $this->assertDirectoryExists('gaufrette://filestream/subdir'); } } 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/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'); diff --git a/tests/Gaufrette/Functional/adapters/Dropbox.php.dist b/tests/Gaufrette/Functional/adapters/Dropbox.php.dist deleted file mode 100644 index 5e1629e98..000000000 --- a/tests/Gaufrette/Functional/adapters/Dropbox.php.dist +++ /dev/null @@ -1,59 +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/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; diff --git a/tests/Gaufrette/Functional/adapters/GoogleCloudStorage.php.dist b/tests/Gaufrette/Functional/adapters/GoogleCloudStorage.php.dist new file mode 100644 index 000000000..f092c3007 --- /dev/null +++ b/tests/Gaufrette/Functional/adapters/GoogleCloudStorage.php.dist @@ -0,0 +1,28 @@ +setApplicationName('Gaufrette'); +$client->addScope(Google\Service\Storage::DEVSTORAGE_FULL_CONTROL); +$client->useApplicationDefaultCredentials(); + +$service = new \Google\Service\Storage($client); + +return new GoogleCloudStorage( + $service, + $bucketName, + [ + GoogleCloudStorage::OPTION_CREATE_BUCKET_IF_NOT_EXISTS => true, + GoogleCloudStorage::OPTION_PROJECT_ID => $projectId, + GoogleCloudStorage::OPTION_LOCATION => $bucketLocation, + ], + true +); 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); 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; 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();