diff --git a/.gitignore b/.gitignore
index d5e9e375a..54a203878 100644
--- a/.gitignore
+++ b/.gitignore
@@ -8,3 +8,5 @@ vendor/
.env
composer.lock
phpunit.xml
+.idea/
+/nbproject/*
\ No newline at end of file
diff --git a/composer.json b/composer.json
index 5f9282606..ee9f32d32 100644
--- a/composer.json
+++ b/composer.json
@@ -36,7 +36,8 @@
"mongodb/mongodb": "^1.1",
"microsoft/windowsazure": "~0.4",
"microsoft/azure-storage": "~0.15.0",
- "akeneo/phpspec-skip-example-extension": "~1.2"
+ "akeneo/phpspec-skip-example-extension": "~1.2",
+ "google/cloud-storage": "^1.1"
},
"suggest": {
"knplabs/knp-gaufrette-bundle": "to use with Symfony2",
@@ -54,6 +55,9 @@
"google/apiclient": "to use GoogleCloudStorage adapter",
"ext-curl": "*",
"ext-mbstring": "*",
+ "ext-mongodb": "*",
+ "mongodb/mongodb": "*",
+ "google/cloud-storage": "to use Google Cloud Client Library",
"ext-fileinfo": "This extension is used to automatically detect the content-type of a file in the AwsS3, OpenCloud, AzureBlogStorage and GoogleCloudStorage adapters"
},
"autoload": {
diff --git a/doc/adapters/google-cloud-client-storage.md b/doc/adapters/google-cloud-client-storage.md
new file mode 100644
index 000000000..1b023d40f
--- /dev/null
+++ b/doc/adapters/google-cloud-client-storage.md
@@ -0,0 +1,66 @@
+---
+currentMenu: google-cloud-client-storage
+---
+
+# Google Cloud Client Storage
+
+This adapter requires an instance of Google\Cloud\Storage\StorageClient that has proper access rights to the bucket you want to use.
+
+For more details see:
+http://googlecloudplatform.github.io/google-cloud-php/
+https://console.cloud.google.com/
+
+In order to get started:
+
+1) Create a project in [Google Cloud Platform](https://console.cloud.google.com/).
+2) Create a bucket for the project in Storage.
+3) Create a Service Account in IAM & Admin section that can write access the bucket, download its key.json file.
+
+**At all times make sure you keep your key.json file private and nobody can access it from the Internet.**
+
+## Example
+
+```php
+ 'your-project-id',
+ 'keyFilePath' => 'path/to/your/project/key.json'
+));
+
+# You can optionally set the directory in the bucket and the acl permissions for all uploaded files...
+# By default Cloud Storage applies the bucket's default object ACL to the object (uploaded file).
+# The example below gives read access to the uploaded files to anyone in the world
+# Note that the public URL of the file IS NOT the bucket's file url,
+# see https://cloud.google.com/storage/docs/access-public-data for details
+
+$adapter = new GoogleCloudClientStorage($storage, 'bucket_name',
+ array(
+ 'directory' => 'bucket_directory',
+ 'acl' => array(
+ 'allUsers' => \Google\Cloud\Storage\Acl::ROLE_READER
+ )
+ )
+);
+
+$key = 'myAmazingFile.txt';
+
+# optional
+$adapter->setMetadata($key,
+ array(
+ 'FileDescription' => 'This is my file. There are many like it, but this one is mine.'
+ )
+);
+
+$filesystem = new Filesystem($adapter);
+
+$filesystem->write($key, 'Uploaded at: '.date('Y-m-d @ H:i:s'), true);
+
+```
+
+Here you can find some more info regarding ACL:
+* [Creating and Managing Access Control Lists (ACLs)](https://cloud.google.com/storage/docs/access-control/create-manage-lists)
+* [Access Control Lists (ACLs)](https://cloud.google.com/storage/docs/access-control/lists)
\ No newline at end of file
diff --git a/doc/adapters/google-cloud-storage.md b/doc/adapters/google-cloud-storage.md
index 7f1f219ce..6203b2080 100644
--- a/doc/adapters/google-cloud-storage.md
+++ b/doc/adapters/google-cloud-storage.md
@@ -9,6 +9,8 @@ To use the GoogleCloudStorage adapter you will need to create a connection using
(https://console.developers.google.com/). You can then create the `\Google_Service_Storage` which is required for the
GoogleCloudStorage adapter.
+Install with: composer require google/cloud-storage
+
## Example
```php
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 9bf58266e..5ddb18c7f 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -39,10 +39,16 @@
-->
+
+
+
+
diff --git a/src/Gaufrette/Adapter/GoogleCloudClientStorage.php b/src/Gaufrette/Adapter/GoogleCloudClientStorage.php
new file mode 100644
index 000000000..229119f26
--- /dev/null
+++ b/src/Gaufrette/Adapter/GoogleCloudClientStorage.php
@@ -0,0 +1,331 @@
+
+ */
+class GoogleCloudClientStorage implements Adapter, MetadataSupporter, ListKeysAware
+{
+ protected $storageClient;
+ protected $bucket;
+ protected $bucketValidated;
+ protected $options = array();
+ protected $metadata = array();
+ protected $resources = array();
+
+ /**
+ * @param Google\Cloud\Storage\StorageClient $service Authenticated storage client class
+ * @param string $bucketName Name of the bucket
+ * @param array $options Options are: "directory" and "acl" (see https://cloud.google.com/storage/docs/access-control/lists)
+ */
+ public function __construct(\Google\Cloud\Storage\StorageClient $storageClient, $bucketName, $options = array())
+ {
+ $this->storageClient = $storageClient;
+ $this->setBucket($bucketName);
+ $this->options = array_replace_recursive(
+ array(
+ 'directory' => '',
+ 'acl' => array()
+ ),
+ $options
+ );
+ $this->options['directory'] = rtrim($this->options['directory'], '/');
+ }
+
+ /**
+ * Get adapter options
+ *
+ * @return array
+ */
+ public function getOptions()
+ {
+ return $this->options;
+ }
+
+ /**
+ * Set adapter options
+ *
+ * @param array $options
+ */
+ public function setOptions($options)
+ {
+ $this->options = array_replace($this->options, $options);
+ }
+
+ protected function computePath($key = null)
+ {
+ if (strlen($this->options['directory']))
+ {
+ return $this->options['directory'].'/'.$key;
+ }
+ return $key;
+ }
+
+ protected function isBucket()
+ {
+ if ($this->bucketValidated === true)
+ {
+ return true;
+ } elseif (!$this->bucket->exists()) {
+ throw new \RuntimeException(sprintf('Bucket %s does not exist.', $this->bucket->name()));
+ }
+ $this->bucketValidated = true;
+ return true;
+ }
+
+ public function setBucket($name)
+ {
+ $this->bucketValidated = null;
+ $this->bucket = $this->storageClient->bucket($name);
+ $this->isBucket();
+ }
+
+ public function getBucket()
+ {
+ return $this->bucket;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function read($key)
+ {
+ $this->isBucket();
+ $object = $this->bucket->object($this->computePath($key));
+ try {
+ $info = $object->info();
+ $this->setResources($key, $info);
+ return $object->downloadAsString();
+ } catch (\Exception $e) {
+ return false;
+ }
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function write($key, $content)
+ {
+ $this->isBucket();
+
+ $options = array(
+ 'resumable' => true,
+ 'name' => $this->computePath($key),
+ 'metadata' => $this->getMetadata($key),
+ );
+
+ $this->bucket->upload(
+ $content,
+ $options
+ );
+
+ $this->updateKeyProperties($key,
+ array(
+ 'acl' => $this->options['acl'],
+ 'metadata' => $this->getMetadata($key)
+ )
+ );
+
+ $size = $this->getResourceByName($key, 'size');
+ return $size === null ? false : $size;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function exists($key)
+ {
+ $this->isBucket();
+ $object = $this->bucket->object($this->computePath($key));
+ return $object->exists();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function isDirectory($key)
+ {
+ return $this->exists($this->computePath(rtrim($key, '/')).'/');
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function listKeys($prefix = null)
+ {
+ $this->isBucket();
+ $keys = array();
+
+ foreach ($this->bucket->objects(array('prefix' => $this->computePath($prefix))) as $e)
+ {
+ $keys[] = $e->name();
+ }
+ sort($keys);
+ return $keys;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function keys()
+ {
+ return $this->listKeys();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function mtime($key)
+ {
+ $this->isBucket();
+ $object = $object = $this->bucket->object($this->computePath($key));
+ $info = $object->info();
+ $this->setResources($key, $info);
+ return strtotime($info['updated']);
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function delete($key)
+ {
+ $this->isBucket();
+ try {
+ $object = $this->bucket->object($this->computePath($key));
+ $object->delete();
+ $this->setMetadata($key, null);
+ } catch (\Exception $e) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function rename($sourceKey, $targetKey)
+ {
+ $this->isBucket();
+
+ $pathedSourceKey = $this->computePath($sourceKey);
+ $pathedTargetKey = $this->computePath($targetKey);
+
+ $object = $this->bucket->object($pathedSourceKey);
+
+ $copiedObject = $object->copy($this->bucket,
+ array(
+ 'name' => $pathedTargetKey
+ )
+ );
+
+ if ($copiedObject->exists())
+ {
+ $this->updateKeyProperties($targetKey,
+ array(
+ 'acl' => $this->options['acl'],
+ 'metadata' => $this->getMetadata($sourceKey)
+ )
+ );
+ $this->setMetadata($targetKey, $this->getMetadata($sourceKey));
+ $this->setMetadata($sourceKey, null);
+ $object->delete();
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setMetadata($key, $metadata)
+ {
+ $this->metadata[$this->computePath($key)] = $metadata;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getMetadata($key)
+ {
+ $pathedKey = $this->computePath($key);
+ if (!isset($this->metadata[$pathedKey]) && $this->exists($pathedKey))
+ {
+ $data = $this->bucket->object($pathedKey)->info();
+ if (isset($data['metadata']))
+ {
+ $this->metadata[$pathedKey] = $data['metadata'];
+ }
+ }
+ return isset($this->metadata[$pathedKey]) ? $this->metadata[$pathedKey] : array();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setResources($key, $data)
+ {
+ $this->resources[$this->computePath($key)] = $data;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getResources($key)
+ {
+ $pathedKey = $this->computePath($key);
+ return isset($this->resources[$pathedKey]) ? $this->resources[$pathedKey] : array();
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getResourceByName($key, $resourceName)
+ {
+ $pathedKey = $this->computePath($key);
+ return isset($this->resources[$pathedKey][$resourceName]) ? $this->resources[$pathedKey][$resourceName] : null;
+ }
+
+ /**
+ * Sets ACL and metadata information.
+ *
+ * @param string $key
+ * @param array $options Can contain "acl" and/or "metadata" arrays.
+ * @return boolean
+ */
+ protected function updateKeyProperties($key, $options = array())
+ {
+ if ($this->exists($key) === false)
+ {
+ return false;
+ }
+
+ $object = $this->bucket->object($this->computePath($key));
+
+ $properties = array_replace_recursive(
+ array(
+ 'acl' => array(),
+ 'metadata' => array()
+ ), $options
+ );
+
+ $acl = $object->acl();
+ foreach ($properties['acl'] as $k => $v)
+ {
+ $acl->add($k, $v);
+ }
+ $object->update(array('metadata' => $properties['metadata']));
+
+ $info = $object->info();
+
+ $this->setResources($key, $info);
+ return true;
+ }
+}
diff --git a/tests/Gaufrette/Functional/Adapter/GoogleCloudClientStorageTest.php b/tests/Gaufrette/Functional/Adapter/GoogleCloudClientStorageTest.php
new file mode 100644
index 000000000..3534d36fb
--- /dev/null
+++ b/tests/Gaufrette/Functional/Adapter/GoogleCloudClientStorageTest.php
@@ -0,0 +1,149 @@
+
+ */
+
+namespace Gaufrette\Functional\Adapter;
+
+class GoogleCloudClientStorageTest extends FunctionalTestCase
+{
+ private $string = 'Yeah mate. No worries, I uploaded just fine. Meow!';
+ private $directory = 'tests';
+
+ public function setUp()
+ {
+ $gccs_project_id = getenv('GCCS_PROJECT_ID');
+ $gccs_bucket_name = getenv('GCCS_BUCKET_NAME');
+ $gccs_json_key_file_path = getenv('GCCS_JSON_KEY_FILE_PATH');
+
+ if (empty($gccs_project_id) || empty($gccs_bucket_name) || empty($gccs_json_key_file_path))
+ {
+ $this->markTestSkipped('Required enviroment variables are not defined.');
+ } elseif (!is_readable($gccs_json_key_file_path)) {
+ $this->markTestSkipped(sprintf('Cannot read JSON key file from "%s".', $gccs_json_key_file_path));
+ }
+
+ $storage = new \Google\Cloud\Storage\StorageClient(
+ array(
+ 'projectId' => $gccs_project_id,
+ 'keyFilePath' => $gccs_json_key_file_path
+ )
+ );
+
+ $adapter = new \Gaufrette\Adapter\GoogleCloudClientStorage($storage, $gccs_bucket_name,
+ array(
+ 'directory' => $this->directory,
+ 'acl' => array(
+ 'allUsers' => \Google\Cloud\Storage\Acl::ROLE_READER
+ )
+ )
+ );
+
+ $this->filesystem = new \Gaufrette\Filesystem($adapter);
+ }
+
+ /**
+ * @test
+ * @group functional
+ * @group gccs
+ *
+ * @expectedException \RuntimeException
+ */
+ public function shouldFailIfBucketIsNotAccessible()
+ {
+ /** @var \Gaufrette\Adapter\GoogleCloudClientStorage $adapter */
+ $adapter = $this->filesystem->getAdapter();
+ $adapter->setBucket('meow_'.mt_rand());
+ }
+
+ /**
+ * @test
+ * @group functional
+ * @group gccs
+ */
+ public function shouldListBucketContent()
+ {
+ $this->assertEquals(strlen($this->string), $this->filesystem->write('Phat/Cat.txt', $this->string, true));
+ $keys = $this->filesystem->keys();
+ $file = $this->directory ? $this->directory.'/Phat/Cat.txt' : 'Phat/Cat.txt';
+ $this->assertTrue(in_array($file, $keys));
+ $this->filesystem->delete('Phat/Cat.txt');
+ }
+
+ /**
+ * @test
+ * @group functional
+ * @group gccs
+ */
+ public function shouldWriteAndReadFile()
+ {
+ $this->assertEquals(strlen($this->string), $this->filesystem->write('Phat/Cat.txt', $this->string, true));
+ $this->assertEquals(strlen($this->string), $this->filesystem->write('Phatter/Cat.txt', $this->string, true));
+
+ $this->assertEquals($this->string, $this->filesystem->read('Phat/Cat.txt'));
+ $this->assertEquals($this->string, $this->filesystem->read('Phatter/Cat.txt'));
+
+ $this->filesystem->delete('Phat/Cat.txt');
+ $this->filesystem->delete('Phatter/Cat.txt');
+ }
+
+ /**
+ * @test
+ * @group functional
+ * @group gccs
+ */
+ public function shouldWriteAndReadFileMetadata()
+ {
+ /** @var \Gaufrette\Adapter\GoogleCloudClientStorage $adapter */
+ $adapter = $this->filesystem->getAdapter();
+ $file = 'PhatCat/Cat.txt';
+ $adapter->setMetadata($file, array('OhMy' => 'I am a cat file!'));
+ $this->assertEquals(strlen($this->string), $this->filesystem->write($file, $this->string, true));
+ $info = $adapter->getMetadata($file);
+ $this->assertEquals($info['OhMy'], 'I am a cat file!');
+ $this->filesystem->delete($file);
+ }
+
+ /**
+ * @test
+ * @group functional
+ * @group gccs
+ */
+ public function shouldWriteAndRenameFile()
+ {
+ /** @var \Gaufrette\Adapter\GoogleCloudClientStorage $adapter */
+ $adapter = $this->filesystem->getAdapter();
+ $file = 'Cat.txt';
+ $adapter->setMetadata($file, array('OhMy' => 'I am a cat file!'));
+ $this->assertEquals(strlen($this->string), $this->filesystem->write($file, $this->string, true));
+ $adapter->rename('Cat.txt', 'Kitten.txt');
+ $this->assertEquals($adapter->getMetadata('Kitten.txt'), $adapter->getResourceByName('Kitten.txt', 'metadata'));
+ $this->filesystem->delete('Kitten.txt');
+ }
+
+ /**
+ * @test
+ * @group functional
+ * @group gccs
+ */
+ public function shouldWriteAndReadPublicFile()
+ {
+ /** @var \Gaufrette\Adapter\GoogleCloudClientStorage $adapter */
+ $adapter = $this->filesystem->getAdapter();
+ $file = 'Cat.txt';
+ $this->assertEquals(strlen($this->string), $this->filesystem->write($file, $this->string, true));
+
+ if ($this->directory)
+ {
+ $public_link = sprintf('https://storage.googleapis.com/%s/%s/Cat.txt', $adapter->getBucket()->name(), $this->directory);
+ } else {
+ $public_link = sprintf('https://storage.googleapis.com/%s/Cat.txt', $adapter->getBucket()->name());
+ }
+
+ $headers = @get_headers($public_link);
+ $this->assertEquals($headers[0], 'HTTP/1.0 200 OK');
+ $this->filesystem->delete('Cat.txt');
+ }
+}
\ No newline at end of file