App Engine Python SDK version 1.9.12
[gae.git] / python / php / sdk / google / appengine / ext / cloud_storage_streams / CloudStorageUrlStatClient.php
blobd6060aa8db59337542a919695b1990ae31d2eb43
1 <?php
2 /**
3 * Copyright 2007 Google Inc.
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
9 * http://www.apache.org/licenses/LICENSE-2.0
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.
17 /**
18 * Cloud Storage Url Stat Client handles stat() calls for objects and buckets.
22 namespace google\appengine\ext\cloud_storage_streams;
24 use google\appengine\util\StringUtil;
26 /**
27 * Client for stating objects in Google Cloud Storage.
29 final class CloudStorageUrlStatClient extends CloudStorageClient {
30 // Maximum number of keys to return when querying a bucket.
31 const MAX_KEYS = 1000;
33 private $quiet;
34 private $prefix = null;
35 private $next_marker = null;
37 public function __construct($bucket, $object, $context, $flags) {
38 parent::__construct($bucket, $object, $context);
39 $this->quiet = ($flags & STREAM_URL_STAT_QUIET) == STREAM_URL_STAT_QUIET;
40 if (isset($object)) {
41 // Drop the leading '/' from the object name.
42 $this->prefix = substr($object, 1);
46 /**
47 * The stat function uses GET requests to the bucket to try and determine if
48 * the object is a 'file' or a 'directory', by listing the contents of the
49 * bucket and then matching the results against the supplied object name.
51 * If a file ends with "/ then Google Cloud Console will show it as a 'folder'
52 * in the UI tool, so we consider an object that ends in "/" as a directory
53 * as well. For backward compatibility, we also treat files with the
54 * "_$folder$" suffix as folders.
56 public function stat() {
57 $prefix = $this->prefix;
58 if (StringUtil::endsWith($prefix, parent::DELIMITER)) {
59 $prefix = substr($prefix, 0, strlen($prefix) - 1);
62 if (isset($prefix)) {
63 $result = $this->headObject($prefix);
64 if ($result !== false) {
65 $mode = parent::S_IFREG;
66 $mtime = $result['mtime'];
67 $size = $result['size'];
68 } else {
69 // Object doesn't exisit, check and see if it's a directory.
70 do {
71 $results = $this->listBucket($prefix);
72 if (false === $results) {
73 return false;
75 // If there are no results then we're done
76 if (empty($results)) {
77 return false;
79 // If there is an entry that contains object_name_$folder$ or
80 // object_name/ then we have a 'directory'.
81 $object_name_folder = $prefix . parent::FOLDER_SUFFIX;
82 $object_name_delimiter = $prefix . parent::DELIMITER;
83 foreach ($results as $result) {
84 if ($result['name'] === $object_name_folder ||
85 $result['name'] === $object_name_delimiter) {
86 $mode = parent::S_IFDIR;
87 break;
90 } while (!isset($mode) && isset($this->next_marker));
92 } else {
93 // We are now just checking that the bucket exists, as there was no
94 // object prefix supplied
95 $results = $this->listBucket();
96 if ($results !== false) {
97 $mode = parent::S_IFDIR;
98 } else {
99 return false;
102 // If mode is not set, then there was no object that matched the criteria.
103 if (!isset($mode)) {
104 return false;
106 // If the app could stat the file, then it must be readable. As different
107 // PHP internal APIs check the access mode, we'll set them all to readable.
108 $mode |= parent::S_IRUSR | parent::S_IRGRP | parent::S_IROTH;
110 if ($this->isBucketWritable($this->bucket_name)) {
111 $mode |= parent::S_IWUSR | parent::S_IWGRP | parent::S_IWOTH;
114 $stat_args["mode"] = $mode;
115 if (isset($mtime)) {
116 $unix_time = strtotime($mtime);
117 if ($unix_time !== false) {
118 $stat_args["mtime"] = $unix_time;
122 if (isset($size)) {
123 $stat_args["size"] = intval($size);
125 return $this->createStatArray($stat_args);
129 * Perform a HEAD request on an object to get size & mtime info.
131 private function headObject($object_name) {
132 $headers = $this->getOAuthTokenHeader(parent::READ_SCOPE);
133 if ($headers === false) {
134 if (!$this->quiet) {
135 trigger_error("Unable to acquire OAuth token.", E_USER_WARNING);
137 return false;
140 $url = $this->createObjectUrl($this->bucket_name, $object_name);
141 $http_response = $this->makeHttpRequest($url, "HEAD", $headers);
142 if ($http_response === false) {
143 if (!$this->quiet) {
144 trigger_error('Unable to connect to the Cloud Storage Service.',
145 E_USER_WARNING);
147 return false;
150 $status_code = $http_response['status_code'];
151 if (HttpResponse::OK !== $status_code) {
152 if (!$this->quiet && HttpResponse::NOT_FOUND !== $status_code) {
153 trigger_error($this->getErrorMessage($http_response['status_code'],
154 $http_response['body']),
155 E_USER_WARNING);
157 return false;
160 $headers = $http_response['headers'];
161 return ['size' => $this->getHeaderValue('x-goog-stored-content-length',
162 $headers),
163 'mtime' => $this->getHeaderValue('Last-Modified', $headers)];
167 * Perform a GET request on a bucket, with the optional $object_prefix. This
168 * is similar to how CloudStorgeDirectoryClient works, except that it is
169 * targeting a specific file rather than trying to enumerate of the files in
170 * a given bucket with a common prefix.
172 private function listBucket($object_prefix = null) {
173 $headers = $this->getOAuthTokenHeader(parent::READ_SCOPE);
174 if ($headers === false) {
175 if (!$this->quiet) {
176 trigger_error("Unable to acquire OAuth token.", E_USER_WARNING);
178 return false;
181 $query_arr = [
182 'delimiter' => parent::DELIMITER,
183 'max-keys' => self::MAX_KEYS,
185 if (isset($object_prefix)) {
186 $query_arr['prefix'] = $object_prefix;
188 if (isset($this->next_marker)) {
189 $query_arr['marker'] = $this->next_marker;
192 $url = $this->createObjectUrl($this->bucket_name);
193 $query_str = http_build_query($query_arr);
194 $http_response = $this->makeHttpRequest(sprintf("%s?%s", $url, $query_str),
195 "GET",
196 $headers);
197 if ($http_response === false) {
198 if (!$this->quiet) {
199 trigger_error('Unable to connect to the Cloud Storage Service.',
200 E_USER_WARNING);
202 return false;
205 if (HttpResponse::OK !== $http_response['status_code']) {
206 if (!$this->quiet) {
207 trigger_error($this->getErrorMessage($http_response['status_code'],
208 $http_response['body']),
209 E_USER_WARNING);
211 return false;
214 // Extract the files into the result array.
215 $xml = simplexml_load_string($http_response['body']);
217 if (isset($xml->NextMarker)) {
218 $this->next_marker = (string) $xml->NextMarker;
219 } else {
220 $this->next_marker = null;
223 $results = [];
224 foreach($xml->Contents as $content) {
225 $results [] = [
226 'name' => (string) $content->Key,
227 'size' => (string) $content->Size,
228 'mtime' => (string) $content->LastModified,
231 // Subdirectories will be returned in the CommonPrefixes section. Refer to
232 // https://developers.google.com/storage/docs/reference-methods#getbucket
233 foreach($xml->CommonPrefixes as $common_prefix) {
234 $results[] = [
235 'name' => (string) $common_prefix->Prefix,
238 return $results;
242 * Test if a given bucket is writable. We will cache results in memcache as
243 * this is an expensive operation. This might lead to incorrect results being
244 * returned for this call for a short period while the result remains in the
245 * cache.
247 private function isBucketWritable($bucket) {
248 $cache_key_name = sprintf(parent::WRITABLE_MEMCACHE_KEY_FORMAT, $bucket);
249 $memcache = new \Memcache();
250 $result = $memcache->get($cache_key_name);
252 if ($result) {
253 return $result['is_writable'];
256 // We determine if the bucket is writable by trying to start a resumable
257 // upload. GCS will cleanup the abandoned upload after 7 days, and it will
258 // not be charged to the bucket owner.
259 $token_header = $this->getOAuthTokenHeader(parent::WRITE_SCOPE);
260 if ($token_header === false) {
261 return false;
263 $headers = array_merge(parent::$upload_start_header, $token_header);
264 $url = parent::createObjectUrl($bucket, parent::WRITABLE_TEMP_FILENAME);
265 $http_response = $this->makeHttpRequest($url,
266 "POST",
267 $headers);
269 if ($http_response === false) {
270 return false;
273 $status_code = $http_response['status_code'];
274 $is_writable = $status_code == HttpResponse::CREATED;
276 $memcache->set($cache_key_name,
277 ['is_writable' => $is_writable],
278 null,
279 $this->context_options['writable_cache_expiry_seconds']);
280 return $is_writable;