StreamWrapper.php 25 KB

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