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.
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
;
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;
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
;
41 // Drop the leading '/' from the object name.
42 $this->prefix
= substr($object, 1);
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);
63 $result = $this->headObject($prefix);
64 if ($result !== false) {
65 $mode = parent
::S_IFREG
;
66 $mtime = $result['mtime'];
67 $size = $result['size'];
69 // Object doesn't exisit, check and see if it's a directory.
71 $results = $this->listBucket($prefix);
72 if (false === $results) {
75 // If there are no results then we're done
76 if (empty($results)) {
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
;
90 } while (!isset($mode) && isset($this->next_marker
));
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
;
102 // If mode is not set, then there was no object that matched the criteria.
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;
116 $unix_time = strtotime($mtime);
117 if ($unix_time !== false) {
118 $stat_args["mtime"] = $unix_time;
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) {
135 trigger_error("Unable to acquire OAuth token.", E_USER_WARNING
);
140 $url = $this->createObjectUrl($this->bucket_name
, $object_name);
141 $http_response = $this->makeHttpRequest($url, "HEAD", $headers);
142 if ($http_response === false) {
144 trigger_error('Unable to connect to the Cloud Storage Service.',
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']),
160 $headers = $http_response['headers'];
161 return ['size' => $this->getHeaderValue('x-goog-stored-content-length',
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) {
176 trigger_error("Unable to acquire OAuth token.", E_USER_WARNING
);
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),
197 if ($http_response === false) {
199 trigger_error('Unable to connect to the Cloud Storage Service.',
205 if (HttpResponse
::OK
!== $http_response['status_code']) {
207 trigger_error($this->getErrorMessage($http_response['status_code'],
208 $http_response['body']),
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
;
220 $this->next_marker
= null;
224 foreach($xml->Contents
as $content) {
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) {
235 'name' => (string) $common_prefix->Prefix
,
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
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);
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) {
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,
269 if ($http_response === 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],
279 $this->context_options
['writable_cache_expiry_seconds']);