881 lines
25 KiB
PHP
881 lines
25 KiB
PHP
|
|
<?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 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();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* This is called when include/require is used on a stream.
|
||
|
|
*/
|
||
|
|
public function stream_set_option()
|
||
|
|
{
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* This is called when touch is used on a stream. See:
|
||
|
|
* https://www.php.net/manual/en/streamwrapper.stream-metadata.php
|
||
|
|
*/
|
||
|
|
public function stream_metadata($path, $option, $value)
|
||
|
|
{
|
||
|
|
if ($option == STREAM_META_TOUCH) {
|
||
|
|
$this->openPath($path);
|
||
|
|
return $this->touch();
|
||
|
|
}
|
||
|
|
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Creates an empty file if it does not exist.
|
||
|
|
* @return bool Returns true if file exists or has been created, false otherwise.
|
||
|
|
*/
|
||
|
|
private function touch()
|
||
|
|
{
|
||
|
|
$object = $this->bucket->object($this->file);
|
||
|
|
try {
|
||
|
|
if (!$object->exists()) {
|
||
|
|
$this->bucket->upload('', [
|
||
|
|
'name' => $this->file
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
return true;
|
||
|
|
} catch (NotFoundException $e) {
|
||
|
|
}
|
||
|
|
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)
|
||
|
|
{
|
||
|
|
$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 = [
|
||
|
|
'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);
|
||
|
|
}
|
||
|
|
}
|