Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement stream wrapper for gs:// #323

Merged
merged 67 commits into from
Feb 22, 2017
Merged

Implement stream wrapper for gs:// #323

merged 67 commits into from
Feb 22, 2017

Conversation

chingor13
Copy link
Contributor

@chingor13 chingor13 commented Jan 24, 2017

This implementation creates 2 new classes:

  • Uploader\StreamableUploader - a special case of ResumableUploader where we do not know the full length of the document we're trying to upload. It uses an in memory buffer to collect data to the point where we can write a chunk to the server using the resumable upload API.
  • Storage\StreamWrapper - implements the callbacks necessary for a stream_wrapper. It delegates read operations to a GuzzleHttp stream and write operations to the StreamableUploader.

Note: this will be squashed when it's ready.

Add StreamWrapper registration instructions to README.

Add ability to register/unregister the StreamWrapper.

first stab at a StreamingUploader class

Refactor uploader

Add bucket->getStreamableUploader()

Can pass storage options to the uploader and downloader streams via the stream context

move register functions into the Storage namespace, autoloaded by composer

add StreamingUploaderTest

cannot lazy load because we need to know if the file open succeeded

rename StreamingUploader to StreamableUploader to match convention

Add test for StreamableUploader to test batching

Add documentation for Bucket#getStreamableUploader with example.

Adding tests for StreamWrapper with the most common use cases.

Fix README with the correct usage
@googlebot googlebot added the cla: yes This human has signed the Contributor License Agreement. label Jan 24, 2017
The method names need to be underscored instead of camel case because
they are the callback methods needed to implement a stream wrapper.
Copy link
Contributor

@jdpedrie jdpedrie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work so far!

