PageRenderTime 24ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 0ms

/php/sdk/google/appengine/ext/cloud_storage_streams/CloudStorageReadClient.php

https://github.com/theosp/google_appengine
PHP | 368 lines | 227 code | 49 blank | 92 comment | 46 complexity | 89e1e236f7921b9c4db624f3c412ce82 MD5 | raw 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 Read Client - Implements only the methods required to
  19. * read bytes from GCS using stream wrappers. For a fully fledged client
  20. * to access Google Cloud Storage you should consult the Google API client.
  21. *
  22. */
  23. namespace google\appengine\ext\cloud_storage_streams;
  24. /**
  25. * Google Cloud Storage Client for reading objects.
  26. */
  27. final class CloudStorageReadClient extends CloudStorageClient {
  28. // Buffer for storing data.
  29. private $read_buffer;
  30. // Position in the read buffer where we are currently
  31. private $buffer_read_position = 0;
  32. // Position in the object where the current block starts from
  33. private $object_block_start_position = 0;
  34. // Next position to read from when this buffer is finished.
  35. private $next_read_position = 0;
  36. // Overall size of the object in GCS
  37. private $object_total_length;
  38. // ETag of the object as it was first read.
  39. private $object_etag;
  40. // We have reached the end of the file while reading it.
  41. private $eof = false;
  42. // When we first read the file we partially complete the stat_result that
  43. // we then return in calls to stat()
  44. private $stat_result = [];
  45. // Metadata for the object as it was first read.
  46. private $metadata = [];
  47. // Content-Type for the object as it was first read.
  48. private $content_type;
  49. // HTTP status codes that indicate that there is an object to read, and we
  50. // need to process the response.
  51. private static $valid_status_codes = [HttpResponse::OK,
  52. HttpResponse::PARTIAL_CONTENT,
  53. HttpResponse::RANGE_NOT_SATISFIABLE];
  54. // Client for caching the results of GCS reads.
  55. private $memcache_client;
  56. public function __construct($bucket, $object, $context) {
  57. parent::__construct($bucket, $object, $context);
  58. $this->memcache_client = new \Memcache();
  59. }
  60. public function __destruct() {
  61. parent::__destruct();
  62. }
  63. // Initialize is called when opening the stream. We will try and retrieve
  64. // the first chunk of the file during this stage, to validate that
  65. // - it exists
  66. // - the app has the ACL to access it.
  67. public function initialize() {
  68. return $this->fillReadBuffer(0);
  69. }
  70. /**
  71. * Read at most $count_bytes from the file.
  72. * If we have reached the end of the buffered amount, and there is more
  73. * data in the file then retreive more bytes from storage.
  74. */
  75. public function read($count_bytes) {
  76. // If we have data in the read_buffer then use it.
  77. $readBuffer_size = strlen($this->read_buffer);
  78. $bytes_available = $readBuffer_size - $this->buffer_read_position;
  79. // If there are no more bytes available then get some.
  80. if ($bytes_available === 0 && !$this->eof) {
  81. // If we know the object size, check it first.
  82. $object_bytes_read = $this->object_block_start_position +
  83. $this->buffer_read_position;
  84. if ($object_bytes_read === $this->object_total_length ||
  85. !isset($this->next_read_position)) {
  86. $this->eof = true;
  87. return false;
  88. }
  89. if (!$this->fillReadBuffer($this->next_read_position)) {
  90. return false;
  91. }
  92. // Re-calculate the number of bytes we can serve.
  93. $readBuffer_size = strlen($this->read_buffer);
  94. $bytes_available = $readBuffer_size - $this->buffer_read_position;
  95. }
  96. if ($bytes_available > 0) {
  97. $bytes_to_read = min($bytes_available, $count_bytes);
  98. $current_buffer_position = $this->buffer_read_position;
  99. $this->buffer_read_position += $bytes_to_read;
  100. return substr($this->read_buffer,
  101. $current_buffer_position,
  102. $bytes_to_read);
  103. }
  104. return false;
  105. }
  106. /**
  107. * Returns true if we have read to the end of file, false otherwise.
  108. */
  109. public function eof() {
  110. return $this->eof;
  111. }
  112. /**
  113. * Seek within the current file. We expect the upper layers of PHP to convert
  114. * SEEK_CUR to SEEK_SET.
  115. */
  116. public function seek($offset, $whence) {
  117. if ($whence == SEEK_END) {
  118. if (isset($this->object_total_length)) {
  119. $whence = SEEK_SET;
  120. $offset = $this->object_total_length + $offset;
  121. } else {
  122. trigger_error("Unable to seek from end for objects with unkonwn size",
  123. E_USER_WARNING);
  124. return false;
  125. }
  126. }
  127. if ($whence != SEEK_SET) {
  128. trigger_error(sprintf("Unsupported seek mode: %d", $whence),
  129. E_USER_WARNING);
  130. return false;
  131. }
  132. // If we know the size, then make sure they are only seeking within it.
  133. if (isset($this->object_total_length) &&
  134. $offset >= $this->object_total_length) {
  135. return false;
  136. }
  137. if ($offset < 0) {
  138. return false;
  139. }
  140. // Clear EOF and work it out next time they read.
  141. $this->eof = false;
  142. // Check if we can seek inside the current buffer
  143. $buffer_end = $this->object_block_start_position +
  144. strlen($this->read_buffer);
  145. if ($this->object_block_start_position <= $offset &&
  146. $offset < $buffer_end) {
  147. $this->buffer_read_position = $offset -
  148. $this->object_block_start_position;
  149. } else {
  150. $this->read_buffer = "";
  151. $this->buffer_read_position = 0;
  152. $this->next_read_position = $offset;
  153. }
  154. return true;
  155. }
  156. /**
  157. * Return our stat buffer, if we have one.
  158. */
  159. public function stat() {
  160. if (!empty($this->stat_result)) {
  161. return $this->stat_result;
  162. } else {
  163. return false;
  164. }
  165. }
  166. /**
  167. * Having tell() at this level in the stack seems bonkers.
  168. */
  169. public function tell() {
  170. return $this->buffer_read_position + $this->object_block_start_position;
  171. }
  172. public function getMetaData() {
  173. return $this->metadata;
  174. }
  175. public function getContentType() {
  176. return $this->content_type;
  177. }
  178. /**
  179. * Override the makeHttpRequest function so we can implement caching.
  180. * If caching is enabled then we try and retrieve a matching request for the
  181. * object name and range from memcache.
  182. * If we find a result in memcache, and optimistic caching is enabled then
  183. * we return that result immediately without checking if the object has
  184. * changed in GCS. Otherwise, we will issue a 'If-None-Match' request with
  185. * the ETag of the object to ensure it is still current.
  186. *
  187. * Optimisitic caching is best suited when the application is soley updating
  188. * objects in cloud storage, as the cache can be invalidated when the object
  189. * is updated by the application.
  190. */
  191. protected function makeHttpRequest($url, $method, $headers, $body = null) {
  192. if (!$this->context_options['enable_cache']) {
  193. return parent::makeHttpRequest($url, $method, $headers, $body);
  194. }
  195. $cache_key = sprintf(parent::MEMCACHE_KEY_FORMAT, $url, $headers['Range']);
  196. $cache_obj = $this->memcache_client->get($cache_key);
  197. if (false !== $cache_obj) {
  198. if ($this->context_options['enable_optimistic_cache']) {
  199. return $cache_obj;
  200. } else {
  201. $cache_etag = $this->getHeaderValue('ETag', $cache_obj['headers']);
  202. if (array_key_exists('If-Match', $headers)) {
  203. // We will perform a If-None-Match to validate the cache object, only
  204. // if it has the same ETag value as what we are asking for.
  205. if ($headers['If-Match'] === $cache_etag) {
  206. unset($headers['If-Match']);
  207. } else {
  208. // We are asking for a different object that what is in the cache.
  209. $cache_etag = null;
  210. }
  211. }
  212. }
  213. if (isset($cache_etag)) {
  214. $headers['If-None-Match'] = $cache_etag;
  215. }
  216. }
  217. $result = parent::makeHttpRequest($url, $method, $headers, $body);
  218. if (false === $result) {
  219. return false;
  220. }
  221. $status_code = $result['status_code'];
  222. if (HttpResponse::NOT_MODIFIED === $result['status_code']) {
  223. return $cache_obj;
  224. }
  225. if (in_array($status_code, self::$valid_status_codes)) {
  226. $this->memcache_client->set($cache_key, $result, 0,
  227. $this->context_options['read_cache_expiry_seconds']);
  228. }
  229. return $result;
  230. }
  231. /**
  232. * Fill our internal buffer with data, by making a http request to Google
  233. * Cloud Storage.
  234. */
  235. private function fillReadBuffer($read_position) {
  236. $headers = $this->getOAuthTokenHeader(parent::READ_SCOPE);
  237. if ($headers === false) {
  238. trigger_error("Unable to acquire OAuth token.", E_USER_WARNING);
  239. return false;
  240. }
  241. $end_range = $read_position + parent::DEFAULT_READ_SIZE - 1;
  242. $range = $this->getRangeHeader($read_position, $end_range);
  243. $headers = array_merge($headers, $range);
  244. // If we have an ETag from the first read then use it to ensure we are
  245. // retrieving the same object.
  246. if (isset($this->object_etag)) {
  247. $headers["If-Match"] = $this->object_etag;
  248. }
  249. $http_response = $this->makeHttpRequest($this->url,
  250. "GET",
  251. $headers);
  252. if ($http_response === false) {
  253. trigger_error("Unable to connect to Google Cloud Storage Service.",
  254. E_USER_WARNING);
  255. return false;
  256. }
  257. $status_code = $http_response['status_code'];
  258. if ($status_code === HttpResponse::NOT_FOUND) {
  259. return false;
  260. }
  261. if ($status_code === HttpResponse::PRECONDITION_FAILED) {
  262. trigger_error("Object content has changed.", E_USER_WARNING);
  263. return false;
  264. }
  265. if (!in_array($status_code, self::$valid_status_codes)) {
  266. trigger_error($this->getErrorMessage($status_code,
  267. $http_response['body']),
  268. E_USER_WARNING);
  269. return false;
  270. }
  271. $this->read_buffer = $http_response['body'];
  272. $this->buffer_read_position = 0;
  273. $this->object_block_start_position = $read_position;
  274. // If we got the complete object in the response then use the
  275. // Content-Length
  276. if ($status_code == HttpResponse::OK) {
  277. $content_length = $this->getHeaderValue('Content-Length',
  278. $http_response['headers']);
  279. assert(isset($content_length));
  280. $this->object_total_length = intval($content_length);
  281. $this->next_read_position = null;
  282. } else if ($status_code == HttpResponse::RANGE_NOT_SATISFIABLE) {
  283. // We've read past the end of the object ... no more data.
  284. $this->read_buffer = "";
  285. $this->eof = true;
  286. $this->next_read_position = null;
  287. if (!isset($this->object_total_length)) {
  288. $this->object_total_length = 0;
  289. }
  290. } else {
  291. $content_range = $this->getHeaderValue('Content-Range',
  292. $http_response['headers']);
  293. assert(isset($content_range));
  294. if (preg_match(parent::CONTENT_RANGE_REGEX, $content_range, $m) === 1) {
  295. $this->next_read_position = intval($m[2]) + 1;
  296. $this->object_total_length = intval($m[3]);
  297. }
  298. }
  299. $this->metadata = self::extractMetaData($http_response['headers']);
  300. $this->content_type = $this->getHeaderValue('Content-Type',
  301. $http_response['headers']);
  302. $this->object_etag =
  303. $this->getHeaderValue('ETag', $http_response['headers']);
  304. if (empty($this->stat_result)) {
  305. $stat_args = ['size' => $this->object_total_length,
  306. 'mode' => parent::S_IFREG];
  307. $last_modified = $this->getHeaderValue('Last-Modified',
  308. $http_response['headers']);
  309. if (isset($last_modified)) {
  310. $unix_time = strtotime($last_modified);
  311. if ($unix_time !== false) {
  312. $stat_args["mtime"] = $unix_time;
  313. }
  314. }
  315. $this->stat_result = $this->createStatArray($stat_args);
  316. }
  317. return true;
  318. }
  319. }