PageRenderTime 55ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

/python/php/sdk/google/appengine/ext/cloud_storage_streams/CloudStorageStreamWrapperTest.php

http://googleappengine.googlecode.com/
PHP | 2312 lines | 1743 code | 265 blank | 304 comment | 33 complexity | 95b101569066224c7fde65dee648af8a MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, Apache-2.0, BSD-3-Clause, GPL-2.0, LGPL-2.1, MIT

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /**
  3. * Copyright 2007 Google Inc.
  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. * Google Cloud Storage Stream Wrapper Tests.
  19. *
  20. * CodeSniffer does not handle files with multiple namespaces well.
  21. * @codingStandardsIgnoreFile
  22. *
  23. */
  24. namespace {
  25. // Mock Memcache class
  26. class Memcache {
  27. // Mock object to validate calls to memcache
  28. static $mock_memcache = null;
  29. public static function setMockMemcache($mock) {
  30. self::$mock_memcache = $mock;
  31. }
  32. public function get($keys, $flags = null) {
  33. return self::$mock_memcache->get($keys, $flags);
  34. }
  35. public function set($key, $value, $flag = null, $expire = 0) {
  36. return self::$mock_memcache->set($key, $value, $flag, $expire);
  37. }
  38. }
  39. // Mock memcached class, used when invalidating cache entries on write.
  40. class Memcached {
  41. // Mock object to validate calls to memcached
  42. static $mock_memcached = null;
  43. public static function setMockMemcached($mock) {
  44. self::$mock_memcached = $mock;
  45. }
  46. public function deleteMulti($keys, $time = 0) {
  47. self::$mock_memcached->deleteMulti($keys, $time);
  48. }
  49. }
  50. } // namespace
  51. namespace google\appengine\ext\cloud_storage_streams {
  52. require_once 'google/appengine/testing/ApiProxyTestBase.php';
  53. use google\appengine\testing\ApiProxyTestBase;
  54. use google\appengine\ext\cloud_storage_streams\CloudStorageClient;
  55. use google\appengine\ext\cloud_storage_streams\CloudStorageReadClient;
  56. use google\appengine\ext\cloud_storage_streams\CloudStorageWriteClient;
  57. use google\appengine\ext\cloud_storage_streams\HttpResponse;
  58. use google\appengine\URLFetchRequest\RequestMethod;
  59. use google\appengine\URLFetchServiceError\ErrorCode;
  60. use google\appengine\runtime\ApplicationError;
  61. class CloudStorageStreamWrapperTest extends ApiProxyTestBase {
  62. public static $allowed_gs_bucket = "";
  63. protected function setUp() {
  64. parent::setUp();
  65. $this->_SERVER = $_SERVER;
  66. if (!defined("GAE_INCLUDE_GS_BUCKETS")) {
  67. define("GAE_INCLUDE_GS_BUCKETS", "foo, bucket/object_name.png, bar, to_bucket");
  68. }
  69. stream_wrapper_register("gs",
  70. "\\google\\appengine\\ext\\cloud_storage_streams\\CloudStorageStreamWrapper",
  71. STREAM_IS_URL);
  72. CloudStorageStreamWrapperTest::$allowed_gs_bucket = "";
  73. // By default disable caching so we don't have to mock out memcache in
  74. // every test
  75. stream_context_set_default(['gs' => ['enable_cache' => false]]);
  76. date_default_timezone_set("UTC");
  77. $this->mock_memcache = $this->getMock('\Memcache');
  78. $this->mock_memcache_call_index = 0;
  79. \Memcache::setMockMemcache($this->mock_memcache);
  80. $this->mock_memcached = $this->getMock('\Memcached');
  81. \Memcached::setMockMemcached($this->mock_memcached);
  82. $this->triggered_errors = [];
  83. set_error_handler(array($this, "errorHandler"));
  84. }
  85. public function errorHandler(
  86. $errno , $errstr, $errfile=null, $errline=null, $errcontext=null) {
  87. $this->triggered_errors[] = ["errno" => $errno, "errstr" => $errstr];
  88. }
  89. protected function tearDown() {
  90. stream_wrapper_unregister("gs");
  91. $_SERVER = $this->_SERVER;
  92. parent::tearDown();
  93. }
  94. /**
  95. * @dataProvider invalidGCSPaths
  96. */
  97. public function testInvalidPathName($path) {
  98. $this->assertFalse(fopen($path, "r"));
  99. $this->assertEquals(E_WARNING, $this->triggered_errors[0]["errno"]);
  100. }
  101. public function invalidGCSPaths() {
  102. return [["gs:///object.png"],
  103. ["gs://"],
  104. ];
  105. }
  106. /**
  107. * @dataProvider invalidGCSBuckets
  108. */
  109. public function testInvalidBucketName($bucket_name) {
  110. $gcs_name = sprintf('gs://%s/file.txt', $bucket_name);
  111. $this->assertFalse(fopen($gcs_name, 'r'));
  112. $this->assertEquals(E_USER_ERROR, $this->triggered_errors[0]["errno"]);
  113. $this->assertEquals("Invalid cloud storage bucket name '$bucket_name'",
  114. $this->triggered_errors[0]["errstr"]);
  115. $this->assertEquals(E_WARNING, $this->triggered_errors[1]["errno"]);
  116. $this->assertStringStartsWith("fopen($gcs_name): failed to open stream",
  117. $this->triggered_errors[1]["errstr"]);
  118. }
  119. public function invalidGCSBuckets() {
  120. return [["BadBucketName"],
  121. [".another_bad_bucket"],
  122. ["a"],
  123. ["goog_bucket"],
  124. [str_repeat('a', 224)],
  125. ["a.bucket"],
  126. ["foobar" . str_repeat('a', 64)],
  127. ];
  128. }
  129. /**
  130. * @dataProvider invalidGCSModes
  131. */
  132. public function testInvalidMode($mode) {
  133. $valid_path = "gs://bucket/object_name.png";
  134. $this->assertFalse(fopen($valid_path, $mode));
  135. $this->assertEquals(E_WARNING, $this->triggered_errors[0]["errno"]);
  136. $this->assertStringStartsWith(
  137. "fopen($valid_path): failed to open stream",
  138. $this->triggered_errors[0]["errstr"]);
  139. }
  140. public function invalidGCSModes() {
  141. return [["r+"], ["w+"], ["a"], ["a+"], ["x+"], ["c"], ["c+"]];
  142. }
  143. public function testReadObjectSuccess() {
  144. $body = "Hello from PHP";
  145. $this->expectFileReadRequest($body,
  146. 0,
  147. CloudStorageReadClient::DEFAULT_READ_SIZE,
  148. null);
  149. $valid_path = "gs://bucket/object_name.png";
  150. $data = file_get_contents($valid_path);
  151. $this->assertEquals($body, $data);
  152. $this->apiProxyMock->verify();
  153. }
  154. public function testReadObjectFailure() {
  155. $body = "Hello from PHP";
  156. $this->expectGetAccessTokenRequest(CloudStorageClient::READ_SCOPE);
  157. $exected_url = self::makeCloudStorageObjectUrl("bucket",
  158. "/object_name.png");
  159. $request_headers = [
  160. "Authorization" => "OAuth foo token",
  161. "Range" => sprintf("bytes=0-%d",
  162. CloudStorageReadClient::DEFAULT_READ_SIZE-1),
  163. "x-goog-api-version" => 2,
  164. ];
  165. $failure_response = [
  166. "status_code" => 400,
  167. "headers" => [],
  168. "body" => "",
  169. ];
  170. $this->expectHttpRequest($exected_url,
  171. RequestMethod::GET,
  172. $request_headers,
  173. null,
  174. $failure_response);
  175. $this->assertFalse(file_get_contents("gs://bucket/object_name.png"));
  176. $this->apiProxyMock->verify();
  177. $this->assertEquals(E_USER_WARNING, $this->triggered_errors[0]["errno"]);
  178. $this->assertEquals("Cloud Storage Error: BAD REQUEST",
  179. $this->triggered_errors[0]["errstr"]);
  180. $this->assertEquals(E_WARNING, $this->triggered_errors[1]["errno"]);
  181. $this->assertStringStartsWith(
  182. "file_get_contents(gs://bucket/object_name.png): failed to open stream",
  183. $this->triggered_errors[1]["errstr"]);
  184. }
  185. public function testReadObjectTransientFailureThenSuccess() {
  186. $body = "Hello from PHP";
  187. $this->expectGetAccessTokenRequest(CloudStorageClient::READ_SCOPE);
  188. $exected_url = self::makeCloudStorageObjectUrl("bucket",
  189. "/object_name.png");
  190. $request_headers = [
  191. "Authorization" => "OAuth foo token",
  192. "Range" => sprintf("bytes=0-%d",
  193. CloudStorageReadClient::DEFAULT_READ_SIZE-1),
  194. "x-goog-api-version" => 2,
  195. ];
  196. // The first request will fail urlfetch deadline exceeded exception
  197. $failure_response = new ApplicationError(ErrorCode::DEADLINE_EXCEEDED);
  198. $this->expectHttpRequest($exected_url,
  199. RequestMethod::GET,
  200. $request_headers,
  201. null,
  202. $failure_response);
  203. // The second request will succeed.
  204. $response_headers = [
  205. "ETag" => "deadbeef",
  206. "Content-Type" => "text/plain",
  207. "Last-Modified" => "Mon, 02 Jul 2012 01:41:01 GMT",
  208. ];
  209. $response = $this->createSuccessfulGetHttpResponse(
  210. $response_headers,
  211. $body,
  212. 0,
  213. CloudStorageReadClient::DEFAULT_READ_SIZE,
  214. null);
  215. $this->expectHttpRequest($exected_url,
  216. RequestMethod::GET,
  217. $request_headers,
  218. null,
  219. $response);
  220. $data = file_get_contents("gs://bucket/object_name.png");
  221. $this->assertEquals($body, $data);
  222. $this->apiProxyMock->verify();
  223. }
  224. public function testReadObjectUrlFetchExceptionThenSuccess() {
  225. $body = "Hello from PHP";
  226. $this->expectGetAccessTokenRequest(CloudStorageClient::READ_SCOPE);
  227. $exected_url = self::makeCloudStorageObjectUrl("bucket",
  228. "/object_name.png");
  229. $request_headers = [
  230. "Authorization" => "OAuth foo token",
  231. "Range" => sprintf("bytes=0-%d",
  232. CloudStorageReadClient::DEFAULT_READ_SIZE-1),
  233. "x-goog-api-version" => 2,
  234. ];
  235. // The first request will fail with a 500 error, which can be retried.
  236. $failure_response = [
  237. "status_code" => 500,
  238. "headers" => [],
  239. "body" => "",
  240. ];
  241. $this->expectHttpRequest($exected_url,
  242. RequestMethod::GET,
  243. $request_headers,
  244. null,
  245. $failure_response);
  246. // The second request will succeed.
  247. $response_headers = [
  248. "ETag" => "deadbeef",
  249. "Content-Type" => "text/plain",
  250. "Last-Modified" => "Mon, 02 Jul 2012 01:41:01 GMT",
  251. ];
  252. $response = $this->createSuccessfulGetHttpResponse(
  253. $response_headers,
  254. $body,
  255. 0,
  256. CloudStorageReadClient::DEFAULT_READ_SIZE,
  257. null);
  258. $this->expectHttpRequest($exected_url,
  259. RequestMethod::GET,
  260. $request_headers,
  261. null,
  262. $response);
  263. $data = file_get_contents("gs://bucket/object_name.png");
  264. $this->assertEquals($body, $data);
  265. $this->apiProxyMock->verify();
  266. }
  267. public function testReadObjectRepeatedTransientFailure() {
  268. $body = "Hello from PHP";
  269. $this->expectGetAccessTokenRequest(CloudStorageClient::READ_SCOPE);
  270. $request_headers = [
  271. "Authorization" => "OAuth foo token",
  272. "Range" => sprintf("bytes=0-%d",
  273. CloudStorageReadClient::DEFAULT_READ_SIZE-1),
  274. "x-goog-api-version" => 2,
  275. ];
  276. $exected_url = self::makeCloudStorageObjectUrl("bucket",
  277. "/object_name.png");
  278. // The first request will fail with a 500 error, which can be retried.
  279. $failure_response = [
  280. "status_code" => 500,
  281. "headers" => [],
  282. "body" => "",
  283. ];
  284. $this->expectHttpRequest($exected_url,
  285. RequestMethod::GET,
  286. $request_headers,
  287. null,
  288. $failure_response);
  289. $this->expectHttpRequest($exected_url,
  290. RequestMethod::GET,
  291. $request_headers,
  292. null,
  293. $failure_response);
  294. $this->expectHttpRequest($exected_url,
  295. RequestMethod::GET,
  296. $request_headers,
  297. null,
  298. $failure_response);
  299. $this->assertFalse(file_get_contents("gs://bucket/object_name.png"));
  300. $this->apiProxyMock->verify();
  301. $this->assertEquals(E_USER_WARNING, $this->triggered_errors[0]["errno"]);
  302. $this->assertEquals("Cloud Storage Error: INTERNAL SERVER ERROR",
  303. $this->triggered_errors[0]["errstr"]);
  304. $this->assertEquals(E_WARNING, $this->triggered_errors[1]["errno"]);
  305. $this->assertStringStartsWith(
  306. "file_get_contents(gs://bucket/object_name.png): failed to open stream",
  307. $this->triggered_errors[1]["errstr"]);
  308. }
  309. public function testReadObjectCacheHitSuccess() {
  310. $body = "Hello from PHP";
  311. // First call is to create the OAuth token.
  312. $this->expectGetAccessTokenRequest(CloudStorageClient::READ_SCOPE);
  313. // Second call is to retrieve the cached read.
  314. $response = [
  315. 'status_code' => 200,
  316. 'headers' => [
  317. 'Content-Length' => strlen($body),
  318. 'ETag' => 'deadbeef',
  319. 'Content-Type' => 'text/plain',
  320. 'Last-Modified' => 'Mon, 02 Jul 2012 01:41:01 GMT',
  321. ],
  322. 'body' => $body,
  323. ];
  324. $this->mock_memcache->expects($this->at($this->mock_memcache_call_index++))
  325. ->method('get')
  326. ->with($this->stringStartsWith('_ah_gs_read_cache'))
  327. ->will($this->returnValue($response));
  328. // We now expect a read request with If-None-Modified set to our etag.
  329. $request_headers = [
  330. 'Authorization' => 'OAuth foo token',
  331. 'Range' => sprintf('bytes=%d-%d',
  332. 0,
  333. CloudStorageReadClient::DEFAULT_READ_SIZE - 1),
  334. 'If-None-Match' => 'deadbeef',
  335. 'x-goog-api-version' => 2,
  336. ];
  337. $response = [
  338. 'status_code' => HttpResponse::NOT_MODIFIED,
  339. 'headers' => [
  340. ],
  341. ];
  342. $expected_url = $this->makeCloudStorageObjectUrl();
  343. $this->expectHttpRequest($expected_url,
  344. RequestMethod::GET,
  345. $request_headers,
  346. null,
  347. $response);
  348. $options = [ 'gs' => [
  349. 'enable_cache' => true,
  350. 'enable_optimistic_cache' => false,
  351. ]
  352. ];
  353. $ctx = stream_context_create($options);
  354. $valid_path = "gs://bucket/object.png";
  355. $data = file_get_contents($valid_path, false, $ctx);
  356. $this->assertEquals($body, $data);
  357. $this->apiProxyMock->verify();
  358. }
  359. public function testReadObjectCacheWriteSuccess() {
  360. $body = "Hello from PHP";
  361. $this->expectFileReadRequest($body,
  362. 0,
  363. CloudStorageReadClient::DEFAULT_READ_SIZE,
  364. null);
  365. // Don't read the page from the cache
  366. $this->mock_memcache->expects($this->at($this->mock_memcache_call_index++))
  367. ->method('get')
  368. ->with($this->stringStartsWith('_ah_gs_read_cache'))
  369. ->will($this->returnValue(false));
  370. // Expect a write back to the cache
  371. $cache_expiry_seconds = 60;
  372. $this->mock_memcache->expects($this->at($this->mock_memcache_call_index++))
  373. ->method('set')
  374. ->with($this->stringStartsWith('_ah_gs_read_cache'),
  375. $this->anything(),
  376. null,
  377. $cache_expiry_seconds)
  378. ->will($this->returnValue(false));
  379. $options = [ 'gs' => [
  380. 'enable_cache' => true,
  381. 'enable_optimistic_cache' => false,
  382. 'read_cache_expiry_seconds' => $cache_expiry_seconds,
  383. ]
  384. ];
  385. $ctx = stream_context_create($options);
  386. $valid_path = "gs://bucket/object_name.png";
  387. $data = file_get_contents($valid_path, false, $ctx);
  388. $this->assertEquals($body, $data);
  389. $this->apiProxyMock->verify();
  390. }
  391. public function testReadObjectOptimisiticCacheHitSuccess() {
  392. $body = "Hello from PHP";
  393. // First call is to create the OAuth token.
  394. $this->expectGetAccessTokenRequest(CloudStorageClient::READ_SCOPE);
  395. // Second call is to retrieve the cached read.
  396. $response = [
  397. 'status_code' => 200,
  398. 'headers' => [
  399. 'Content-Length' => strlen($body),
  400. 'ETag' => 'deadbeef',
  401. 'Content-Type' => 'text/plain',
  402. 'Last-Modified' => 'Mon, 02 Jul 2012 01:41:01 GMT',
  403. ],
  404. 'body' => $body,
  405. ];
  406. $this->mock_memcache->expects($this->at($this->mock_memcache_call_index++))
  407. ->method('get')
  408. ->with($this->stringStartsWith('_ah_gs_read_cache'))
  409. ->will($this->returnValue($response));
  410. $options = [ 'gs' => [
  411. 'enable_cache' => true,
  412. 'enable_optimistic_cache' => true,
  413. ]
  414. ];
  415. $ctx = stream_context_create($options);
  416. $valid_path = "gs://bucket/object_name.png";
  417. $data = file_get_contents($valid_path, false, $ctx);
  418. $this->assertEquals($body, $data);
  419. $this->apiProxyMock->verify();
  420. }
  421. public function testReadObjectPartialContentResponseSuccess() {
  422. // GCS returns a 206 even if you can obtain all of the file in the first
  423. // read - this test simulates that behavior.
  424. $body = "Hello from PHP.";
  425. $this->expectFileReadRequest($body,
  426. 0,
  427. CloudStorageReadClient::DEFAULT_READ_SIZE,
  428. null,
  429. true);
  430. $valid_path = "gs://bucket/object_name.png";
  431. $data = file_get_contents($valid_path);
  432. $this->assertEquals($body, $data);
  433. $this->apiProxyMock->verify();
  434. }
  435. public function testReadLargeObjectSuccess() {
  436. $body = str_repeat("1234567890", 100000);
  437. $data_len = strlen($body);
  438. $read_chunks = ceil($data_len / CloudStorageReadClient::DEFAULT_READ_SIZE);
  439. $start_chunk = 0;
  440. $etag = null;
  441. for ($i = 0; $i < $read_chunks; $i++) {
  442. $this->expectFileReadRequest($body,
  443. $start_chunk,
  444. CloudStorageReadClient::DEFAULT_READ_SIZE,
  445. $etag,
  446. true);
  447. $start_chunk += CloudStorageReadClient::DEFAULT_READ_SIZE;
  448. $etag = "deadbeef";
  449. }
  450. $valid_path = "gs://bucket/object_name.png";
  451. $fp = fopen($valid_path, "rt");
  452. $data = stream_get_contents($fp);
  453. fclose($fp);
  454. $this->assertEquals($body, $data);
  455. $this->apiProxyMock->verify();
  456. }
  457. public function testSeekReadObjectSuccess() {
  458. $body = "Hello from PHP";
  459. $this->expectFileReadRequest($body,
  460. 0,
  461. CloudStorageReadClient::DEFAULT_READ_SIZE,
  462. null);
  463. $valid_path = "gs://bucket/object_name.png";
  464. $fp = fopen($valid_path, "r");
  465. $this->assertEquals(0, fseek($fp, 4, SEEK_SET));
  466. $this->assertEquals($body[4], fread($fp, 1));
  467. $this->assertEquals(-1, fseek($fp, 100, SEEK_SET));
  468. $this->assertTrue(fclose($fp));
  469. $this->apiProxyMock->verify();
  470. }
  471. public function testReadZeroSizedObjectSuccess() {
  472. $this->expectFileReadRequest("",
  473. 0,
  474. CloudStorageReadClient::DEFAULT_READ_SIZE,
  475. null);
  476. $data = file_get_contents("gs://bucket/object_name.png");
  477. $this->assertEquals("", $data);
  478. $this->apiProxyMock->verify();
  479. }
  480. public function testFileSizeSucess() {
  481. $body = "Hello from PHP";
  482. $this->expectFileReadRequest($body,
  483. 0,
  484. CloudStorageReadClient::DEFAULT_READ_SIZE,
  485. null);
  486. $valid_path = "gs://bucket/object_name.png";
  487. $fp = fopen($valid_path, "r");
  488. $stat = fstat($fp);
  489. fclose($fp);
  490. $this->assertEquals(strlen($body), $stat["size"]);
  491. $this->apiProxyMock->verify();
  492. }
  493. public function testDeleteObjectSuccess() {
  494. $this->expectGetAccessTokenRequest(CloudStorageClient::WRITE_SCOPE);
  495. $request_headers = $this->getStandardRequestHeaders();
  496. $response = [
  497. 'status_code' => 204,
  498. 'headers' => [
  499. ],
  500. ];
  501. $expected_url = $this->makeCloudStorageObjectUrl("my_bucket",
  502. "/some%file.txt");
  503. $this->expectHttpRequest($expected_url,
  504. RequestMethod::DELETE,
  505. $request_headers,
  506. null,
  507. $response);
  508. $this->assertTrue(unlink("gs://my_bucket/some%file.txt"));
  509. $this->apiProxyMock->verify();
  510. }
  511. public function testDeleteObjectFail() {
  512. $this->expectGetAccessTokenRequest(CloudStorageClient::WRITE_SCOPE);
  513. $request_headers = $this->getStandardRequestHeaders();
  514. $response = [
  515. 'status_code' => 404,
  516. 'headers' => [
  517. ],
  518. 'body' => "<?xml version='1.0' encoding='utf-8'?>
  519. <Error>
  520. <Code>NoSuchBucket</Code>
  521. <Message>No Such Bucket</Message>
  522. </Error>",
  523. ];
  524. $expected_url = $this->makeCloudStorageObjectUrl();
  525. $this->expectHttpRequest($expected_url,
  526. RequestMethod::DELETE,
  527. $request_headers,
  528. null,
  529. $response);
  530. $this->assertFalse(unlink("gs://bucket/object.png"));
  531. $this->apiProxyMock->verify();
  532. $this->assertEquals(
  533. [["errno" => E_USER_WARNING,
  534. "errstr" => "Cloud Storage Error: No Such Bucket (NoSuchBucket)"]],
  535. $this->triggered_errors);
  536. }
  537. public function testStatBucketSuccess() {
  538. $this->expectGetAccessTokenRequest(CloudStorageClient::READ_SCOPE);
  539. $request_headers = $this->getStandardRequestHeaders();
  540. $file_results = ['file1.txt', 'file2.txt'];
  541. $response = [
  542. 'status_code' => 200,
  543. 'headers' => [
  544. ],
  545. 'body' => $this->makeGetBucketXmlResponse("", $file_results),
  546. ];
  547. $expected_url = $this->makeCloudStorageObjectUrl("bucket", null);
  548. $expected_query = http_build_query([
  549. "delimiter" => CloudStorageClient::DELIMITER,
  550. "max-keys" => CloudStorageUrlStatClient::MAX_KEYS,
  551. ]);
  552. $this->expectHttpRequest(sprintf("%s?%s", $expected_url, $expected_query),
  553. RequestMethod::GET,
  554. $request_headers,
  555. null,
  556. $response);
  557. // Return a false is writable check from the cache
  558. $this->expectIsWritableMemcacheLookup(true, false);
  559. $this->assertTrue(is_dir("gs://bucket"));
  560. $this->apiProxyMock->verify();
  561. }
  562. public function testStatObjectSuccess() {
  563. $this->expectGetAccessTokenRequest(CloudStorageClient::READ_SCOPE);
  564. // Return the object we want in the second request so we test fetching from
  565. // the marker to get all of the results
  566. $last_modified = 'Mon, 01 Jul 2013 10:02:46 GMT';
  567. $request_headers = $this->getStandardRequestHeaders();
  568. $file_results = [
  569. ['key' => 'object1.png', 'size' => '3337', 'mtime' => $last_modified],
  570. ];
  571. $response = [
  572. 'status_code' => 200,
  573. 'headers' => [
  574. ],
  575. 'body' => $this->makeGetBucketXmlResponse("", $file_results, "foo"),
  576. ];
  577. $expected_url = $this->makeCloudStorageObjectUrl("bucket", null);
  578. $expected_query = http_build_query([
  579. 'delimiter' => CloudStorageClient::DELIMITER,
  580. 'max-keys' => CloudStorageUrlStatClient::MAX_KEYS,
  581. 'prefix' => 'object.png',
  582. ]);
  583. $this->expectHttpRequest(sprintf("%s?%s", $expected_url, $expected_query),
  584. RequestMethod::GET,
  585. $request_headers,
  586. null,
  587. $response);
  588. $this->expectGetAccessTokenRequest(CloudStorageClient::READ_SCOPE);
  589. $file_results = [
  590. ['key' => 'object.png', 'size' => '37337', 'mtime' => $last_modified],
  591. ];
  592. $response['body'] = $this->makeGetBucketXmlResponse("", $file_results);
  593. $expected_query = http_build_query([
  594. 'delimiter' => CloudStorageClient::DELIMITER,
  595. 'max-keys' => CloudStorageUrlStatClient::MAX_KEYS,
  596. 'prefix' => 'object.png',
  597. 'marker' => 'foo',
  598. ]);
  599. $this->expectHttpRequest(sprintf("%s?%s", $expected_url, $expected_query),
  600. RequestMethod::GET,
  601. $request_headers,
  602. null,
  603. $response);
  604. // Don't find the key in the cache, to force a write attempt to the bucket.
  605. $temp_url = $this->makeCloudStorageObjectUrl("bucket",
  606. CloudStorageClient::WRITABLE_TEMP_FILENAME);
  607. $this->expectIsWritableMemcacheLookup(false, false);
  608. $this->expectFileWriteStartRequest(null, null, 'foo', $temp_url, null);
  609. $this->expectIsWritableMemcacheSet(true);
  610. $result = stat("gs://bucket/object.png");
  611. $this->assertEquals(37337, $result['size']);
  612. $this->assertEquals(0100666, $result['mode']);
  613. $this->assertEquals(strtotime($last_modified), $result['mtime']);
  614. $this->apiProxyMock->verify();
  615. }
  616. public function testStatObjectAsFolderSuccess() {
  617. $this->expectGetAccessTokenRequest(CloudStorageClient::READ_SCOPE);
  618. $request_headers = $this->getStandardRequestHeaders();
  619. $last_modified = 'Mon, 01 Jul 2013 10:02:46 GMT';
  620. $file_results = [];
  621. $common_prefixes_results = ['name' => 'a/b/'];
  622. $response = [
  623. 'status_code' => 200,
  624. 'headers' => [
  625. ],
  626. 'body' => $this->makeGetBucketXmlResponse(
  627. 'a/b',
  628. $file_results,
  629. null,
  630. $common_prefixes_results),
  631. ];
  632. $expected_url = $this->makeCloudStorageObjectUrl('bucket', null);
  633. $expected_query = http_build_query([
  634. 'delimiter' => CloudStorageClient::DELIMITER,
  635. 'max-keys' => CloudStorageUrlStatClient::MAX_KEYS,
  636. 'prefix' => 'a/b',
  637. ]);
  638. $this->expectHttpRequest(sprintf("%s?%s", $expected_url, $expected_query),
  639. RequestMethod::GET,
  640. $request_headers,
  641. null,
  642. $response);
  643. // Return a false is writable check from the cache
  644. $this->expectIsWritableMemcacheLookup(true, false);
  645. $this->assertTrue(is_dir('gs://bucket/a/b/'));
  646. $this->apiProxyMock->verify();
  647. }
  648. public function testStatObjectWithCommonPrefixSuccess() {
  649. $this->expectGetAccessTokenRequest(CloudStorageClient::READ_SCOPE);
  650. $request_headers = $this->getStandardRequestHeaders();
  651. $last_modified = 'Mon, 01 Jul 2013 10:02:46 GMT';
  652. $common_prefix_results = ['a/b/c/',
  653. 'a/b/d/',
  654. ];
  655. $response = [
  656. 'status_code' => 200,
  657. 'headers' => [
  658. ],
  659. 'body' => $this->makeGetBucketXmlResponse('a/b',
  660. [],
  661. null,
  662. $common_prefix_results),
  663. ];
  664. $expected_url = $this->makeCloudStorageObjectUrl('bucket', null);
  665. $expected_query = http_build_query([
  666. 'delimiter' => CloudStorageClient::DELIMITER,
  667. 'max-keys' => CloudStorageUrlStatClient::MAX_KEYS,
  668. 'prefix' => 'a/b',
  669. ]);
  670. $this->expectHttpRequest(sprintf("%s?%s", $expected_url, $expected_query),
  671. RequestMethod::GET,
  672. $request_headers,
  673. null,
  674. $response);
  675. // Return a false is writable check from the cache
  676. $this->expectIsWritableMemcacheLookup(true, false);
  677. $this->assertTrue(is_dir('gs://bucket/a/b'));
  678. $this->apiProxyMock->verify();
  679. }
  680. public function testStatObjectFailed() {
  681. $this->expectGetAccessTokenRequest(CloudStorageClient::READ_SCOPE);
  682. $request_headers = $this->getStandardRequestHeaders();
  683. $response = [
  684. 'status_code' => 404,
  685. 'headers' => [
  686. ],
  687. ];
  688. $expected_url = $this->makeCloudStorageObjectUrl("bucket", null);
  689. $expected_query = http_build_query([
  690. 'delimiter' => CloudStorageClient::DELIMITER,
  691. 'max-keys' => CloudStorageUrlStatClient::MAX_KEYS,
  692. 'prefix' => 'object.png',
  693. ]);
  694. $this->expectHttpRequest(sprintf("%s?%s", $expected_url, $expected_query),
  695. RequestMethod::GET,
  696. $request_headers,
  697. null,
  698. $response);
  699. $result = stat("gs://bucket/object.png");
  700. $this->apiProxyMock->verify();
  701. $this->assertEquals(
  702. [["errno" => E_USER_WARNING,
  703. "errstr" => "Cloud Storage Error: NOT FOUND"],
  704. ["errno" => E_WARNING,
  705. "errstr" => "stat(): stat failed for gs://bucket/object.png"]],
  706. $this->triggered_errors);
  707. }
  708. public function testRenameInvalidToPath() {
  709. $this->assertFalse(rename("gs://bucket/object.png", "gs://to/"));
  710. $this->assertEquals(
  711. [["errno" => E_USER_ERROR,
  712. "errstr" => "Invalid cloud storage bucket name 'to'"],
  713. ["errno" => E_USER_ERROR,
  714. "errstr" => "Invalid Google Cloud Storage path: gs://to/"]],
  715. $this->triggered_errors);
  716. }
  717. public function testRenameInvalidFromPath() {
  718. $this->assertFalse(rename("gs://bucket/", "gs://to/object.png"));
  719. $this->assertEquals(
  720. [["errno" => E_USER_ERROR,
  721. "errstr" => "Invalid Google Cloud Storage path: gs://bucket/"]],
  722. $this->triggered_errors);
  723. }
  724. public function testRenameObjectWithoutContextSuccess() {
  725. $this->expectGetAccessTokenRequest(CloudStorageClient::WRITE_SCOPE);
  726. // First there is a stat
  727. $request_headers = $this->getStandardRequestHeaders();
  728. $response = [
  729. 'status_code' => 200,
  730. 'headers' => [
  731. 'Content-Length' => 37337,
  732. 'ETag' => 'abcdef',
  733. 'Content-Type' => 'text/plain',
  734. ],
  735. ];
  736. $expected_url = $this->makeCloudStorageObjectUrl();
  737. $this->expectHttpRequest($expected_url,
  738. RequestMethod::HEAD,
  739. $request_headers,
  740. null,
  741. $response);
  742. // Then there is a copy
  743. $request_headers = [
  744. "Authorization" => "OAuth foo token",
  745. "x-goog-copy-source" => '/bucket/object.png',
  746. "x-goog-copy-source-if-match" => 'abcdef',
  747. "x-goog-metadata-directive" => "COPY",
  748. "x-goog-api-version" => 2,
  749. ];
  750. $response = [
  751. 'status_code' => 200,
  752. 'headers' => [
  753. ]
  754. ];
  755. $expected_url = $this->makeCloudStorageObjectUrl("to_bucket", "/to.png");
  756. $this->expectHttpRequest($expected_url,
  757. RequestMethod::PUT,
  758. $request_headers,
  759. null,
  760. $response);
  761. // Then we unlink the original.
  762. $request_headers = $this->getStandardRequestHeaders();
  763. $response = [
  764. 'status_code' => 204,
  765. 'headers' => [
  766. ],
  767. ];
  768. $expected_url = $this->makeCloudStorageObjectUrl();
  769. $this->expectHttpRequest($expected_url,
  770. RequestMethod::DELETE,
  771. $request_headers,
  772. null,
  773. $response);
  774. $from = "gs://bucket/object.png";
  775. $to = "gs://to_bucket/to.png";
  776. // Simulate the rename is acting on a uploaded file which is then being
  777. // moved into the allowed include bucket which will trigger a warning.
  778. $_FILES['foo']['tmp_name'] = $from;
  779. $this->assertTrue(rename($from, $to));
  780. $this->apiProxyMock->verify();
  781. $this->assertEquals(
  782. [['errno' => E_USER_WARNING,
  783. 'errstr' => sprintf('Moving uploaded file (%s) to an allowed include ' .
  784. 'bucket (%s) which may be vulnerable to local ' .
  785. 'file inclusion (LFI).', $from, 'to_bucket')]],
  786. $this->triggered_errors);
  787. $_FILES = [];
  788. }
  789. public function testRenameObjectWithContextSuccess() {
  790. $this->expectGetAccessTokenRequest(CloudStorageClient::WRITE_SCOPE);
  791. // First there is a stat
  792. $request_headers = $this->getStandardRequestHeaders();
  793. $response = [
  794. 'status_code' => 200,
  795. 'headers' => [
  796. 'Content-Length' => 37337,
  797. 'ETag' => 'abcdef',
  798. // Ensure the pre-existing headers are preserved.
  799. 'Cache-Control' => 'public, max-age=6000',
  800. 'Content-Disposition' => 'attachment; filename=object.png',
  801. 'Content-Encoding' => 'text/plain',
  802. 'Content-Language' => 'en',
  803. // Ensure context overrides original.
  804. 'Content-Type' => 'text/plain',
  805. ],
  806. ];
  807. $expected_url = $this->makeCloudStorageObjectUrl();
  808. $this->expectHttpRequest($expected_url,
  809. RequestMethod::HEAD,
  810. $request_headers,
  811. null,
  812. $response);
  813. // Then there is a copy with new context
  814. $request_headers = [
  815. "Authorization" => "OAuth foo token",
  816. "x-goog-copy-source" => "/bucket/object.png",
  817. "x-goog-copy-source-if-match" => "abcdef",
  818. "x-goog-metadata-directive" => "REPLACE",
  819. "Cache-Control" => "public, max-age=6000",
  820. "Content-Disposition" => "attachment; filename=object.png",
  821. "Content-Encoding" => "text/plain",
  822. "Content-Language" => "en",
  823. "Content-Type" => "image/png",
  824. "x-goog-meta-foo" => "bar",
  825. "x-goog-acl" => "public-read-write",
  826. "x-goog-api-version" => 2,
  827. ];
  828. $response = [
  829. 'status_code' => 200,
  830. 'headers' => [
  831. ]
  832. ];
  833. $expected_url = $this->makeCloudStorageObjectUrl("to_bucket", "/to.png");
  834. $this->expectHttpRequest($expected_url,
  835. RequestMethod::PUT,
  836. $request_headers,
  837. null,
  838. $response);
  839. // Then we unlink the original.
  840. $request_headers = $this->getStandardRequestHeaders();
  841. $response = [
  842. 'status_code' => 204,
  843. 'headers' => [
  844. ],
  845. ];
  846. $expected_url = $this->makeCloudStorageObjectUrl();
  847. $this->expectHttpRequest($expected_url,
  848. RequestMethod::DELETE,
  849. $request_headers,
  850. null,
  851. $response);
  852. $from = "gs://bucket/object.png";
  853. $to = "gs://to_bucket/to.png";
  854. $ctx = stream_context_create([
  855. "gs" => ["Content-Type" => "image/png",
  856. "acl" => "public-read-write",
  857. "metadata" => ["foo"=> "bar"]]]);
  858. $this->assertTrue(rename($from, $to, $ctx));
  859. $this->apiProxyMock->verify();
  860. }
  861. public function testRenameObjectWithContextAllMetaSuccess() {
  862. $this->expectGetAccessTokenRequest(CloudStorageClient::WRITE_SCOPE);
  863. // First there is a stat.
  864. $request_headers = $this->getStandardRequestHeaders();
  865. $response = [
  866. 'status_code' => 200,
  867. 'headers' => [
  868. 'Content-Length' => 37337,
  869. 'ETag' => 'abcdef',
  870. // Ensure context overrides original values.
  871. 'Cache-Control' => 'public, max-age=6000',
  872. 'Content-Disposition' => 'attachment; filename=object.png',
  873. 'Content-Encoding' => 'text/plain',
  874. 'Content-Language' => 'en',
  875. 'Content-Type' => 'text/plain',
  876. ],
  877. ];
  878. $expected_url = $this->makeCloudStorageObjectUrl();
  879. $this->expectHttpRequest($expected_url,
  880. RequestMethod::HEAD,
  881. $request_headers,
  882. null,
  883. $response);
  884. // Then there is a copy with new context.
  885. $request_headers = [
  886. "Authorization" => "OAuth foo token",
  887. "x-goog-copy-source" => "/bucket/object.png",
  888. "x-goog-copy-source-if-match" => "abcdef",
  889. "x-goog-metadata-directive" => "REPLACE",
  890. // All meta heads have had a 2 appended to check that context overrides.
  891. "Cache-Control" => "public, max-age=6002",
  892. "Content-Disposition" => "attachment; filename=object.png2",
  893. "Content-Encoding" => "text/plain2",
  894. "Content-Language" => "en2",
  895. "Content-Type" => "image/png2",
  896. "x-goog-meta-foo" => "bar",
  897. "x-goog-acl" => "public-read-write",
  898. "x-goog-api-version" => 2,
  899. ];
  900. $response = [
  901. 'status_code' => 200,
  902. 'headers' => [
  903. ]
  904. ];
  905. $expected_url = $this->makeCloudStorageObjectUrl("to_bucket", "/to.png");
  906. $this->expectHttpRequest($expected_url,
  907. RequestMethod::PUT,
  908. $request_headers,
  909. null,
  910. $response);
  911. // Then we unlink the original.
  912. $request_headers = $this->getStandardRequestHeaders();
  913. $response = [
  914. 'status_code' => 204,
  915. 'headers' => [
  916. ],
  917. ];
  918. $expected_url = $this->makeCloudStorageObjectUrl();
  919. $this->expectHttpRequest($expected_url,
  920. RequestMethod::DELETE,
  921. $request_headers,
  922. null,
  923. $response);
  924. $from = "gs://bucket/object.png";
  925. $to = "gs://to_bucket/to.png";
  926. $ctx = stream_context_create([
  927. "gs" => [
  928. "acl" => "public-read-write",
  929. "metadata" => ["foo"=> "bar"],
  930. // Metadata heads to override.
  931. "Cache-Control" => "public, max-age=6002",
  932. "Content-Disposition" => "attachment; filename=object.png2",
  933. "Content-Encoding" => "text/plain2",
  934. "Content-Language" => "en2",
  935. "Content-Type" => "image/png2",
  936. ],
  937. ]);
  938. $this->assertTrue(rename($from, $to, $ctx));
  939. $this->apiProxyMock->verify();
  940. }
  941. public function testRenameObjectFromObjectNotFound() {
  942. $this->expectGetAccessTokenRequest(CloudStorageClient::WRITE_SCOPE);
  943. // First there is a stat
  944. $request_headers = $this->getStandardRequestHeaders();
  945. $response = [
  946. 'status_code' => 404,
  947. 'headers' => [
  948. ],
  949. ];
  950. $expected_url = $this->makeCloudStorageObjectUrl();
  951. $this->expectHttpRequest($expected_url,
  952. RequestMethod::HEAD,
  953. $request_headers,
  954. null,
  955. $response);
  956. $from = "gs://bucket/object.png";
  957. $to = "gs://to_bucket/to_object";
  958. $this->assertFalse(rename($from, $to));
  959. $this->apiProxyMock->verify();
  960. $this->assertEquals(
  961. [["errno" => E_USER_WARNING,
  962. "errstr" => "Unable to rename: gs://to_bucket/to_object. " .
  963. "Cloud Storage Error: NOT FOUND"]],
  964. $this->triggered_errors);
  965. }
  966. public function testRenameObjectCopyFailed() {
  967. $this->expectGetAccessTokenRequest(CloudStorageClient::WRITE_SCOPE);
  968. // First there is a stat
  969. $request_headers = $this->getStandardRequestHeaders();
  970. $response = [
  971. 'status_code' => 200,
  972. 'headers' => [
  973. 'Content-Length' => 37337,
  974. 'ETag' => 'abcdef',
  975. 'Content-Type' => 'text/plain',
  976. ],
  977. ];
  978. $expected_url = $this->makeCloudStorageObjectUrl();
  979. $this->expectHttpRequest($expected_url,
  980. RequestMethod::HEAD,
  981. $request_headers,
  982. null,
  983. $response);
  984. // Then there is a copy
  985. $request_headers = [
  986. "Authorization" => "OAuth foo token",
  987. "x-goog-copy-source" => '/bucket/object.png',
  988. "x-goog-copy-source-if-match" => 'abcdef',
  989. "x-goog-metadata-directive" => "COPY",
  990. "x-goog-api-version" => 2,
  991. ];
  992. $response = [
  993. 'status_code' => 412,
  994. 'headers' => [
  995. ]
  996. ];
  997. $expected_url = $this->makeCloudStorageObjectUrl("to_bucket", "/to_object");
  998. $this->expectHttpRequest($expected_url,
  999. RequestMethod::PUT,
  1000. $request_headers,
  1001. null,
  1002. $response);
  1003. $from = "gs://bucket/object.png";
  1004. $to = "gs://to_bucket/to_object";
  1005. $this->assertFalse(rename($from, $to));
  1006. $this->apiProxyMock->verify();
  1007. $this->assertEquals(
  1008. [["errno" => E_USER_WARNING,
  1009. "errstr" => "Error copying to gs://to_bucket/to_object. " .
  1010. "Cloud Storage Error: PRECONDITION FAILED"]],
  1011. $this->triggered_errors);
  1012. }
  1013. public function testRenameObjectUnlinkFailed() {
  1014. $this->expectGetAccessTokenRequest(CloudStorageClient::WRITE_SCOPE);
  1015. // First there is a stat
  1016. $request_headers = $this->getStandardRequestHeaders();
  1017. $response = [
  1018. 'status_code' => 200,
  1019. 'headers' => [
  1020. 'Content-Length' => 37337,
  1021. 'ETag' => 'abcdef',
  1022. 'Content-Type' => 'text/plain',
  1023. ],
  1024. ];
  1025. $expected_url = $this->makeCloudStorageObjectUrl();
  1026. $this->expectHttpRequest($expected_url,
  1027. RequestMethod::HEAD,
  1028. $request_headers,
  1029. null,
  1030. $response);
  1031. // Then there is a copy
  1032. $request_headers = [
  1033. "Authorization" => "OAuth foo token",
  1034. "x-goog-copy-source" => '/bucket/object.png',
  1035. "x-goog-copy-source-if-match" => 'abcdef',
  1036. "x-goog-metadata-directive" => "COPY",
  1037. "x-goog-api-version" => 2,
  1038. ];
  1039. $response = [
  1040. 'status_code' => 200,
  1041. 'headers' => [
  1042. ]
  1043. ];
  1044. $expected_url = $this->makeCloudStorageObjectUrl("to_bucket",
  1045. "/to_object");
  1046. $this->expectHttpRequest($expected_url,
  1047. RequestMethod::PUT,
  1048. $request_headers,
  1049. null,
  1050. $response);
  1051. // Then we unlink the original.
  1052. $request_headers = $this->getStandardRequestHeaders();
  1053. $response = [
  1054. 'status_code' => 404,
  1055. 'headers' => [
  1056. ],
  1057. ];
  1058. $expected_url = $this->makeCloudStorageObjectUrl();
  1059. $this->expectHttpRequest($expected_url,
  1060. RequestMethod::DELETE,
  1061. $request_headers,
  1062. null,
  1063. $response);
  1064. $from = "gs://bucket/object.png";
  1065. $to = "gs://to_bucket/to_object";
  1066. $this->assertFalse(rename($from, $to));
  1067. $this->apiProxyMock->verify();
  1068. $this->assertEquals(
  1069. [["errno" => E_USER_WARNING,
  1070. "errstr" => "Unable to unlink: gs://bucket/object.png. " .
  1071. "Cloud Storage Error: NOT FOUND"]],
  1072. $this->triggered_errors);
  1073. }
  1074. public function testWriteObjectSuccess() {
  1075. $this->writeObjectSuccessWithMetadata("Hello To PHP.");
  1076. }
  1077. public function testWriteObjectWithMetadata() {
  1078. $metadata = ["foo" => "far", "bar" => "boo"];
  1079. $this->writeObjectSuccessWithMetadata("Goodbye To PHP.", $metadata);
  1080. }
  1081. public function testWriteObjectWithAllMetadataHeaders() {
  1082. $metadata = ['foo' => 'far', 'bar' => 'boo'];
  1083. $headers = [
  1084. 'Cache-Control' => 'public, max-age=6000',
  1085. 'Content-Disposition' => 'attachment; filename=object.png',
  1086. 'Content-Encoding' => 'text/plain',
  1087. 'Content-Language' => 'en',
  1088. ];
  1089. $this->writeObjectSuccessWithMetadata("some text.", $metadata, $headers);
  1090. }
  1091. private function writeObjectSuccessWithMetadata($data,
  1092. array $metadata = null,
  1093. array $headers = []) {
  1094. $data_len = strlen($data);
  1095. $expected_url = $this->makeCloudStorageObjectUrl();
  1096. $this->expectFileWriteStartRequest("text/plain",
  1097. "public-read",
  1098. "foo_upload_id",
  1099. $expected_url,
  1100. $metadata,
  1101. $headers);
  1102. $this->expectFileWriteContentRequest($expected_url,
  1103. "foo_upload_id",
  1104. $data,
  1105. 0,
  1106. $data_len - 1,
  1107. true);
  1108. $context = [
  1109. "gs" => [
  1110. "acl" => "public-read",
  1111. "Content-Type" => "text/plain",
  1112. 'enable_cache' => true,
  1113. ] + $headers,
  1114. ];
  1115. if (isset($metadata)) {
  1116. $context["gs"]["metadata"] = $metadata;
  1117. }
  1118. $range = sprintf("bytes=0-%d", CloudStorageClient::DEFAULT_READ_SIZE - 1);
  1119. $cache_key = sprintf(CloudStorageClient::MEMCACHE_KEY_FORMAT,
  1120. $expected_url,
  1121. $range);
  1122. $this->mock_memcached->expects($this->once())
  1123. ->method('deleteMulti')
  1124. ->with($this->identicalTo([$cache_key]));
  1125. stream_context_set_default($context);
  1126. $this->assertEquals($data_len,
  1127. file_put_contents("gs://bucket/object.png", $data));
  1128. $this->apiProxyMock->verify();
  1129. }
  1130. public function testWriteInvalidMetadata() {
  1131. $metadata = ["f o o" => "far"];
  1132. $context = [
  1133. "gs" => [
  1134. "acl" => "public-read",
  1135. "Content-Type" => "text/plain",
  1136. "metadata" => $metadata
  1137. ],
  1138. ];
  1139. stream_context_set_default($context);
  1140. $this->expectGetAccessTokenRequest(CloudStorageClient::WRITE_SCOPE);
  1141. file_put_contents("gs://bucket/object.png", "Some data");
  1142. $this->apiProxyMock->verify();
  1143. $this->assertEquals(
  1144. ["errno" => E_USER_WARNING,
  1145. "errstr" => "Invalid metadata key: f o o"],
  1146. $this->triggered_errors[0]);
  1147. }
  1148. /**
  1149. * @dataProvider supportedStreamReadModes
  1150. */
  1151. public function testReadMetaDataAndContentTypeInReadMode($mode) {
  1152. $metadata = ["foo" => "far", "bar" => "boo"];
  1153. $this->expectFileReadRequest("Test data",
  1154. 0,
  1155. CloudStorageReadClient::DEFAULT_READ_SIZE,
  1156. null,
  1157. null,
  1158. $metadata,
  1159. "image/png");
  1160. $stream = new CloudStorageStreamWrapper();
  1161. $this->assertTrue($stream->stream_open("gs://bucket/object_name.png",
  1162. $mode,
  1163. 0,
  1164. $unused));
  1165. $this->assertEquals($metadata, $stream->getMetaData());
  1166. $this->assertEquals("image/png", $stream->getContentType());
  1167. }
  1168. /**
  1169. * @dataProvider supportedStreamWriteModes
  1170. */
  1171. public function testReadMetaDataAndContentTypeInWriteMode($mode) {
  1172. $metadata = ["foo" => "far", "bar" => "boo"];
  1173. $headers = [
  1174. "Cache-Control" => "public, max-age=6000",
  1175. "Content-Disposition" => "attachment; filename=object.png",
  1176. "Content-Encoding" => "text/plain",
  1177. "Content-Language" => "en",
  1178. "Content-Type" => "image/png",
  1179. ];
  1180. $expected_url = $this->makeCloudStorageObjectUrl();
  1181. $this->expectFileWriteStartRequest("image/png",
  1182. "public-read",
  1183. "foo_upload_id",
  1184. $expected_url,
  1185. $metadata,
  1186. $headers);
  1187. $context = [
  1188. "gs" => [
  1189. "acl" => "public-read",
  1190. "Content-Type" => "image/png",
  1191. "metadata" => $metadata
  1192. ],
  1193. ];
  1194. stream_context_set_default($context);
  1195. $stream = new CloudStorageStreamWrapper();
  1196. $this->assertTrue($stream->stream_open("gs://bucket/object.png",
  1197. $mode,
  1198. 0,
  1199. $unused));
  1200. $this->assertEquals($metadata, $stream->getMetaData());
  1201. $this->assertEquals("image/png", $stream->getContentType());
  1202. }
  1203. /**
  1204. * DataProvider for
  1205. * - testReadMetaDataAndContentTypeInReadMode
  1206. */
  1207. public function supportedStreamReadModes() {
  1208. return [["r"], ["rt"], ["rb"]];
  1209. }
  1210. /**
  1211. * DataProvider for
  1212. * - testReadMetaDataAndContentTypeInWriteMode
  1213. */
  1214. public function supportedStreamWriteModes() {
  1215. return [["w"], ["wt"], ["wb"]];
  1216. }
  1217. public function testWriteLargeObjectSuccess() {
  1218. $data_to_write = str_repeat("1234567890", 100000);
  1219. $data_len = strlen($data_to_write);
  1220. $expected_url = $this->makeCloudStorageObjectUrl();
  1221. $this->expectFileWriteStartRequest("text/plain",
  1222. "public-read",
  1223. "foo_upload_id",
  1224. $expected_url);
  1225. $chunks = floor($data_len / CloudStorageWriteClient::WRITE_CHUNK_SIZE);
  1226. $start_byte = 0;
  1227. $end_byte = CloudStorageWriteClient::WRITE_CHUNK_SIZE - 1;
  1228. for ($i = 0 ; $i < $chunks ; $i++) {
  1229. $this->expectFileWriteContentRequest($expected_url,
  1230. "foo_upload_id",
  1231. $data_to_write,
  1232. $start_byte,
  1233. $end_byte,
  1234. false);
  1235. $start_byte += Clo

Large files files are truncated, but you can click here to view the full file