blob: 101acdc1f7b8b0b4eb8b5f90171087822b2d2839 [file] [log] [blame]
<?php
/**
* Copyright 2007 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* Cloud Storage Url Stat Client handles stat() calls for objects and buckets.
*
*/
namespace google\appengine\ext\cloud_storage_streams;
require_once 'google/appengine/ext/cloud_storage_streams/CloudStorageClient.php';
require_once 'google/appengine/util/string_util.php';
use \google\appengine\util as util;
/**
* Client for stating objects in Google Cloud Storage.
*/
final class CloudStorageUrlStatClient extends CloudStorageClient {
// Maximum number of keys to return when querying a bucket.
const MAX_KEYS = 1000;
private $quiet;
private $prefix = null;
private $next_marker = null;
public function __construct($bucket, $object, $context, $flags) {
parent::__construct($bucket, $object, $context);
$this->quiet = ($flags & STREAM_URL_STAT_QUIET) == STREAM_URL_STAT_QUIET;
if (isset($object)) {
// Drop the leading '/' from the object name.
$this->prefix = substr($object, 1);
}
}
/**
* The stat function uses GET requests to the bucket to try and determine if
* the object is a 'file' or a 'directory', by listing the contents of the
* bucket and then matching the results against the supplied object name.
*
* If a file ends with "_$folder$" then Google Cloud Storage Manager will
* show it as a 'folder' in the UI tool, so we consider an object that ends
* in "_$folder$" as a directory as well.
*/
public function stat() {
$prefix = $this->prefix;
if (util\endsWith($prefix, parent::DELIMITER)) {
$prefix = substr($prefix, 0, strlen($prefix) - 1);
}
if (isset($prefix)) {
while (!isset($mode)) {
$results = $this->makeRequest($prefix);
if (false === $results) {
return false;
}
// If there are no results then we're done
if (empty($results)) {
return false;
}
// If there is an entry in $results that contains the object_name
// exactly then we have a matching file - If there is an entry that
// contains object_name_$folder$ or object_name/ then we have a
// 'directory'
$object_name_folder = $prefix . parent::FOLDER_SUFFIX;
$object_name_delimiter = $prefix . parent::DELIMITER;
foreach ($results as $result) {
if ($result['name'] === $prefix) {
$mode = parent::S_IFREG;
$mtime = $result['mtime'];
$size = $result['size'];
break;
} else if ($result['name'] === $object_name_folder ||
strncmp($result['name'],
$object_name_delimiter,
strlen($object_name_delimiter)) == 0) {
$mode = parent::S_IFDIR;
break;
}
}
}
} else {
// We are now just checking that the bucket exists, as there was no
// object prefix supplied
$results = $this->makeRequest();
if ($results !== false) {
$mode = parent::S_IFDIR;
} else {
return false;
}
}
// If mode is not set, then there was no object that matched the criteria.
if (!isset($mode)) {
return false;
}
// If the app could stat the file, then it must be readable. As different
// PHP internal APIs check the access mode, we'll set them all to readable.
$mode |= parent::S_IRUSR | parent::S_IRGRP | parent::S_IROTH;
if ($this->isBucketWritable($this->bucket_name)) {
$mode |= parent::S_IWUSR | parent::S_IWGRP | parent::S_IWOTH;
}
$stat_args["mode"] = $mode;
if (isset($mtime)) {
$unix_time = strtotime($mtime);
if ($unix_time !== false) {
$stat_args["mtime"] = $unix_time;
}
}
if (isset($size)) {
$stat_args["size"] = intval($size);
}
return $this->createStatArray($stat_args);
}
/**
* Perform a GET request on a bucket, with the optional $object_prefix. This
* is similar to how CloudStorgeDirectoryClient works, except that it is
* targeting a specific file rather than trying to enumerate of the files in
* a given bucket with a common prefix.
*/
private function makeRequest($object_prefix = null) {
$headers = $this->getOAuthTokenHeader(parent::READ_SCOPE);
if ($headers === false) {
if (!$this->quiet) {
trigger_error("Unable to acquire OAuth token.", E_USER_WARNING);
}
return false;
}
$query_arr = [
'delimiter' => parent::DELIMITER,
'max-keys' => self::MAX_KEYS,
];
if (isset($object_prefix)) {
$query_arr['prefix'] = $object_prefix;
}
if (isset($this->next_marker)) {
$query_arr['marker'] = $this->next_marker;
}
$url = $this->createObjectUrl($this->bucket_name);
$query_str = http_build_query($query_arr);
$http_response = $this->makeHttpRequest(sprintf("%s?%s", $url, $query_str),
"GET",
$headers);
if ($http_response === false) {
if (!$this->quiet) {
trigger_error('Unable to connect to the Cloud Storage Service.',
E_USER_WARNING);
}
return false;
}
if (HttpResponse::OK !== $http_response['status_code']) {
if (!$this->quiet) {
trigger_error($this->getErrorMessage($http_response['status_code'],
$http_response['body']),
E_USER_WARNING);
}
return false;
}
// Extract the files into the result array.
$xml = simplexml_load_string($http_response['body']);
if (isset($xml->NextMarker)) {
$this->next_marker = (string) $xml->NextMarker;
} else {
$this->next_marker = null;
}
$results = [];
foreach($xml->Contents as $content) {
$results [] = [
'name' => (string) $content->Key,
'size' => (string) $content->Size,
'mtime' => (string) $content->LastModified,
];
}
// Subdirectories will be returned in the CommonPrefixes section. Refer to
// https://developers.google.com/storage/docs/reference-methods#getbucket
foreach($xml->CommonPrefixes as $common_prefix) {
$results[] = [
'name' => (string) $common_prefix->Prefix,
];
}
return $results;
}
/**
* Test if a given bucket is writable. We will cache results in memcache as
* this is an expensive operation. This might lead to incorrect results being
* returned for this call for a short period while the result remains in the
* cache.
*/
private function isBucketWritable($bucket) {
$cache_key_name = sprintf(parent::WRITABLE_MEMCACHE_KEY_FORMAT, $bucket);
$memcache = new \Memcache();
$result = $memcache->get($cache_key_name);
if ($result) {
return $result['is_writable'];
}
// We determine if the bucket is writable by trying to start a resumable
// upload. GCS will cleanup the abandoned upload after 7 days, and it will
// not be charged to the bucket owner.
$token_header = $this->getOAuthTokenHeader(parent::WRITE_SCOPE);
if ($token_header === false) {
return false;
}
$headers = array_merge(parent::$upload_start_header, $token_header);
$url = parent::createObjectUrl($bucket, parent::WRITABLE_TEMP_FILENAME);
$http_response = $this->makeHttpRequest($url,
"POST",
$headers);
if ($http_response === false) {
return false;
}
$status_code = $http_response['status_code'];
$is_writable = $status_code == HttpResponse::CREATED;
$memcache->set($cache_key_name,
['is_writable' => $is_writable],
null,
$this->context_options['writable_cache_expiry_seconds']);
return $is_writable;
}
}