PluginProbe
WP-Stateless – Google Cloud Storage / 3.0.3
WP-Stateless – Google Cloud Storage v3.0.3
4.4.3 2.1.7 2.1.8 2.1.9 2.2.0 2.2.1 2.2.2 2.2.3 2.2.4 2.2.5 2.2.6 2.2.7 2.3.0 2.3.1 2.3.2 3.0 3.0.1 3.0.2 3.0.3 3.0.4 3.1.0 3.1.1 3.2.0 3.2.1 3.2.2 All 62 releases
wp-stateless / lib / Google / vendor / google / cloud-storage / src / StreamWrapper.php

StreamWrapper.php in WP-Stateless – Google Cloud Storage 3.0.3, at lib/Google/vendor/google/cloud-storage/src/StreamWrapper.php

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