$protocol = $protocol ?: 'gs';
if (!in_array($protocol, stream_get_wrappers())) {
stream_wrapper_register($protocol, 'Google\Cloud\Storage\StreamWrapper')
or die("Failed to register '$protocol://' protocol");

This comment was marked as spam.

This comment was marked as spam.

{
$protocol = $protocol ?: 'gs';
if (!in_array($protocol, stream_get_wrappers())) {
stream_wrapper_register($protocol, 'Google\Cloud\Storage\StreamWrapper')

This comment was marked as spam.

@@ -0,0 +1,17 @@
<?php

This comment was marked as spam.

$this->file = substr($url['path'], 1);
$this->mode = $mode;

$client = $this->getOption('client') ?: new StorageClient();

This comment was marked as spam.

This comment was marked as spam.

{
const DEFAULT_WRITE_CHUNK_SIZE = 262144;

public function __construct(...$args)

This comment was marked as spam.

This comment was marked as spam.

* 'gs'.
* @throws \RuntimeException
*/
public static function registerStreamWrapper(string $protocol = null)

This comment was marked as spam.

This comment was marked as spam.

*/
public static function getDefaultClient()
{
return self::$defaultClient;

This comment was marked as spam.

This comment was marked as spam.

try {
$response = $this->requestWrapper->send($request, $this->requestOptions);
} catch (GoogleException $ex) {
throw new GoogleException(

This comment was marked as spam.


try {
$response = $this->requestWrapper->send($request, $this->requestOptions);
} catch (GoogleException $ex) {

This comment was marked as spam.

This comment was marked as spam.

Copy link
Contributor

@bshaffer bshaffer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer we not use static methods here - The storage client should be instantiated first, so that authentication can be set normally, i.e.

$storage = new StorageClient([
    'projectId' => $projectId,
    // any other configuration options
]);
$storage->registerStreamWrapper();

@@ -217,4 +217,32 @@ public function createBucket($name, array $options = [])
$response = $this->connection->insertBucket($options + ['name' => $name, 'project' => $this->projectId]);
return new Bucket($this->connection, $name, $response);
}

/**
* Register a this StorageClient as the handler for stream reading/writing.

This comment was marked as spam.

README.md Outdated
require 'vendor/autoload.php';

use Google\Cloud\Storage\StorageClient;
StorageClient::registerStreamWrapper();

This comment was marked as spam.

Copy link
Contributor

@dwsupplee dwsupplee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Everything is coming along nicely :). Great work 👍

* 'gs'.
* @throws \RuntimeException
*/
public function registerAsStreamWrapper(string $protocol = null)

This comment was marked as spam.

*/
public function registerAsStreamWrapper(string $protocol = null)
{
if (StreamWrapper::register($protocol)) {

This comment was marked as spam.

This comment was marked as spam.

$protocol = $protocol ?: 'gs';
if (!in_array($protocol, stream_get_wrappers())) {
if (!stream_wrapper_register($protocol, StreamWrapper::class)) {
throw new RuntimeException("Failed to register '$protocol://' protocol");

This comment was marked as spam.

* 'gs'.
* @throws \RuntimeException
*/
public static function register(string $protocol = null)

This comment was marked as spam.

);
} elseif ($this->isReadable()) {
try {
$this->stream = $this->bucket->object($this->file)->downloadAsStream($this->getOptions());

This comment was marked as spam.

This comment was marked as spam.

* Uploader that is a special case of the ResumableUploader where we can write
* the file contents in a streaming manner.
*/
class StreamableUploader extends ResumableUploader

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

*/
public function __construct()
{
call_user_func_array(array($this, 'parent::__construct'), func_get_args());

This comment was marked as spam.

*/
private function resetBuffer($data = "")
{
$this->buffer = new BufferStream($this->chunkSize);

This comment was marked as spam.

This comment was marked as spam.

* Write some partial data. If there's enough data to send a chunk,
* then we will send a chunk. Any remaining data is sent on close.
*
* @param mixed $data The data being written. Can be a string or stream.

This comment was marked as spam.

// Must be public according to the PHP documentation
public $context;

// a GuzzleHttp\Psr7\StreamInterface instance

This comment was marked as spam.

Copy link
Contributor

@jdpedrie jdpedrie left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good! Just a couple non-code things noted. David should still review before merge. He has deeper knowledge of Storage than I do.

/**
* Registers this StorageClient as the handler for stream reading/writing.
*
* @param string $protocol The name of the protocol to use. Defaults to

This comment was marked as spam.


private $protocol;
private $bucket;
private $file;

This comment was marked as spam.

return true;
}

private function returnError($message, $flags)

This comment was marked as spam.

This comment was marked as spam.

@jdpedrie
Copy link
Contributor

One other thing... rather than all these @codingStandardsIgnoreStart and @codingStandardsIgnoreEnd annotations, let's just modify the ruleset to exclude that rule from being applied to StreamWrapper.php.

./ruleset.xml

<?xml version="1.0"?>
<ruleset name="Google-Cloud-PHP-PSR2">
  <rule ref="PSR2" />
  <rule ref="Generic.Files.LineLength">
    <exclude-pattern>src/*/V[0-9]+</exclude-pattern>
  </rule>
  <rule ref="PSR1.Methods.CamelCapsMethodName">
    <exclude-pattern>src/Storage/StreamWrapper.php</exclude-pattern>
  </rule>
  <file>src</file>
</ruleset>

Ignore the camel case method name rule for StreamWrapper as the required
callback method names are snake cased.
@tmatsuo
Copy link
Contributor

tmatsuo commented Feb 17, 2017

@jdpedrie Thanks!

@dwsupplee Any chance you can take a look? I'd like to publish our WordPress plugin before Next ;)

@dwsupplee
Copy link
Contributor

Absolutely! Sorry for the delay. I'll do a final once over today :).

Copy link
Contributor

@dwsupplee dwsupplee left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for all your hard work on this so far 👊 . I tested the code against quite a few scenarios lats night and it held up very well.

Outside of the comments, LGTM!

* Example:
* ```
* $uploader = $bucket->getStreamableUploader(
* "initial contents",

This comment was marked as spam.

* ['name' => 'data.txt']
* );
*
* $uploader->write('some line');

This comment was marked as spam.

* applied using md5 hashing functionality. If true and the
* calculated hash does not match that of the upstream server the
* upload will be rejected.
* @type int $chunkSize If provided the upload will be done in chunks.

This comment was marked as spam.

* for best performance it is recommended to pass in a cached
* version of the already calculated SHA.
* }
* @return StreamableUploader

This comment was marked as spam.

* @param string $file Optional file to try to write.
* @return boolean
*/
public function isWritable($file = null)

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

This comment was marked as spam.

* @param string|StreamInterface $data Data to write
* @return int The number of bytes written
*/
public function write($data)

This comment was marked as spam.

/**
* Triggers the upload process.
*
* @param bool $remainder If true, send the all the remaining data and close

This comment was marked as spam.


/**
* @group storage
* @group streamWrapper

This comment was marked as spam.

*/
public function dir_rewinddir()
{
$this->directoryGenerator = $this->bucket->objects([

This comment was marked as spam.

/**
* Callback handler for reading an entry from a directory handle.
*
* @return string

This comment was marked as spam.

@chingor13
Copy link
Contributor Author

The travis runs appear to be borked and I don't have access to kick the jobs.

@dwsupplee
Copy link
Contributor

I cancelled the queued builds except for the latest. That should help speed things up, but we may be at the mercy of other builds running in the org.

@dwsupplee
Copy link
Contributor

dwsupplee commented Feb 22, 2017

@chingor13 is there anything else you would like to touch on before we merge?

@chingor13
Copy link
Contributor Author

@dwsupplee I think it's good to go :)

@dwsupplee dwsupplee merged commit 4ac28cf into googleapis:master Feb 22, 2017
@dwsupplee
Copy link
Contributor

Awesome! Thanks so much for this.

@jdpedrie
Copy link
Contributor

Great work guys!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cla: yes This human has signed the Contributor License Agreement.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants