PluginProbe
Media Cloud Sync / 1.1.1
Media Cloud Sync v1.1.1
1.4.2 1.4.1 1.4.0 1.3.12 1.3.11 1.3.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.2.0 1.2.10 1.2.11 1.2.12 1.2.13 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 All 36 releases
media-cloud-sync / includes / sdk / google / google / cloud-storage / src / StreamWrapper.php

StreamWrapper.php in Media Cloud Sync 1.1.1, at includes/sdk/google/google/cloud-storage/src/StreamWrapper.php

688 lines 23.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 /**
4 * Copyright 2017 Google Inc. All Rights Reserved.
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18 namespace Dudlewebs\WPMCS\Google\Cloud\Storage;
19
20 use Dudlewebs\WPMCS\Google\Cloud\Core\Exception\NotFoundException;
21 use Dudlewebs\WPMCS\Google\Cloud\Core\Exception\ServiceException;
22 use Dudlewebs\WPMCS\Google\Cloud\Storage\Bucket;
23 use Dudlewebs\WPMCS\GuzzleHttp\Psr7\CachingStream;
24 /**
25 * A streamWrapper implementation for handling `gs://bucket/path/to/file.jpg`.
26 * Note that you can only open a file with mode 'r', 'rb', 'rt', 'w', 'wb', 'wt', 'a', 'ab', or 'at'.
27 *
28 * See: http://php.net/manual/en/class.streamwrapper.php
29 */
30 class StreamWrapper
31 {
32 const DEFAULT_PROTOCOL = 'gs';
33 const FILE_WRITABLE_MODE = 33206;
34 // 100666 in octal
35 const FILE_READABLE_MODE = 33060;
36 // 100444 in octal
37 const DIRECTORY_WRITABLE_MODE = 16895;
38 // 40777 in octal
39 const DIRECTORY_READABLE_MODE = 16676;
40 // 40444 in octal
41 const TAIL_NAME_SUFFIX = '~';
42 /**
43 * @var resource|null Must be public according to the PHP documentation.
44 *
45 * Contains array of context options in form ['protocol' => ['option' => value]].
46 * Options used by StreamWrapper:
47 *
48 * flush (bool) `true`: fflush() will flush output buffer; `false`: fflush() will do nothing
49 */
50 public $context;
51 /**
52 * @var \Psr\Http\Message\StreamInterface
53 */
54 private $stream;
55 /**
56 * @var string Protocol used to open this stream
57 */
58 private $protocol;
59 /**
60 * @var Bucket Reference to the bucket the opened file
61 * lives in or will live in.
62 */
63 private $bucket;
64 /**
65 * @var string Name of the file opened by this stream.
66 */
67 private $file;
68 /**
69 * @var StorageClient[] $clients The default clients to use if using
70 * global methods such as fopen on a stream wrapper. Keyed by protocol.
71 */
72 private static $clients = [];
73 /**
74 * @var ObjectIterator Used for iterating through a directory
75 */
76 private $directoryIterator;
77 /**
78 * @var StorageObject
79 */
80 private $object;
81 /**
82 * @var array Context options passed to stream_open(), used for append mode and flushing.
83 */
84 private $options = [];
85 /**
86 * @var bool `true`: fflush() will flush output buffer and redirect output to the "tail" object.
87 */
88 private $flushing = \false;
89 /**
90 * @var string|null Content type for composed object. Will be filled on first composing.
91 */
92 private $contentType = null;
93 /**
94 * @var bool `true`: writing the "tail" object, next fflush() or fclose() will compose.
95 */
96 private $composing = \false;
97 /**
98 * @var bool `true`: data has been written to the stream.
99 */
100 private $dirty = \false;
101 /**
102 * Ensure we close the stream when this StreamWrapper is destroyed.
103 */
104 public function __destruct()
105 {
106 $this->stream_close();
107 }
108 /**
109 * Starting PHP 7.4, this is called when include/require is used on a stream.
110 * Absence of this method presents a warning.
111 * https://www.php.net/manual/en/migration74.incompatible.php
112 */
113 public function stream_set_option()
114 {
115 return \false;
116 }
117 /**
118 * Register a StreamWrapper for reading and writing to Google Storage
119 *
120 * @param StorageClient $client The StorageClient configuration to use.
121 * @param string $protocol The name of the protocol to use. **Defaults to**
122 * `gs`.
123 * @throws \RuntimeException
124 */
125 public static function register(StorageClient $client, $protocol = null)
126 {
127 $protocol = $protocol ?: self::DEFAULT_PROTOCOL;
128 if (!in_array($protocol, stream_get_wrappers())) {
129 if (!stream_wrapper_register($protocol, StreamWrapper::class, \STREAM_IS_URL)) {
130 throw new \RuntimeException("Failed to register '{$protocol}://' protocol");
131 }
132 self::$clients[$protocol] = $client;
133 return \true;
134 }
135 return \false;
136 }
137 /**
138 * Unregisters the SteamWrapper
139 *
140 * @param string $protocol The name of the protocol to unregister. **Defaults
141 * to** `gs`.
142 */
143 public static function unregister($protocol = null)
144 {
145 $protocol = $protocol ?: self::DEFAULT_PROTOCOL;
146 stream_wrapper_unregister($protocol);
147 unset(self::$clients[$protocol]);
148 }
149 /**
150 * Get the default client to use for streams.
151 *
152 * @param string $protocol The name of the protocol to get the client for.
153 * **Defaults to** `gs`.
154 * @return StorageClient
155 */
156 public static function getClient($protocol = null)
157 {
158 $protocol = $protocol ?: self::DEFAULT_PROTOCOL;
159 return self::$clients[$protocol];
160 }
161 /**
162 * Callback handler for when a stream is opened. For reads, we need to
163 * download the file to see if it can be opened.
164 *
165 * @param string $path The path of the resource to open
166 * @param string $mode The fopen mode. Currently supports ('r', 'rb', 'rt', 'w', 'wb', 'wt', 'a', 'ab', 'at')
167 * @param int $flags Bitwise options STREAM_USE_PATH|STREAM_REPORT_ERRORS|STREAM_MUST_SEEK
168 * @param string $openedPath Will be set to the path on success if STREAM_USE_PATH option is set
169 * @return bool
170 */
171 public function stream_open($path, $mode, $flags, &$openedPath)
172 {
173 $client = $this->openPath($path);
174 // strip off 'b' or 't' from the mode
175 $mode = rtrim($mode, 'bt');
176 $options = [];
177 if ($this->context) {
178 $contextOptions = stream_context_get_options($this->context);
179 if (array_key_exists($this->protocol, $contextOptions)) {
180 $options = $contextOptions[$this->protocol] ?: [];
181 }
182 if (isset($options['flush'])) {
183 $this->flushing = (bool) $options['flush'];
184 unset($options['flush']);
185 }
186 $this->options = $options;
187 }
188 if ($mode == 'w') {
189 $this->stream = new WriteStream(null, $options);
190 $this->stream->setUploader($this->bucket->getStreamableUploader($this->stream, $options + ['name' => $this->file]));
191 } elseif ($mode == 'a') {
192 try {
193 $info = $this->bucket->object($this->file)->info();
194 $this->composing = $info['size'] > 0;
195 } catch (NotFoundException $e) {
196 }
197 $this->stream = new WriteStream(null, $options);
198 $name = $this->file;
199 if ($this->composing) {
200 $name .= self::TAIL_NAME_SUFFIX;
201 }
202 $this->stream->setUploader($this->bucket->getStreamableUploader($this->stream, $options + ['name' => $name]));
203 } elseif ($mode == 'r') {
204 try {
205 // Lazy read from the source
206 $options['restOptions']['stream'] = \true;
207 $this->stream = new ReadStream($this->bucket->object($this->file)->downloadAsStream($options));
208 // Wrap the response in a caching stream to make it seekable
209 if (!$this->stream->isSeekable() && $flags & \STREAM_MUST_SEEK) {
210 $this->stream = new CachingStream($this->stream);
211 }
212 } catch (ServiceException $ex) {
213 return $this->returnError($ex->getMessage(), $flags);
214 }
215 } else {
216 return $this->returnError('Unknown stream_open mode.', $flags);
217 }
218 if ($flags & \STREAM_USE_PATH) {
219 $openedPath = $path;
220 }
221 return \true;
222 }
223 /**
224 * Callback handler for when we try to read a certain number of bytes.
225 *
226 * @param int $count The number of bytes to read.
227 *
228 * @return string
229 */
230 public function stream_read($count)
231 {
232 return $this->stream->read($count);
233 }
234 /**
235 * Callback handler for when we try to write data to the stream.
236 *
237 * @param string $data The data to write
238 *
239 * @return int The number of bytes written.
240 */
241 public function stream_write($data)
242 {
243 $result = $this->stream->write($data);
244 $this->dirty = $this->dirty || (bool) $result;
245 return $result;
246 }
247 /**
248 * Callback handler for getting data about the stream.
249 *
250 * @return array
251 */
252 public function stream_stat()
253 {
254 $mode = $this->stream->isWritable() ? self::FILE_WRITABLE_MODE : self::FILE_READABLE_MODE;
255 return $this->makeStatArray(['mode' => $mode, 'size' => $this->stream->getSize()]);
256 }
257 /**
258 * Callback handler for checking to see if the stream is at the end of file.
259 *
260 * @return bool
261 */
262 public function stream_eof()
263 {
264 return $this->stream->eof();
265 }
266 /**
267 * Callback handler for trying to close the stream.
268 */
269 public function stream_close()
270 {
271 if (isset($this->stream)) {
272 $this->stream->close();
273 }
274 if ($this->composing) {
275 if ($this->dirty) {
276 $this->compose();
277 $this->dirty = \false;
278 }
279 try {
280 $this->bucket->object($this->file . self::TAIL_NAME_SUFFIX)->delete();
281 } catch (NotFoundException $e) {
282 }
283 $this->composing = \false;
284 }
285 }
286 /**
287 * Callback handler for trying to seek to a certain location in the stream.
288 *
289 * @param int $offset The stream offset to seek to
290 * @param int $whence Flag for what the offset is relative to. See:
291 * http://php.net/manual/en/streamwrapper.stream-seek.php
292 * @return bool
293 */
294 public function stream_seek($offset, $whence = \SEEK_SET)
295 {
296 if ($this->stream->isSeekable()) {
297 $this->stream->seek($offset, $whence);
298 return \true;
299 }
300 return \false;
301 }
302 /**
303 * Callhack handler for inspecting our current position in the stream
304 *
305 * @return int
306 */
307 public function stream_tell()
308 {
309 return $this->stream->tell();
310 }
311 /**
312 * Callback handler for trying to close an opened directory.
313 *
314 * @return bool
315 */
316 public function dir_closedir()
317 {
318 return \false;
319 }
320 /**
321 * Callback handler for trying to open a directory.
322 *
323 * @param string $path The url directory to open
324 * @param int $options Whether or not to enforce safe_mode
325 * @return bool
326 */
327 public function dir_opendir($path, $options)
328 {
329 $this->openPath($path);
330 return $this->dir_rewinddir();
331 }
332 /**
333 * Callback handler for reading an entry from a directory handle.
334 *
335 * @return string|bool
336 */
337 public function dir_readdir()
338 {
339 $name = $this->directoryIterator->current();
340 if ($name) {
341 $this->directoryIterator->next();
342 return $name;
343 }
344 return \false;
345 }
346 /**
347 * Callback handler for rewind the directory handle.
348 *
349 * @return bool
350 */
351 public function dir_rewinddir()
352 {
353 try {
354 $iterator = $this->bucket->objects(['prefix' => $this->file, 'fields' => 'items/name,nextPageToken']);
355 // The delimiter options do not give us what we need, so instead we
356 // list all results matching the given prefix, enumerate the
357 // iterator, filter and transform results, and yield a fresh
358 // generator containing only the directory listing.
359 $this->directoryIterator = call_user_func(function () use ($iterator) {
360 $yielded = [];
361 $pathLen = strlen($this->makeDirectory($this->file));
362 foreach ($iterator as $object) {
363 $name = substr($object->name(), $pathLen);
364 $parts = explode('/', $name);
365 // since the service call returns nested results and we only
366 // want to yield results directly within the requested directory,
367 // check if we've already yielded this value.
368 if ($parts[0] === "" || in_array($parts[0], $yielded)) {
369 continue;
370 }
371 $yielded[] = $parts[0];
372 yield $name => $parts[0];
373 }
374 });
375 } catch (ServiceException $e) {
376 return \false;
377 }
378 return \true;
379 }
380 /**
381 * Callback handler for trying to create a directory. If no file path is specified,
382 * or STREAM_MKDIR_RECURSIVE option is set, then create the bucket if it does not exist.
383 *
384 * @param string $path The url directory to create
385 * @param int $mode The permissions on the directory
386 * @param int $options Bitwise mask of options. STREAM_MKDIR_RECURSIVE
387 * @return bool
388 */
389 public function mkdir($path, $mode, $options)
390 {
391 $path = $this->makeDirectory($path);
392 $client = $this->openPath($path);
393 $predefinedAcl = $this->determineAclFromMode($mode);
394 try {
395 if ($options & \STREAM_MKDIR_RECURSIVE || $this->file == '') {
396 if (!$this->bucket->exists()) {
397 $client->createBucket($this->bucket->name(), ['predefinedAcl' => $predefinedAcl, 'predefinedDefaultObjectAcl' => $predefinedAcl]);
398 }
399 }
400 // If the file name is empty, we were trying to create a bucket. In this case,
401 // don't create the placeholder file.
402 if ($this->file != '') {
403 $bucketInfo = $this->bucket->info();
404 $ublEnabled = isset($bucketInfo['iamConfiguration']['uniformBucketLevelAccess']) && $bucketInfo['iamConfiguration']['uniformBucketLevelAccess']['enabled'] === \true;
405 // if bucket has uniform bucket level access enabled, don't set ACLs.
406 $acl = [];
407 if (!$ublEnabled) {
408 $acl = ['predefinedAcl' => $predefinedAcl];
409 }
410 // Fake a directory by creating an empty placeholder file whose name ends in '/'
411 $this->bucket->upload('', ['name' => $this->file] + $acl);
412 }
413 } catch (ServiceException $e) {
414 return \false;
415 }
416 return \true;
417 }
418 /**
419 * Callback handler for trying to move a file or directory.
420 *
421 * @param string $from The URL to the current file
422 * @param string $to The URL of the new file location
423 * @return bool
424 */
425 public function rename($from, $to)
426 {
427 $this->openPath($from);
428 $destination = (array) parse_url($to) + ['path' => '', 'host' => ''];
429 $destinationBucket = $destination['host'];
430 $destinationPath = substr($destination['path'], 1);
431 // loop through to rename file and children, if given path is a directory.
432 $objects = $this->bucket->objects(['prefix' => $this->file]);
433 foreach ($objects as $obj) {
434 $oldName = $obj->name();
435 $newPath = str_replace($this->file, $destinationPath, $oldName);
436 try {
437 $obj->rename($newPath, ['destinationBucket' => $destinationBucket]);
438 } catch (ServiceException $e) {
439 return \false;
440 }
441 }
442 return \true;
443 }
444 /**
445 * Callback handler for trying to remove a directory or a bucket. If the path is empty
446 * or '/', the bucket will be deleted.
447 *
448 * Note that the STREAM_MKDIR_RECURSIVE flag is ignored because the option cannot
449 * be set via the `rmdir()` function.
450 *
451 * @param string $path The URL directory to remove. If the path is empty or is '/',
452 * This will attempt to destroy the bucket.
453 * @param int $options Bitwise mask of options.
454 * @return bool
455 */
456 public function rmdir($path, $options)
457 {
458 $path = $this->makeDirectory($path);
459 $this->openPath($path);
460 try {
461 if ($this->file == '') {
462 $this->bucket->delete();
463 return \true;
464 } else {
465 return $this->unlink($path);
466 }
467 } catch (ServiceException $e) {
468 return \false;
469 }
470 }
471 /**
472 * Callback handler for retrieving the underlaying resource
473 *
474 * @param int $castAs STREAM_CAST_FOR_SELECT|STREAM_CAST_AS_STREAM
475 * @return resource|bool
476 */
477 public function stream_cast($castAs)
478 {
479 return \false;
480 }
481 /**
482 * Callback handler for deleting a file
483 *
484 * @param string $path The URL of the file to delete
485 * @return bool
486 */
487 public function unlink($path)
488 {
489 $client = $this->openPath($path);
490 $object = $this->bucket->object($this->file);
491 try {
492 $object->delete();
493 return \true;
494 } catch (ServiceException $e) {
495 return \false;
496 }
497 }
498 /**
499 * Callback handler for retrieving information about a file
500 *
501 * @param string $path The URI to the file
502 * @param int $flags Bitwise mask of options
503 * @return array|bool
504 */
505 public function url_stat($path, $flags)
506 {
507 $client = $this->openPath($path);
508 // if directory
509 $dir = $this->getDirectoryInfo($this->file);
510 if ($dir) {
511 return $this->urlStatDirectory($dir);
512 }
513 return $this->urlStatFile();
514 }
515 /**
516 * Callback handler for fflush() function.
517 *
518 * @return bool
519 */
520 public function stream_flush()
521 {
522 if (!$this->flushing) {
523 return \false;
524 }
525 if (!$this->dirty) {
526 return \true;
527 }
528 if (isset($this->stream)) {
529 $this->stream->close();
530 }
531 if ($this->composing) {
532 $this->compose();
533 }
534 $options = $this->options;
535 $this->stream = new WriteStream(null, $options);
536 $this->stream->setUploader($this->bucket->getStreamableUploader($this->stream, $options + ['name' => $this->file . self::TAIL_NAME_SUFFIX]));
537 $this->composing = \true;
538 $this->dirty = \false;
539 return \true;
540 }
541 /**
542 * Parse the URL and set protocol, filename and bucket.
543 *
544 * @param string $path URL to open
545 * @return StorageClient
546 */
547 private function openPath($path)
548 {
549 $url = (array) parse_url($path) + ['scheme' => '', 'path' => '', 'host' => ''];
550 $this->protocol = $url['scheme'];
551 $this->file = ltrim($url['path'], '/');
552 $client = self::getClient($this->protocol);
553 $this->bucket = $client->bucket($url['host']);
554 return $client;
555 }
556 /**
557 * Given a path, ensure that we return a path that looks like a directory
558 *
559 * @param string $path
560 * @return string
561 */
562 private function makeDirectory($path)
563 {
564 if ($path == '' or $path == '/') {
565 return '';
566 }
567 if (substr($path, -1) == '/') {
568 return $path;
569 }
570 return $path . '/';
571 }
572 /**
573 * Calculate the `url_stat` response for a directory
574 *
575 * @return array|bool
576 */
577 private function urlStatDirectory(StorageObject $object)
578 {
579 $stats = [];
580 $info = $object->info();
581 // equivalent to 40777 and 40444 in octal
582 $stats['mode'] = $this->bucket->isWritable() ? self::DIRECTORY_WRITABLE_MODE : self::DIRECTORY_READABLE_MODE;
583 $this->statsFromFileInfo($info, $stats);
584 return $this->makeStatArray($stats);
585 }
586 /**
587 * Calculate the `url_stat` response for a file
588 *
589 * @return array|bool
590 */
591 private function urlStatFile()
592 {
593 try {
594 $this->object = $this->bucket->object($this->file);
595 $info = $this->object->info();
596 } catch (ServiceException $e) {
597 // couldn't stat file
598 return \false;
599 }
600 // equivalent to 100666 and 100444 in octal
601 $stats = array('mode' => $this->bucket->isWritable() ? self::FILE_WRITABLE_MODE : self::FILE_READABLE_MODE);
602 $this->statsFromFileInfo($info, $stats);
603 return $this->makeStatArray($stats);
604 }
605 /**
606 * Given a `StorageObject` info array, extract the available fields into the
607 * provided `$stats` array.
608 *
609 * @param array $info Array provided from a `StorageObject`.
610 * @param array $stats Array to put the calculated stats into.
611 */
612 private function statsFromFileInfo(array &$info, array &$stats)
613 {
614 $stats['size'] = isset($info['size']) ? (int) $info['size'] : null;
615 $stats['mtime'] = isset($info['updated']) ? strtotime($info['updated']) : null;
616 $stats['ctime'] = isset($info['timeCreated']) ? strtotime($info['timeCreated']) : null;
617 }
618 /**
619 * Get the given path as a directory.
620 *
621 * In list objects calls, directories are returned with a trailing slash. By
622 * providing the given path with a trailing slash as a list prefix, we can
623 * check whether the given path exists as a directory.
624 *
625 * If the path does not exist or is not a directory, return null.
626 *
627 * @param string $path
628 * @return StorageObject|null
629 */
630 private function getDirectoryInfo($path)
631 {
632 $scan = $this->bucket->objects(['prefix' => $this->makeDirectory($path), 'resultLimit' => 1, 'fields' => 'items/name,items/size,items/updated,items/timeCreated,nextPageToken']);
633 return $scan->current();
634 }
635 /**
636 * Returns the associative array that a `stat()` response expects using the
637 * provided stats. Defaults the remaining fields to 0.
638 *
639 * @param array $stats Sparse stats entries to set.
640 * @return array
641 */
642 private function makeStatArray($stats)
643 {
644 return array_merge(array_fill_keys(['dev', 'ino', 'mode', 'nlink', 'uid', 'gid', 'rdev', 'size', 'atime', 'mtime', 'ctime', 'blksize', 'blocks'], 0), $stats);
645 }
646 /**
647 * Helper for whether or not to trigger an error or just return false on an error.
648 *
649 * @param string $message The PHP error message to emit.
650 * @param int $flags Bitwise mask of options (STREAM_REPORT_ERRORS)
651 * @return bool Returns false
652 */
653 private function returnError($message, $flags)
654 {
655 if ($flags & \STREAM_REPORT_ERRORS) {
656 trigger_error($message, \E_USER_WARNING);
657 }
658 return \false;
659 }
660 /**
661 * Helper for determining which predefinedAcl to use given a mode.
662 *
663 * @param int $mode Decimal representation of the file system permissions
664 * @return string
665 */
666 private function determineAclFromMode($mode)
667 {
668 if ($mode & 04) {
669 // If any user can read, assume it should be publicRead.
670 return 'publicRead';
671 } elseif ($mode & 040) {
672 // If any group user can read, assume it should be projectPrivate.
673 return 'projectPrivate';
674 }
675 // Otherwise, assume only the project/bucket owner can use the bucket.
676 return 'private';
677 }
678 private function compose()
679 {
680 if (!isset($this->contentType)) {
681 $info = $this->bucket->object($this->file)->info();
682 $this->contentType = $info['contentType'] ?: 'application/octet-stream';
683 }
684 $options = ['destination' => ['contentType' => $this->contentType]];
685 $this->bucket->compose([$this->file, $this->file . self::TAIL_NAME_SUFFIX], $this->file, $options);
686 }
687 }
688