PageRenderTime 54ms CodeModel.GetById 10ms 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
  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 += CloudStorageWriteClient::WRITE_CHUNK_SIZE;
  1236. $end_byte += CloudStorageWriteClient::WRITE_CHUNK_SIZE;
  1237. }
  1238. // Write out the remainder
  1239. $this->expectFileWriteContentRequest($expected_url,
  1240. "foo_upload_id",
  1241. $data_to_write,
  1242. $start_byte,
  1243. $data_len - 1,
  1244. true);
  1245. $file_context = [
  1246. "gs" => [
  1247. "acl" => "public-read",
  1248. "Content-Type" => "text/plain",
  1249. 'enable_cache' => true,
  1250. ],
  1251. ];
  1252. $delete_keys = [];
  1253. for ($i = 0; $i < $data_len; $i += CloudStorageClient::DEFAULT_READ_SIZE) {
  1254. $range = sprintf("bytes=%d-%d",
  1255. $i,
  1256. $i + CloudStorageClient::DEFAULT_READ_SIZE - 1);
  1257. $delete_keys[] = sprintf(CloudStorageClient::MEMCACHE_KEY_FORMAT,
  1258. $expected_url,
  1259. $range);
  1260. }
  1261. $this->mock_memcached->expects($this->once())
  1262. ->method('deleteMulti')
  1263. ->with($this->identicalTo($delete_keys));
  1264. $ctx = stream_context_create($file_context);
  1265. $this->assertEquals($data_len,
  1266. file_put_contents("gs://bucket/object.png",
  1267. $data_to_write,
  1268. 0,
  1269. $ctx));
  1270. $this->apiProxyMock->verify();
  1271. }
  1272. public function testWriteEmptyObjectSuccess() {
  1273. $data_to_write = "";
  1274. $data_len = 0;
  1275. $expected_url = $this->makeCloudStorageObjectUrl("bucket",
  1276. "/empty_file.txt");
  1277. $this->expectFileWriteStartRequest("text/plain",
  1278. "public-read",
  1279. "foo_upload_id",
  1280. $expected_url);
  1281. $this->expectFileWriteContentRequest($expected_url,
  1282. "foo_upload_id",
  1283. $data_to_write,
  1284. null, // start_byte
  1285. 0, // write_length
  1286. true); // Complete write
  1287. $file_context = [
  1288. "gs" => [
  1289. "acl" => "public-read",
  1290. "Content-Type" => "text/plain",
  1291. ],
  1292. ];
  1293. $ctx = stream_context_create($file_context);
  1294. $fp = fopen("gs://bucket/empty_file.txt", "wt", false, $ctx);
  1295. $this->assertEquals($data_len, fwrite($fp, $data_to_write));
  1296. fclose($fp);
  1297. $this->apiProxyMock->verify();
  1298. }
  1299. public function testInvalidBucketForInclude() {
  1300. // Uses GAE_INCLUDE_GS_BUCKETS, which is not defined.
  1301. stream_wrapper_unregister("gs");
  1302. stream_wrapper_register("gs",
  1303. "\\google\\appengine\\ext\\cloud_storage_streams\\CloudStorageStreamWrapper",
  1304. 0);
  1305. include 'gs://unknownbucket/object.php';
  1306. $this->assertEquals(E_WARNING, $this->triggered_errors[0]["errno"]);
  1307. $this->assertStringStartsWith(
  1308. "include(gs://unknownbucket/object.php): failed to open stream:",
  1309. $this->triggered_errors[0]["errstr"]);
  1310. $this->assertEquals(E_WARNING, $this->triggered_errors[1]["errno"]);
  1311. $this->assertStringStartsWith(
  1312. "include(): Failed opening 'gs://unknownbucket/object.php'",
  1313. $this->triggered_errors[1]["errstr"]);
  1314. }
  1315. public function testValidBucketForInclude() {
  1316. stream_wrapper_unregister("gs");
  1317. stream_wrapper_register("gs",
  1318. "\\google\\appengine\\ext\\cloud_storage_streams\\CloudStorageStreamWrapper",
  1319. 0);
  1320. $body = '<?php $a = "foo";';
  1321. $this->expectFileReadRequest($body,
  1322. 0,
  1323. CloudStorageReadClient::DEFAULT_READ_SIZE,
  1324. null);
  1325. $valid_path = "gs://bucket/object_name.png";
  1326. require $valid_path;
  1327. $this->assertEquals($a, 'foo');
  1328. $this->apiProxyMock->verify();
  1329. }
  1330. public function testInvalidDirectoryForInclude() {
  1331. // Uses GAE_INCLUDE_GS_BUCKETS, which is not defined.
  1332. stream_wrapper_unregister('gs');
  1333. stream_wrapper_register('gs',
  1334. '\\google\\appengine\\ext\\cloud_storage_streams\\' .
  1335. 'CloudStorageStreamWrapper',
  1336. 0);
  1337. include 'gs://baz/foo/object.php';
  1338. $this->assertEquals(E_WARNING, $this->triggered_errors[0]["errno"]);
  1339. $this->assertStringStartsWith(
  1340. 'include(gs://baz/foo/object.php): failed to open stream:',
  1341. $this->triggered_errors[0]["errstr"]);
  1342. $this->assertEquals(E_WARNING, $this->triggered_errors[1]["errno"]);
  1343. $this->assertStringStartsWith(
  1344. "include(): Failed opening 'gs://baz/foo/object.php'",
  1345. $this->triggered_errors[1]["errstr"]);
  1346. }
  1347. /**
  1348. * DataProvider for
  1349. * - testOpenDirInvalidPath
  1350. */
  1351. public function invalidRootDirPath() {
  1352. return [["gs://"], ["gs:///"]];
  1353. }
  1354. /**
  1355. * DataProvider for
  1356. * - testReadRootDirSuccess
  1357. */
  1358. public function validRootDirPath() {
  1359. return [["gs://bucket"], ["gs://bucket/"]];
  1360. }
  1361. /**
  1362. * @dataProvider invalidRootDirPath
  1363. */
  1364. public function testOpenDirInvalidPath($path) {
  1365. $this->assertFalse(opendir($path));
  1366. $this->assertEquals(
  1367. ["errno" => E_USER_ERROR,
  1368. "errstr" => "Invalid Google Cloud Storage path: $path"],
  1369. $this->triggered_errors[0]);
  1370. }
  1371. /**
  1372. * @dataProvider validRootDirPath
  1373. */
  1374. public function testReadRootDirSuccess($path) {
  1375. $this->expectGetAccessTokenRequest(CloudStorageClient::READ_SCOPE);
  1376. $request_headers = $this->getStandardRequestHeaders();
  1377. $file_results = ['file1.txt', 'file2.txt', 'file3.txt' ];
  1378. $common_prefixes_results = ['dir/'];
  1379. $response = [
  1380. 'status_code' => 200,
  1381. 'headers' => [
  1382. ],
  1383. 'body' => $this->makeGetBucketXmlResponse(
  1384. "",
  1385. $file_results,
  1386. null,
  1387. $common_prefixes_results),
  1388. ];
  1389. $expected_url = $this->makeCloudStorageObjectUrl("bucket", null);
  1390. $expected_query = http_build_query([
  1391. "delimiter" => CloudStorageDirectoryClient::DELIMITER,
  1392. "max-keys" => CloudStorageDirectoryClient::MAX_KEYS,
  1393. ]);
  1394. $this->expectHttpRequest(sprintf("%s?%s", $expected_url, $expected_query),
  1395. RequestMethod::GET,
  1396. $request_headers,
  1397. null,
  1398. $response);
  1399. $res = opendir($path);
  1400. $this->assertEquals("file1.txt", readdir($res));
  1401. $this->assertEquals("file2.txt", readdir($res));
  1402. $this->assertEquals("file3.txt", readdir($res));
  1403. $this->assertEquals("dir/", readdir($res));
  1404. $this->assertFalse(readdir($res));
  1405. closedir($res);
  1406. $this->apiProxyMock->verify();
  1407. }
  1408. public function testReadADirSuccess() {
  1409. $this->expectGetAccessTokenRequest(CloudStorageClient::READ_SCOPE);
  1410. $request_headers = $this->getStandardRequestHeaders();
  1411. $file_results = ['f/file1.txt', 'f/file2.txt', 'f/', 'f_$folder$'];
  1412. $common_prefixes_results = ['f/sub/'];
  1413. $response = [
  1414. 'status_code' => 200,
  1415. 'headers' => [
  1416. ],
  1417. 'body' => $this->makeGetBucketXmlResponse(
  1418. "f/",
  1419. $file_results,
  1420. null,
  1421. $common_prefixes_results),
  1422. ];
  1423. $expected_url = $this->makeCloudStorageObjectUrl("bucket", null);
  1424. $expected_query = http_build_query([
  1425. "delimiter" => CloudStorageDirectoryClient::DELIMITER,
  1426. "max-keys" => CloudStorageDirectoryClient::MAX_KEYS,
  1427. "prefix" => "f/",
  1428. ]);
  1429. $this->expectHttpRequest(sprintf("%s?%s", $expected_url, $expected_query),
  1430. RequestMethod::GET,
  1431. $request_headers,
  1432. null,
  1433. $response);
  1434. $res = opendir("gs://bucket/f");
  1435. $this->assertEquals("file1.txt", readdir($res));
  1436. $this->assertEquals("file2.txt", readdir($res));
  1437. $this->assertEquals("sub/", readdir($res));
  1438. $this->assertFalse(readdir($res));
  1439. closedir($res);
  1440. $this->apiProxyMock->verify();
  1441. }
  1442. public function testReaddirTruncatedSuccess() {
  1443. $this->expectGetAccessTokenRequest(CloudStorageClient::READ_SCOPE);
  1444. $request_headers = $this->getStandardRequestHeaders();
  1445. // First query with a truncated response
  1446. $response_body = "<?xml version='1.0' encoding='UTF-8'?>
  1447. <ListBucketResult xmlns='http://doc.s3.amazonaws.com/2006-03-01'>
  1448. <Name>sjl-test</Name>
  1449. <Prefix>f/</Prefix>
  1450. <Marker></Marker>
  1451. <NextMarker>AA</NextMarker>
  1452. <Delimiter>/</Delimiter>
  1453. <IsTruncated>true</IsTruncated>
  1454. <Contents>
  1455. <Key>f/file1.txt</Key>
  1456. </Contents>
  1457. <Contents>
  1458. <Key>f/file2.txt</Key>
  1459. </Contents>
  1460. </ListBucketResult>";
  1461. $response = [
  1462. 'status_code' => 200,
  1463. 'headers' => [
  1464. ],
  1465. 'body' => $response_body,
  1466. ];
  1467. $expected_url = $this->makeCloudStorageObjectUrl("bucket", null);
  1468. $expected_query = http_build_query([
  1469. "delimiter" => CloudStorageDirectoryClient::DELIMITER,
  1470. "max-keys" => CloudStorageDirectoryClient::MAX_KEYS,
  1471. "prefix" => "f/",
  1472. ]);
  1473. $this->expectHttpRequest(sprintf("%s?%s", $expected_url, $expected_query),
  1474. RequestMethod::GET,
  1475. $request_headers,
  1476. null,
  1477. $response);
  1478. // Second query with the remaining response
  1479. $response_body = "<?xml version='1.0' encoding='UTF-8'?>
  1480. <ListBucketResult xmlns='http://doc.s3.amazonaws.com/2006-03-01'>
  1481. <Name>sjl-test</Name>
  1482. <Prefix>f/</Prefix>
  1483. <Marker>AA</Marker>
  1484. <Delimiter>/</Delimiter>
  1485. <IsTruncated>false</IsTruncated>
  1486. <Contents>
  1487. <Key>f/file3.txt</Key>
  1488. </Contents>
  1489. <Contents>
  1490. <Key>f/file4.txt</Key>
  1491. </Contents>
  1492. </ListBucketResult>";
  1493. $response = [
  1494. 'status_code' => 200,
  1495. 'headers' => [
  1496. ],
  1497. 'body' => $response_body,
  1498. ];
  1499. $expected_query = http_build_query([
  1500. "delimiter" => CloudStorageDirectoryClient::DELIMITER,
  1501. "max-keys" => CloudStorageDirectoryClient::MAX_KEYS,
  1502. "prefix" => "f/",
  1503. "marker" => "AA",
  1504. ]);
  1505. $this->expectGetAccessTokenRequest(CloudStorageClient::READ_SCOPE);
  1506. $this->expectHttpRequest(sprintf("%s?%s", $expected_url, $expected_query),
  1507. RequestMethod::GET,
  1508. $request_headers,
  1509. null,
  1510. $response);
  1511. $res = opendir("gs://bucket/f");
  1512. $this->assertEquals("file1.txt", readdir($res));
  1513. $this->assertEquals("file2.txt", readdir($res));
  1514. $this->assertEquals("file3.txt", readdir($res));
  1515. $this->assertEquals("file4.txt", readdir($res));
  1516. $this->assertFalse(readdir($res));
  1517. closedir($res);
  1518. $this->apiProxyMock->verify();
  1519. }
  1520. public function testRewindDirSuccess() {
  1521. $this->expectGetAccessTokenRequest(CloudStorageClient::READ_SCOPE);
  1522. $request_headers = $this->getStandardRequestHeaders();
  1523. $response = [
  1524. 'status_code' => 200,
  1525. 'headers' => [
  1526. ],
  1527. 'body' => $this->makeGetBucketXmlResponse(
  1528. "f/",
  1529. ["f/file1.txt", "f/file2.txt"]),
  1530. ];
  1531. $expected_url = $this->makeCloudStorageObjectUrl("bucket", null);
  1532. $expected_query = http_build_query([
  1533. "delimiter" => CloudStorageDirectoryClient::DELIMITER,
  1534. "max-keys" => CloudStorageDirectoryClient::MAX_KEYS,
  1535. "prefix" => "f/",
  1536. ]);
  1537. $this->expectHttpRequest(sprintf("%s?%s", $expected_url, $expected_query),
  1538. RequestMethod::GET,
  1539. $request_headers,
  1540. null,
  1541. $response);
  1542. // Expect the requests again when we rewinddir
  1543. $this->expectGetAccessTokenRequest(CloudStorageClient::READ_SCOPE);
  1544. $this->expectHttpRequest(sprintf("%s?%s", $expected_url, $expected_query),
  1545. RequestMethod::GET,
  1546. $request_headers,
  1547. null,
  1548. $response);
  1549. $res = opendir("gs://bucket/f");
  1550. $this->assertEquals("file1.txt", readdir($res));
  1551. rewinddir($res);
  1552. $this->assertEquals("file1.txt", readdir($res));
  1553. $this->assertEquals("file2.txt", readdir($res));
  1554. $this->assertFalse(readdir($res));
  1555. closedir($res);
  1556. $this->apiProxyMock->verify();
  1557. }
  1558. /**
  1559. * DataProvider for
  1560. * - testMkDirInvalidPath
  1561. * - testRmDirInvalidPath
  1562. */
  1563. public function invalidDirPath() {
  1564. return [["gs://"], ["gs:///"], ["gs://bucket"], ["gs://bucket/"]];
  1565. }
  1566. /**
  1567. * DataProvider for
  1568. * - testMkDirSuccess
  1569. * - testRmDirSuccess
  1570. * - testRmDirNotEmpty
  1571. */
  1572. public function validDirPath() {
  1573. // Each data set contains [gcs_path, bucket_name, object_name, prefix]
  1574. return [["gs://bucket/dira/dirb/", "bucket", "/dira/dirb/", "dira/dirb/"],
  1575. ["gs://bucket/dira/dirb", "bucket", "/dira/dirb/", "dira/dirb/"]];
  1576. }
  1577. /**
  1578. * @dataProvider invalidDirPath
  1579. */
  1580. public function testMkInvalidPath($invalid_path) {
  1581. $this->assertFalse(mkdir($invalid_path));
  1582. $this->assertEquals(
  1583. [["errno" => E_USER_ERROR,
  1584. "errstr" => "Invalid Google Cloud Storage path: $invalid_path"]],
  1585. $this->triggered_errors);
  1586. }
  1587. /**
  1588. * @dataProvider validDirPath
  1589. */
  1590. public function testMkDirSuccess($path, $bucket, $object, $prefix) {
  1591. $this->expectGetAccessTokenRequest(CloudStorageClient::WRITE_SCOPE);
  1592. $request_headers = [
  1593. "Authorization" => "OAuth foo token",
  1594. "x-goog-if-generation-match" => 0,
  1595. "Content-Range" => "bytes */0",
  1596. "x-goog-api-version" => 2,
  1597. ];
  1598. $response = [
  1599. 'status_code' => 200,
  1600. 'headers' => [
  1601. ],
  1602. ];
  1603. $expected_url = $this->makeCloudStorageObjectUrl($bucket, $object);
  1604. $this->expectHttpRequest($expected_url,
  1605. RequestMethod::PUT,
  1606. $request_headers,
  1607. null,
  1608. $response);
  1609. $this->assertTrue(mkdir($path));
  1610. $this->apiProxyMock->verify();
  1611. }
  1612. /**
  1613. * @dataProvider invalidDirPath
  1614. */
  1615. public function testRmDirInvalidPath($path) {
  1616. $this->assertFalse(rmdir($path));
  1617. $this->assertEquals(
  1618. [["errno" => E_USER_ERROR,
  1619. "errstr" => "Invalid Google Cloud Storage path: $path"]],
  1620. $this->triggered_errors);
  1621. }
  1622. /**
  1623. * @dataProvider validDirPath
  1624. */
  1625. public function testRmDirSuccess($path, $bucket, $object, $prefix) {
  1626. // Expect a request to list the contents of the bucket to ensure that it is
  1627. // empty.
  1628. $this->expectGetAccessTokenRequest(CloudStorageClient::READ_SCOPE);
  1629. $request_headers = $this->getStandardRequestHeaders();
  1630. // First query with a truncated response
  1631. $response = [
  1632. 'status_code' => 200,
  1633. 'headers' => [
  1634. ],
  1635. 'body' => $this->makeGetBucketXmlResponse($prefix, []),
  1636. ];
  1637. $expected_url = $this->makeCloudStorageObjectUrl($bucket, null);
  1638. $expected_query = http_build_query([
  1639. "delimiter" => CloudStorageDirectoryClient::DELIMITER,
  1640. "max-keys" => CloudStorageDirectoryClient::MAX_KEYS,
  1641. "prefix" => $prefix,
  1642. ]);
  1643. $this->expectHttpRequest(sprintf("%s?%s", $expected_url, $expected_query),
  1644. RequestMethod::GET,
  1645. $request_headers,
  1646. null,
  1647. $response);
  1648. // Expect the unlink request for the folder.
  1649. $this->expectGetAccessTokenRequest(CloudStorageClient::WRITE_SCOPE);
  1650. $request_headers = $this->getStandardRequestHeaders();
  1651. $response = [
  1652. 'status_code' => 204,
  1653. 'headers' => [
  1654. ],
  1655. ];
  1656. $expected_url = $this->makeCloudStorageObjectUrl($bucket, $object);
  1657. $this->expectHttpRequest($expected_url,
  1658. RequestMethod::DELETE,
  1659. $request_headers,
  1660. null,
  1661. $response);
  1662. $this->assertTrue(rmdir($path));
  1663. $this->apiProxyMock->verify();
  1664. }
  1665. /**
  1666. * @dataProvider validDirPath
  1667. */
  1668. public function testRmDirNotEmpty($path, $bucket, $object, $prefix) {
  1669. // Expect a request to list the contents of the bucket to ensure that it is
  1670. // empty.
  1671. $this->expectGetAccessTokenRequest(CloudStorageClient::READ_SCOPE);
  1672. $request_headers = $this->getStandardRequestHeaders();
  1673. // First query with a truncated response
  1674. $response = [
  1675. 'status_code' => 200,
  1676. 'headers' => [
  1677. ],
  1678. 'body' => $this->makeGetBucketXmlResponse(
  1679. $prefix,
  1680. [$prefix . "file1.txt"]),
  1681. ];
  1682. $expected_url = $this->makeCloudStorageObjectUrl($bucket, null);
  1683. $expected_query = http_build_query([
  1684. "delimiter" => CloudStorageDirectoryClient::DELIMITER,
  1685. "max-keys" => CloudStorageDirectoryClient::MAX_KEYS,
  1686. "prefix" => $prefix,
  1687. ]);
  1688. $this->expectHttpRequest(sprintf("%s?%s", $expected_url, $expected_query),
  1689. RequestMethod::GET,
  1690. $request_headers,
  1691. null,
  1692. $response);
  1693. $this->assertFalse(rmdir($path));
  1694. $this->apiProxyMock->verify();
  1695. $this->assertEquals(
  1696. [["errno" => E_USER_WARNING,
  1697. "errstr" => "The directory is not empty."]],
  1698. $this->triggered_errors);
  1699. }
  1700. public function testStreamCast() {
  1701. $body = "Hello from PHP";
  1702. $this->expectFileReadRequest($body,
  1703. 0,
  1704. CloudStorageReadClient::DEFAULT_READ_SIZE,
  1705. null);
  1706. $valid_path = "gs://bucket/object_name.png";
  1707. $this->assertFalse(gzopen($valid_path, 'rb'));
  1708. $this->apiProxyMock->verify();
  1709. $this->assertEquals(
  1710. [["errno" => E_WARNING,
  1711. "errstr" => "gzopen(): cannot represent a stream of type " .
  1712. "user-space as a File Descriptor"]],
  1713. $this->triggered_errors);
  1714. }
  1715. private function expectFileReadRequest($body,
  1716. $start_byte,
  1717. $length,
  1718. $etag = null,
  1719. $paritial_content = null,
  1720. $metadata = null,
  1721. $content_type = null) {
  1722. $this->expectGetAccessTokenRequest(CloudStorageClient::READ_SCOPE);
  1723. assert($length > 0);
  1724. $last_byte = $start_byte + $length - 1;
  1725. $request_headers = [
  1726. "Authorization" => "OAuth foo token",
  1727. "Range" => sprintf("bytes=%d-%d", $start_byte, $last_byte),
  1728. ];
  1729. if (isset($etag)) {
  1730. $request_headers['If-Match'] = $etag;
  1731. }
  1732. $request_headers["x-goog-api-version"] = 2;
  1733. $response_headers = [
  1734. "ETag" => "deadbeef",
  1735. "Last-Modified" => "Mon, 02 Jul 2012 01:41:01 GMT",
  1736. ];
  1737. if (isset($content_type)) {
  1738. $response_headers["Content-Type"] = $content_type;
  1739. } else {
  1740. $response_headers["Content-Type"] = "binary/octet-stream";
  1741. }
  1742. if (isset($metadata)) {
  1743. foreach ($metadata as $key => $value) {
  1744. $response_headers["x-goog-meta-" . $key] = $value;
  1745. }
  1746. }
  1747. $response = $this->createSuccessfulGetHttpResponse($response_headers,
  1748. $body,
  1749. $start_byte,
  1750. $length,
  1751. $paritial_content);
  1752. $exected_url = self::makeCloudStorageObjectUrl("bucket",
  1753. "/object_name.png");
  1754. $this->expectHttpRequest($exected_url,
  1755. RequestMethod::GET,
  1756. $request_headers,
  1757. null,
  1758. $response);
  1759. }
  1760. private function expectGetAccessTokenRequest($scope) {
  1761. $req = new \google\appengine\GetAccessTokenRequest();
  1762. $req->addScope($scope);
  1763. $resp = new \google\appengine\GetAccessTokenResponse();
  1764. $resp->setAccessToken('foo token');
  1765. $resp->setExpirationTime(12345);
  1766. $this->apiProxyMock->expectCall('app_identity_service',
  1767. 'GetAccessToken',
  1768. $req,
  1769. $resp);
  1770. $this->mock_memcache->expects($this->at($this->mock_memcache_call_index++))
  1771. ->method('get')
  1772. ->with($this->stringStartsWith('_ah_app_identity'))
  1773. ->will($this->returnValue(false));
  1774. $this->mock_memcache->expects($this->at($this->mock_memcache_call_index++))
  1775. ->method('set')
  1776. ->with($this->stringStartsWith('_ah_app_identity'),
  1777. $this->anything(),
  1778. $this->anything(),
  1779. $this->anything())
  1780. ->will($this->returnValue(false));
  1781. }
  1782. private function createSuccessfulGetHttpResponse($headers,
  1783. $body,
  1784. $start_byte,
  1785. $length,
  1786. $return_partial_content) {
  1787. $total_body_length = strlen($body);
  1788. $partial_content = false;
  1789. $range_cannot_be_satisfied = false;
  1790. if ($total_body_length <= $start_byte) {
  1791. $range_cannot_be_satisfied = true;
  1792. $body = "<Message>The requested range cannot be satisfied.</Message>";
  1793. } else {
  1794. if ($start_byte != 0 || $length < $total_body_length) {
  1795. $final_length = min($length, $total_body_length - $start_byte);
  1796. $body = substr($body, $start_byte, $final_length);
  1797. $partial_content = true;
  1798. } else if ($return_partial_content) {
  1799. $final_length = strlen($body);
  1800. $partial_content = true;
  1801. }
  1802. }
  1803. $success_headers = [];
  1804. if ($range_cannot_be_satisfied) {
  1805. $status_code = HttpResponse::RANGE_NOT_SATISFIABLE;
  1806. $success_headers["Content-Length"] = $total_body_length;
  1807. } else if (!$partial_content) {
  1808. $status_code = HttpResponse::OK;
  1809. $success_headers["Content-Length"] = $total_body_length;
  1810. } else {
  1811. $status_code = HttpResponse::PARTIAL_CONTENT;
  1812. $end_range = $start_byte + $final_length - 1;
  1813. $success_headers["Content-Length"] = $final_length;
  1814. $success_headers["Content-Range"] = sprintf("bytes %d-%d/%d",
  1815. $start_byte,
  1816. $end_range,
  1817. $total_body_length);
  1818. }
  1819. return [
  1820. 'status_code' => $status_code,
  1821. 'headers' => array_merge($success_headers, $headers),
  1822. 'body' => $body,
  1823. ];
  1824. }
  1825. private function expectFileWriteStartRequest($content_type,
  1826. $acl,
  1827. $id,
  1828. $url,
  1829. $metadata = NULL,
  1830. array $headers = null) {
  1831. $this->expectGetAccessTokenRequest(CloudStorageClient::WRITE_SCOPE);
  1832. $upload_id = "https://host/bucket/object.png?upload_id=" . $id;
  1833. // The upload will start with a POST to acquire the upload ID.
  1834. $request_headers = [
  1835. "x-goog-resumable" => "start",
  1836. "Authorization" => "OAuth foo token",
  1837. ];
  1838. if ($headers) {
  1839. $request_headers += $headers;
  1840. }
  1841. if ($content_type != null) {
  1842. $request_headers['Content-Type'] = $content_type;
  1843. }
  1844. if ($acl != null) {
  1845. $request_headers['x-goog-acl'] = $acl;
  1846. }
  1847. if (isset($metadata)) {
  1848. foreach ($metadata as $key => $value) {
  1849. $request_headers["x-goog-meta-" . $key] = $value;
  1850. }
  1851. }
  1852. $request_headers["x-goog-api-version"] = 2;
  1853. $response = [
  1854. 'status_code' => 201,
  1855. 'headers' => [
  1856. 'Location' => $upload_id,
  1857. ],
  1858. ];
  1859. $this->expectHttpRequest($url,
  1860. RequestMethod::POST,
  1861. $request_headers,
  1862. null,
  1863. $response);
  1864. }
  1865. private function expectFileWriteContentRequest($url,
  1866. $upload_id,
  1867. $data,
  1868. $start_byte,
  1869. $end_byte,
  1870. $complete) {
  1871. // The upload will be completed with a PUT with the final length
  1872. $this->expectGetAccessTokenRequest(CloudStorageClient::WRITE_SCOPE);
  1873. // If start byte is null then we assume that this is a PUT with no content,
  1874. // and the end_byte contains the length of the data to write.
  1875. if (is_null($start_byte)) {
  1876. $range = sprintf("bytes */%d", $end_byte);
  1877. $status_code = HttpResponse::OK;
  1878. $body = null;
  1879. } else {
  1880. $length = $end_byte - $start_byte + 1;
  1881. if ($complete) {
  1882. $total_len = $end_byte + 1;
  1883. $range = sprintf("bytes %d-%d/%d", $start_byte, $end_byte, $total_len);
  1884. $status_code = HttpResponse::OK;
  1885. } else {
  1886. $range = sprintf("bytes %d-%d/*", $start_byte, $end_byte);
  1887. $status_code = HttpResponse::RESUME_INCOMPLETE;
  1888. }
  1889. $body = substr($data, $start_byte, $length);
  1890. }
  1891. $request_headers = [
  1892. "Authorization" => "OAuth foo token",
  1893. "Content-Range" => $range,
  1894. "x-goog-api-version" => 2,
  1895. ];
  1896. $response = [
  1897. 'status_code' => $status_code,
  1898. 'headers' => [
  1899. ],
  1900. ];
  1901. $expected_url = $url . "?upload_id=" . $upload_id;
  1902. $this->expectHttpRequest($expected_url,
  1903. RequestMethod::PUT,
  1904. $request_headers,
  1905. $body,
  1906. $response);
  1907. }
  1908. private function expectHttpRequest($url, $method, $headers, $body, $result) {
  1909. $req = new \google\appengine\URLFetchRequest();
  1910. $req->setUrl($url);
  1911. $req->setMethod($method);
  1912. $req->setMustValidateServerCertificate(true);
  1913. foreach($headers as $k => $v) {
  1914. $h = $req->addHeader();
  1915. $h->setKey($k);
  1916. $h->setValue($v);
  1917. }
  1918. if (isset($body)) {
  1919. $req->setPayload($body);
  1920. }
  1921. if ($result instanceof \Exception) {
  1922. $resp = $result;
  1923. } else {
  1924. $resp = new \google\appengine\URLFetchResponse();
  1925. $resp->setStatusCode($result['status_code']);
  1926. foreach($result['headers'] as $k => $v) {
  1927. $h = $resp->addHeader();
  1928. $h->setKey($k);
  1929. $h->setValue($v);
  1930. }
  1931. if (isset($result['body'])) {
  1932. $resp->setContent($result['body']);
  1933. }
  1934. }
  1935. $this->apiProxyMock->expectCall('urlfetch',
  1936. 'Fetch',
  1937. $req,
  1938. $resp);
  1939. }
  1940. private function expectIsWritableMemcacheLookup($key_found, $result) {
  1941. if ($key_found) {
  1942. $lookup_result = ['is_writable' => $result];
  1943. } else {
  1944. $lookup_result = false;
  1945. }
  1946. $this->mock_memcache->expects($this->at($this->mock_memcache_call_index++))
  1947. ->method('get')
  1948. ->with($this->stringStartsWith(
  1949. '_ah_gs_write_bucket_cache_'))
  1950. ->will($this->returnValue($lookup_result));
  1951. }
  1952. private function expectIsWritableMemcacheSet($value) {
  1953. $this->mock_memcache->expects($this->at($this->mock_memcache_call_index++))
  1954. ->method('set')
  1955. ->with($this->stringStartsWith('_ah_gs_write_bucket_cache_'),
  1956. ['is_writable' => $value],
  1957. null,
  1958. CloudStorageClient::DEFAULT_WRITABLE_CACHE_EXPIRY_SECONDS)
  1959. ->will($this->returnValue(false));
  1960. }
  1961. private function makeCloudStorageObjectUrl($bucket = "bucket",
  1962. $object = "/object.png") {
  1963. return CloudStorageClient::createObjectUrl($bucket, $object);
  1964. }
  1965. private function getStandardRequestHeaders() {
  1966. return [
  1967. "Authorization" => "OAuth foo token",
  1968. "x-goog-api-version" => 2,
  1969. ];
  1970. }
  1971. private function makeGetBucketXmlResponse($prefix,
  1972. $contents_array,
  1973. $next_marker = null,
  1974. $common_prefix_array = null) {
  1975. $result = "<?xml version='1.0' encoding='UTF-8'?>
  1976. <ListBucketResult xmlns='http://doc.s3.amazonaws.com/2006-03-01'>
  1977. <Name>sjl-test</Name>
  1978. <Prefix>" . $prefix . "</Prefix>
  1979. <Marker></Marker>";
  1980. if (isset($next_marker)) {
  1981. $result .= "<NextMarker>" . $next_marker . "</NextMarker>";
  1982. }
  1983. $result .= "<Delimiter>/</Delimiter>
  1984. <IsTruncated>false</IsTruncated>";
  1985. foreach($contents_array as $content) {
  1986. $result .= '<Contents>';
  1987. if (is_string($content)) {
  1988. $result .= '<Key>' . $content . '</Key>';
  1989. } else {
  1990. $result .= '<Key>' . $content['key'] . '</Key>';
  1991. $result .= '<Size>' . $content['size'] . '</Size>';
  1992. $result .= '<LastModified>' . $content['mtime'] . '</LastModified>';
  1993. }
  1994. $result .= '</Contents>';
  1995. }
  1996. if (isset($common_prefix_array)) {
  1997. foreach($common_prefix_array as $common_prefix) {
  1998. $result .= '<CommonPrefixes>';
  1999. $result .= '<Prefix>' . $common_prefix . '</Prefix>';
  2000. $result .= '</CommonPrefixes>';
  2001. }
  2002. }
  2003. $result .= "</ListBucketResult>";
  2004. return $result;
  2005. }
  2006. }
  2007. // TODO: b/13132830: Remove once feature releases.
  2008. /**
  2009. * Gets the value of a configuration option.
  2010. *
  2011. * Override built-in ini_get() to fake INI value that would normally be provided
  2012. * by gae extension, but is not on devappserver. INI will always be true during
  2013. * these tests.
  2014. *
  2015. * - google_app_engine.enable_additional_cloud_storage_headers: true
  2016. *
  2017. * @param string $varname
  2018. * The configuration option name.
  2019. * @return mixed
  2020. * Returns the value of the configuration option as a string on success, or an
  2021. * empty string for null values. Returns FALSE if the configuration option
  2022. * doesn't exist.
  2023. *
  2024. * @see http://php.net/ini_get
  2025. */
  2026. function ini_get($varname) {
  2027. if ($varname == 'google_app_engine.enable_additional_cloud_storage_headers') {
  2028. return true;
  2029. }
  2030. return \ini_get($varname);
  2031. }
  2032. } // namespace google\appengine\ext\cloud_storage_streams;