123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850 |
- <?php
- /**
- * Copyright 2017 Google Inc. All Rights Reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- namespace Google\Cloud\Storage;
- use Google\Cloud\Core\Exception\NotFoundException;
- use Google\Cloud\Core\Exception\ServiceException;
- use Google\Cloud\Storage\Bucket;
- use GuzzleHttp\Psr7\CachingStream;
- /**
- * A streamWrapper implementation for handling `gs://bucket/path/to/file.jpg`.
- * Note that you can only open a file with mode 'r', 'rb', 'rt', 'w', 'wb', 'wt', 'a', 'ab', or 'at'.
- *
- * See: http://php.net/manual/en/class.streamwrapper.php
- */
- class StreamWrapper
- {
- const DEFAULT_PROTOCOL = 'gs';
- const FILE_WRITABLE_MODE = 33206; // 100666 in octal
- const FILE_READABLE_MODE = 33060; // 100444 in octal
- const DIRECTORY_WRITABLE_MODE = 16895; // 40777 in octal
- const DIRECTORY_READABLE_MODE = 16676; // 40444 in octal
- const TAIL_NAME_SUFFIX = '~';
- /**
- * @var resource|null Must be public according to the PHP documentation.
- *
- * Contains array of context options in form ['protocol' => ['option' => value]].
- * Options used by StreamWrapper:
- *
- * flush (bool) `true`: fflush() will flush output buffer; `false`: fflush() will do nothing
- */
- public $context;
- /**
- * @var \Psr\Http\Message\StreamInterface
- */
- private $stream;
- /**
- * @var string Protocol used to open this stream
- */
- private $protocol;
- /**
- * @var Bucket Reference to the bucket the opened file
- * lives in or will live in.
- */
- private $bucket;
- /**
- * @var string Name of the file opened by this stream.
- */
- private $file;
- /**
- * @var StorageClient[] $clients The default clients to use if using
- * global methods such as fopen on a stream wrapper. Keyed by protocol.
- */
- private static $clients = [];
- /**
- * @var ObjectIterator Used for iterating through a directory
- */
- private $directoryIterator;
- /**
- * @var StorageObject
- */
- private $object;
- /**
- * @var array Context options passed to stream_open(), used for append mode and flushing.
- */
- private $options = [];
- /**
- * @var bool `true`: fflush() will flush output buffer and redirect output to the "tail" object.
- */
- private $flushing = false;
- /**
- * @var string|null Content type for composed object. Will be filled on first composing.
- */
- private $contentType = null;
- /**
- * @var bool `true`: writing the "tail" object, next fflush() or fclose() will compose.
- */
- private $composing = false;
- /**
- * @var bool `true`: data has been written to the stream.
- */
- private $dirty = false;
- /**
- * Ensure we close the stream when this StreamWrapper is destroyed.
- */
- public function __destruct()
- {
- $this->stream_close();
- }
- /**
- * Starting PHP 7.4, this is called when include/require is used on a stream.
- * Absence of this method presents a warning.
- * https://www.php.net/manual/en/migration74.incompatible.php
- */
- public function stream_set_option()
- {
- return false;
- }
- /**
- * Register a StreamWrapper for reading and writing to Google Storage
- *
- * @param StorageClient $client The StorageClient configuration to use.
- * @param string $protocol The name of the protocol to use. **Defaults to**
- * `gs`.
- * @throws \RuntimeException
- */
- public static function register(StorageClient $client, $protocol = null)
- {
- $protocol = $protocol ?: self::DEFAULT_PROTOCOL;
- if (!in_array($protocol, stream_get_wrappers())) {
- if (!stream_wrapper_register($protocol, StreamWrapper::class, STREAM_IS_URL)) {
- throw new \RuntimeException("Failed to register '$protocol://' protocol");
- }
- self::$clients[$protocol] = $client;
- return true;
- }
- return false;
- }
- /**
- * Unregisters the SteamWrapper
- *
- * @param string $protocol The name of the protocol to unregister. **Defaults
- * to** `gs`.
- */
- public static function unregister($protocol = null)
- {
- $protocol = $protocol ?: self::DEFAULT_PROTOCOL;
- stream_wrapper_unregister($protocol);
- unset(self::$clients[$protocol]);
- }
- /**
- * Get the default client to use for streams.
- *
- * @param string $protocol The name of the protocol to get the client for.
- * **Defaults to** `gs`.
- * @return StorageClient
- */
- public static function getClient($protocol = null)
- {
- $protocol = $protocol ?: self::DEFAULT_PROTOCOL;
- return self::$clients[$protocol];
- }
- /**
- * Callback handler for when a stream is opened. For reads, we need to
- * download the file to see if it can be opened.
- *
- * @param string $path The path of the resource to open
- * @param string $mode The fopen mode. Currently supports ('r', 'rb', 'rt', 'w', 'wb', 'wt', 'a', 'ab', 'at')
- * @param int $flags Bitwise options STREAM_USE_PATH|STREAM_REPORT_ERRORS|STREAM_MUST_SEEK
- * @param string $openedPath Will be set to the path on success if STREAM_USE_PATH option is set
- * @return bool
- */
- public function stream_open($path, $mode, $flags, &$openedPath)
- {
- $client = $this->openPath($path);
- // strip off 'b' or 't' from the mode
- $mode = rtrim($mode, 'bt');
- $options = [];
- if ($this->context) {
- $contextOptions = stream_context_get_options($this->context);
- if (array_key_exists($this->protocol, $contextOptions)) {
- $options = $contextOptions[$this->protocol] ?: [];
- }
- if (isset($options['flush'])) {
- $this->flushing = (bool)$options['flush'];
- unset($options['flush']);
- }
- $this->options = $options;
- }
- if ($mode == 'w') {
- $this->stream = new WriteStream(null, $options);
- $this->stream->setUploader(
- $this->bucket->getStreamableUploader(
- $this->stream,
- $options + ['name' => $this->file]
- )
- );
- } elseif ($mode == 'a') {
- try {
- $info = $this->bucket->object($this->file)->info();
- $this->composing = ($info['size'] > 0);
- } catch (NotFoundException $e) {
- }
- $this->stream = new WriteStream(null, $options);
- $name = $this->file;
- if ($this->composing) {
- $name .= self::TAIL_NAME_SUFFIX;
- }
- $this->stream->setUploader(
- $this->bucket->getStreamableUploader(
- $this->stream,
- $options + ['name' => $name]
- )
- );
- } elseif ($mode == 'r') {
- try {
- // Lazy read from the source
- $options['restOptions']['stream'] = true;
- $this->stream = new ReadStream(
- $this->bucket->object($this->file)->downloadAsStream($options)
- );
- // Wrap the response in a caching stream to make it seekable
- if (!$this->stream->isSeekable() && ($flags & STREAM_MUST_SEEK)) {
- $this->stream = new CachingStream($this->stream);
- }
- } catch (ServiceException $ex) {
- return $this->returnError($ex->getMessage(), $flags);
- }
- } else {
- return $this->returnError('Unknown stream_open mode.', $flags);
- }
- if ($flags & STREAM_USE_PATH) {
- $openedPath = $path;
- }
- return true;
- }
- /**
- * Callback handler for when we try to read a certain number of bytes.
- *
- * @param int $count The number of bytes to read.
- *
- * @return string
- */
- public function stream_read($count)
- {
- return $this->stream->read($count);
- }
- /**
- * Callback handler for when we try to write data to the stream.
- *
- * @param string $data The data to write
- *
- * @return int The number of bytes written.
- */
- public function stream_write($data)
- {
- $result = $this->stream->write($data);
- $this->dirty = $this->dirty || (bool)$result;
- return $result;
- }
- /**
- * Callback handler for getting data about the stream.
- *
- * @return array
- */
- public function stream_stat()
- {
- $mode = $this->stream->isWritable()
- ? self::FILE_WRITABLE_MODE
- : self::FILE_READABLE_MODE;
- return $this->makeStatArray([
- 'mode' => $mode,
- 'size' => $this->stream->getSize()
- ]);
- }
- /**
- * Callback handler for checking to see if the stream is at the end of file.
- *
- * @return bool
- */
- public function stream_eof()
- {
- return $this->stream->eof();
- }
- /**
- * Callback handler for trying to close the stream.
- */
- public function stream_close()
- {
- if (isset($this->stream)) {
- $this->stream->close();
- }
- if ($this->composing) {
- if ($this->dirty) {
- $this->compose();
- $this->dirty = false;
- }
- try {
- $this->bucket->object($this->file . self::TAIL_NAME_SUFFIX)->delete();
- } catch (NotFoundException $e) {
- }
- $this->composing = false;
- }
- }
- /**
- * Callback handler for trying to seek to a certain location in the stream.
- *
- * @param int $offset The stream offset to seek to
- * @param int $whence Flag for what the offset is relative to. See:
- * http://php.net/manual/en/streamwrapper.stream-seek.php
- * @return bool
- */
- public function stream_seek($offset, $whence = SEEK_SET)
- {
- if ($this->stream->isSeekable()) {
- $this->stream->seek($offset, $whence);
- return true;
- }
- return false;
- }
- /**
- * Callhack handler for inspecting our current position in the stream
- *
- * @return int
- */
- public function stream_tell()
- {
- return $this->stream->tell();
- }
- /**
- * Callback handler for trying to close an opened directory.
- *
- * @return bool
- */
- public function dir_closedir()
- {
- return false;
- }
- /**
- * Callback handler for trying to open a directory.
- *
- * @param string $path The url directory to open
- * @param int $options Whether or not to enforce safe_mode
- * @return bool
- */
- public function dir_opendir($path, $options)
- {
- $this->openPath($path);
- return $this->dir_rewinddir();
- }
- /**
- * Callback handler for reading an entry from a directory handle.
- *
- * @return string|bool
- */
- public function dir_readdir()
- {
- $name = $this->directoryIterator->current();
- if ($name) {
- $this->directoryIterator->next();
- return $name;
- }
- return false;
- }
- /**
- * Callback handler for rewind the directory handle.
- *
- * @return bool
- */
- public function dir_rewinddir()
- {
- try {
- $iterator = $this->bucket->objects([
- 'prefix' => $this->file,
- 'fields' => 'items/name,nextPageToken'
- ]);
- // The delimiter options do not give us what we need, so instead we
- // list all results matching the given prefix, enumerate the
- // iterator, filter and transform results, and yield a fresh
- // generator containing only the directory listing.
- $this->directoryIterator = call_user_func(function () use ($iterator) {
- $yielded = [];
- $pathLen = strlen($this->makeDirectory($this->file));
- foreach ($iterator as $object) {
- $name = substr($object->name(), $pathLen);
- $parts = explode('/', $name);
- // since the service call returns nested results and we only
- // want to yield results directly within the requested directory,
- // check if we've already yielded this value.
- if ($parts[0] === "" || in_array($parts[0], $yielded)) {
- continue;
- }
- $yielded[] = $parts[0];
- yield $name => $parts[0];
- }
- });
- } catch (ServiceException $e) {
- return false;
- }
- return true;
- }
- /**
- * Callback handler for trying to create a directory. If no file path is specified,
- * or STREAM_MKDIR_RECURSIVE option is set, then create the bucket if it does not exist.
- *
- * @param string $path The url directory to create
- * @param int $mode The permissions on the directory
- * @param int $options Bitwise mask of options. STREAM_MKDIR_RECURSIVE
- * @return bool
- */
- public function mkdir($path, $mode, $options)
- {
- $path = $this->makeDirectory($path);
- $client = $this->openPath($path);
- $predefinedAcl = $this->determineAclFromMode($mode);
- try {
- if ($options & STREAM_MKDIR_RECURSIVE || $this->file == '') {
- if (!$this->bucket->exists()) {
- $client->createBucket($this->bucket->name(), [
- 'predefinedAcl' => $predefinedAcl,
- 'predefinedDefaultObjectAcl' => $predefinedAcl
- ]);
- }
- }
- // If the file name is empty, we were trying to create a bucket. In this case,
- // don't create the placeholder file.
- if ($this->file != '') {
- $bucketInfo = $this->bucket->info();
- $ublEnabled = isset($bucketInfo['iamConfiguration']['uniformBucketLevelAccess']) &&
- $bucketInfo['iamConfiguration']['uniformBucketLevelAccess']['enabled'] === true;
- // if bucket has uniform bucket level access enabled, don't set ACLs.
- $acl = [];
- if (!$ublEnabled) {
- $acl = [
- 'predefinedAcl' => $predefinedAcl
- ];
- }
- // Fake a directory by creating an empty placeholder file whose name ends in '/'
- $this->bucket->upload('', [
- 'name' => $this->file,
- ] + $acl);
- }
- } catch (ServiceException $e) {
- return false;
- }
- return true;
- }
- /**
- * Callback handler for trying to move a file or directory.
- *
- * @param string $from The URL to the current file
- * @param string $to The URL of the new file location
- * @return bool
- */
- public function rename($from, $to)
- {
- $this->openPath($from);
- $destination = (array) parse_url($to) + [
- 'path' => '',
- 'host' => ''
- ];
- $destinationBucket = $destination['host'];
- $destinationPath = substr($destination['path'], 1);
- // loop through to rename file and children, if given path is a directory.
- $objects = $this->bucket->objects([
- 'prefix' => $this->file
- ]);
- foreach ($objects as $obj) {
- $oldName = $obj->name();
- $newPath = str_replace($this->file, $destinationPath, $oldName);
- try {
- $obj->rename($newPath, [
- 'destinationBucket' => $destinationBucket
- ]);
- } catch (ServiceException $e) {
- return false;
- }
- }
- return true;
- }
- /**
- * Callback handler for trying to remove a directory or a bucket. If the path is empty
- * or '/', the bucket will be deleted.
- *
- * Note that the STREAM_MKDIR_RECURSIVE flag is ignored because the option cannot
- * be set via the `rmdir()` function.
- *
- * @param string $path The URL directory to remove. If the path is empty or is '/',
- * This will attempt to destroy the bucket.
- * @param int $options Bitwise mask of options.
- * @return bool
- */
- public function rmdir($path, $options)
- {
- $path = $this->makeDirectory($path);
- $this->openPath($path);
- try {
- if ($this->file == '') {
- $this->bucket->delete();
- return true;
- } else {
- return $this->unlink($path);
- }
- } catch (ServiceException $e) {
- return false;
- }
- }
- /**
- * Callback handler for retrieving the underlaying resource
- *
- * @param int $castAs STREAM_CAST_FOR_SELECT|STREAM_CAST_AS_STREAM
- * @return resource|bool
- */
- public function stream_cast($castAs)
- {
- return false;
- }
- /**
- * Callback handler for deleting a file
- *
- * @param string $path The URL of the file to delete
- * @return bool
- */
- public function unlink($path)
- {
- $client = $this->openPath($path);
- $object = $this->bucket->object($this->file);
- try {
- $object->delete();
- return true;
- } catch (ServiceException $e) {
- return false;
- }
- }
- /**
- * Callback handler for retrieving information about a file
- *
- * @param string $path The URI to the file
- * @param int $flags Bitwise mask of options
- * @return array|bool
- */
- public function url_stat($path, $flags)
- {
- $client = $this->openPath($path);
- // if directory
- $dir = $this->getDirectoryInfo($this->file);
- if ($dir) {
- return $this->urlStatDirectory($dir);
- }
- return $this->urlStatFile();
- }
- /**
- * Callback handler for fflush() function.
- *
- * @return bool
- */
- public function stream_flush()
- {
- if (!$this->flushing) {
- return false;
- }
- if (!$this->dirty) {
- return true;
- }
- if (isset($this->stream)) {
- $this->stream->close();
- }
- if ($this->composing) {
- $this->compose();
- }
- $options = $this->options;
- $this->stream = new WriteStream(null, $options);
- $this->stream->setUploader(
- $this->bucket->getStreamableUploader(
- $this->stream,
- $options + ['name' => $this->file . self::TAIL_NAME_SUFFIX]
- )
- );
- $this->composing = true;
- $this->dirty = false;
- return true;
- }
- /**
- * Parse the URL and set protocol, filename and bucket.
- *
- * @param string $path URL to open
- * @return StorageClient
- */
- private function openPath($path)
- {
- $url = (array) parse_url($path) + [
- 'scheme' => '',
- 'path' => '',
- 'host' => ''
- ];
- $this->protocol = $url['scheme'];
- $this->file = ltrim($url['path'], '/');
- $client = self::getClient($this->protocol);
- $this->bucket = $client->bucket($url['host']);
- return $client;
- }
- /**
- * Given a path, ensure that we return a path that looks like a directory
- *
- * @param string $path
- * @return string
- */
- private function makeDirectory($path)
- {
- if ($path == '' or $path == '/') {
- return '';
- }
- if (substr($path, -1) == '/') {
- return $path;
- }
- return $path . '/';
- }
- /**
- * Calculate the `url_stat` response for a directory
- *
- * @return array|bool
- */
- private function urlStatDirectory(StorageObject $object)
- {
- $stats = [];
- $info = $object->info();
- // equivalent to 40777 and 40444 in octal
- $stats['mode'] = $this->bucket->isWritable()
- ? self::DIRECTORY_WRITABLE_MODE
- : self::DIRECTORY_READABLE_MODE;
- $this->statsFromFileInfo($info, $stats);
- return $this->makeStatArray($stats);
- }
- /**
- * Calculate the `url_stat` response for a file
- *
- * @return array|bool
- */
- private function urlStatFile()
- {
- try {
- $this->object = $this->bucket->object($this->file);
- $info = $this->object->info();
- } catch (ServiceException $e) {
- // couldn't stat file
- return false;
- }
- // equivalent to 100666 and 100444 in octal
- $stats = array(
- 'mode' => $this->bucket->isWritable()
- ? self::FILE_WRITABLE_MODE
- : self::FILE_READABLE_MODE
- );
- $this->statsFromFileInfo($info, $stats);
- return $this->makeStatArray($stats);
- }
- /**
- * Given a `StorageObject` info array, extract the available fields into the
- * provided `$stats` array.
- *
- * @param array $info Array provided from a `StorageObject`.
- * @param array $stats Array to put the calculated stats into.
- */
- private function statsFromFileInfo(array &$info, array &$stats)
- {
- $stats['size'] = (isset($info['size']))
- ? (int) $info['size']
- : null;
- $stats['mtime'] = (isset($info['updated']))
- ? strtotime($info['updated'])
- : null;
- $stats['ctime'] = (isset($info['timeCreated']))
- ? strtotime($info['timeCreated'])
- : null;
- }
- /**
- * Get the given path as a directory.
- *
- * In list objects calls, directories are returned with a trailing slash. By
- * providing the given path with a trailing slash as a list prefix, we can
- * check whether the given path exists as a directory.
- *
- * If the path does not exist or is not a directory, return null.
- *
- * @param string $path
- * @return StorageObject|null
- */
- private function getDirectoryInfo($path)
- {
- $scan = $this->bucket->objects([
- 'prefix' => $this->makeDirectory($path),
- 'resultLimit' => 1,
- 'fields' => 'items/name,items/size,items/updated,items/timeCreated,nextPageToken'
- ]);
- return $scan->current();
- }
- /**
- * Returns the associative array that a `stat()` response expects using the
- * provided stats. Defaults the remaining fields to 0.
- *
- * @param array $stats Sparse stats entries to set.
- * @return array
- */
- private function makeStatArray($stats)
- {
- return array_merge(
- array_fill_keys([
- 'dev',
- 'ino',
- 'mode',
- 'nlink',
- 'uid',
- 'gid',
- 'rdev',
- 'size',
- 'atime',
- 'mtime',
- 'ctime',
- 'blksize',
- 'blocks'
- ], 0),
- $stats
- );
- }
- /**
- * Helper for whether or not to trigger an error or just return false on an error.
- *
- * @param string $message The PHP error message to emit.
- * @param int $flags Bitwise mask of options (STREAM_REPORT_ERRORS)
- * @return bool Returns false
- */
- private function returnError($message, $flags)
- {
- if ($flags & STREAM_REPORT_ERRORS) {
- trigger_error($message, E_USER_WARNING);
- }
- return false;
- }
- /**
- * Helper for determining which predefinedAcl to use given a mode.
- *
- * @param int $mode Decimal representation of the file system permissions
- * @return string
- */
- private function determineAclFromMode($mode)
- {
- if ($mode & 0004) {
- // If any user can read, assume it should be publicRead.
- return 'publicRead';
- } elseif ($mode & 0040) {
- // If any group user can read, assume it should be projectPrivate.
- return 'projectPrivate';
- }
- // Otherwise, assume only the project/bucket owner can use the bucket.
- return 'private';
- }
- private function compose()
- {
- if (!isset($this->contentType)) {
- $info = $this->bucket->object($this->file)->info();
- $this->contentType = $info['contentType'] ?: 'application/octet-stream';
- }
- $options = ['destination' => ['contentType' => $this->contentType]];
- $this->bucket->compose([$this->file, $this->file . self::TAIL_NAME_SUFFIX], $this->file, $options);
- }
- }
|