PageRenderTime 53ms CodeModel.GetById 21ms RepoModel.GetById 0ms app.codeStats 1ms

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

https://gitlab.com/gregtyka/frankenserver
PHP | 368 lines | 229 code | 49 blank | 90 comment | 50 complexity | 54c856edbc6f90f65c9613d9f641b431 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. */
  19. namespace google\appengine\ext\cloud_storage_streams;
  20. /**
  21. * Google Cloud Storage Read Client - Implements only the methods required to
  22. * read bytes from GCS using stream wrappers. For a fully fledged client
  23. * to access Google Cloud Storage you should consult the Google API client.
  24. */
  25. final class CloudStorageReadClient extends CloudStorageClient {
  26. // Buffer for storing data.
  27. private $read_buffer;
  28. // Position in the read buffer where we are currently
  29. private $buffer_read_position = 0;
  30. // Position in the object where the current block starts from
  31. private $object_block_start_position = 0;
  32. // Next position to read from when this buffer is finished.
  33. private $next_read_position = 0;
  34. // Overall size of the object in GCS
  35. private $object_total_length;
  36. // ETag of the object as it was first read.
  37. private $object_etag;
  38. // We have reached the end of the file while reading it.
  39. private $eof = false;
  40. // When we first read the file we partially complete the stat_result that
  41. // we then return in calls to stat()
  42. private $stat_result = [];
  43. // Metadata for the object as it was first read.
  44. private $metadata = [];
  45. // Content-Type for the object as it was first read.
  46. private $content_type;
  47. // HTTP status codes that indicate that there is an object to read, and we
  48. // need to process the response.
  49. private static $valid_status_codes = [HttpResponse::OK,
  50. HttpResponse::PARTIAL_CONTENT,
  51. HttpResponse::RANGE_NOT_SATISFIABLE];
  52. // Client for caching the results of GCS reads.
  53. private $memcache_client;
  54. public function __construct($bucket, $object, $context) {
  55. parent::__construct($bucket, $object, $context);
  56. $this->memcache_client = new \Memcache();
  57. }
  58. public function __destruct() {
  59. parent::__destruct();
  60. }
  61. // Initialize is called when opening the stream. We will try and retrieve
  62. // the first chunk of the file during this stage, to validate that
  63. // - it exists
  64. // - the app has the ACL to access it.
  65. public function initialize() {
  66. return $this->fillReadBuffer(0);
  67. }
  68. /**
  69. * Read at most $count_bytes from the file.
  70. * If we have reached the end of the buffered amount, and there is more
  71. * data in the file then retreive more bytes from storage.
  72. */
  73. public function read($count_bytes) {
  74. // If we have data in the read_buffer then use it.
  75. $readBuffer_size = strlen($this->read_buffer);
  76. $bytes_available = $readBuffer_size - $this->buffer_read_position;
  77. // If there are no more bytes available then get some.
  78. if ($bytes_available === 0 && !$this->eof) {
  79. // If we know the object size, check it first.
  80. $object_bytes_read = $this->object_block_start_position +
  81. $this->buffer_read_position;
  82. if ($object_bytes_read === $this->object_total_length ||
  83. !isset($this->next_read_position)) {
  84. $this->eof = true;
  85. return false;
  86. }
  87. if (!$this->fillReadBuffer($this->next_read_position)) {
  88. return false;
  89. }
  90. // Re-calculate the number of bytes we can serve.
  91. $readBuffer_size = strlen($this->read_buffer);
  92. $bytes_available = $readBuffer_size - $this->buffer_read_position;
  93. }
  94. if ($bytes_available > 0) {
  95. $bytes_to_read = min($bytes_available, $count_bytes);
  96. $current_buffer_position = $this->buffer_read_position;
  97. $this->buffer_read_position += $bytes_to_read;
  98. return substr($this->read_buffer,
  99. $current_buffer_position,
  100. $bytes_to_read);
  101. }
  102. return false;
  103. }
  104. /**
  105. * Returns true if we have read to the end of file, false otherwise.
  106. */
  107. public function eof() {
  108. return $this->eof;
  109. }
  110. /**
  111. * Seek within the current file. We expect the upper layers of PHP to convert
  112. * SEEK_CUR to SEEK_SET.
  113. */
  114. public function seek($offset, $whence) {
  115. if ($whence == SEEK_END) {
  116. if (isset($this->object_total_length)) {
  117. $whence = SEEK_SET;
  118. $offset = $this->object_total_length + $offset;
  119. } else {
  120. trigger_error("Unable to seek from end for objects with unknown size",
  121. E_USER_WARNING);
  122. return false;
  123. }
  124. }
  125. if ($whence != SEEK_SET) {
  126. trigger_error(sprintf("Unsupported seek mode: %d", $whence),
  127. E_USER_WARNING);
  128. return false;
  129. }
  130. // If we know the size, then make sure they are only seeking within it.
  131. if (isset($this->object_total_length) &&
  132. $offset >= $this->object_total_length) {
  133. return false;
  134. }
  135. if ($offset < 0) {
  136. return false;
  137. }
  138. // Clear EOF and work it out next time they read.
  139. $this->eof = false;
  140. // Check if we can seek inside the current buffer
  141. $block_start = $this->object_block_start_position;
  142. $buffer_end = $block_start + strlen($this->read_buffer);
  143. if ($block_start <= $offset && $offset < $buffer_end) {
  144. $this->buffer_read_position = $offset - $block_start;
  145. } else {
  146. $this->read_buffer = "";
  147. $this->buffer_read_position = 0;
  148. $this->next_read_position = $offset;
  149. }
  150. return true;
  151. }
  152. /**
  153. * Return our stat buffer, if we have one.
  154. */
  155. public function stat() {
  156. if (!empty($this->stat_result)) {
  157. return $this->stat_result;
  158. } else {
  159. return false;
  160. }
  161. }
  162. /**
  163. * Having tell() at this level in the stack seems bonkers.
  164. */
  165. public function tell() {
  166. if (strlen($this->read_buffer) == 0) {
  167. return $this->next_read_position;
  168. } else {
  169. return $this->buffer_read_position + $this->object_block_start_position;
  170. }
  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 = static::getReadMemcacheKey($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. }