array( * 'username' => USERNAME, * 'password' => PASSWORD, * 'tenantid' => TENANT_ID, * 'tenantname' => TENANT_NAME, // Optional instead of tenantid. * 'endpoint' => AUTH_ENDPOINT_URL, * ) * ) * ); * // Open the file. * $handle = fopen('swift://mycontainer/myobject.txt', 'r+', false, $context); * * // You can get the entire file, or use fread() to loop through the file. * $contents = stream_get_contents($handle); * * fclose($handle); * ?> * * Remarks * - file_get_contents() works fine. * - You can write to a stream, too. Nothing is pushed to the server until * fflush() or fclose() is called. * - Mode strings (w, r, w+, r+, c, c+, a, a+, x, x+) all work, though certain * constraints are slightly relaxed to accomodate efficient local buffering. * - Files are buffered locally. * * USING FILE-LEVEL FUNCTIONS * * PHP provides a number of file-level functions that stream wrappers can * optionally support. Here are a few such functions: * * - file_exists() * - is_readable() * - stat() * - filesize() * - fileperms() * * The OpenStack stream wrapper provides support for these file-level functions. * But there are a few things you should know: * * - Each call to one of these functions generates at least one request. It may * be as many as three: * * An auth request * * A request for the container (to get container permissions) * * A request for the object * - IMPORTANT: Unlike the fopen()/fclose()... functions NONE of these functions * retrieves the body of the file. If you are working with large files, using * these functions may be orders of magnitude faster than using fopen(), etc. * (The crucial detail: These kick off a HEAD request, will fopen() does a * GET request). * - You must use Bootstrap::setConfiguration() to pass in all of the values you * would normally pass into a stream context: * * endpoint * * username * * password * - Most of the information from this family of calls can also be obtained using * fstat(). If you were going to open a stream anyway, you might as well use * fopen()/fstat(). * - stat() and fstat() fake the permissions and ownership as follows: * * uid/gid are always sset to the current user. This basically assumes that if * the current user can access the object, the current user has ownership over * the file. As the OpenStack ACL system developers, this may change. * * Mode is faked. Swift uses ACLs, not UNIX mode strings. So we fake the string: * - 770: The ACL has the object marked as private. * - 775: The ACL has the object marked as public. * - ACLs are actually set on the container, so every file in a public container * will return 775. * - stat/fstat provide only one timestamp. Swift only tracks mtime, so mtime, atime, * and ctime are all set to the last modified time. * * DIRECTORIES * * OpenStack Swift does not really have directories. Rather, it allows * characters such as '/' to be used to designate namespaces on object * names. (For simplicity, this library uses only '/' as a separator). * * This allows for simulated directory listings. Requesting * `scandir('swift://foo/bar/')` is really a request to "find all of the items * in the 'foo' container whose names start with 'bar/'". * * Because of this... * * - Directory reading functions like scandir(), opendir(), readdir() * and so forth are supported. * - Functions to create or remove directories (mkdir() and rmdir()) are * meaningless, and thus not supported. * * Swift still has support for "directory markers" (special zero-byte files * that act like directories). However, since there are no standards for how * said markers ought to be created, they are not supported by the stream * wrapper. * * As usual, the underlying \OpenStack\ObjectStore\v1\Resource\Container class * supports the full range of Swift features. * * SUPPORTED CONTEXT PARAMETERS * * This section details paramters that can be passed either * through a stream context or through * \OpenStack\Bootstrap\setConfiguration(). * * PHP functions that do not allow you to pass a context may still be supported * here IF you have set options using Bootstrap::setConfiguration(). * * You are required to pass in authentication information. This * comes in one of three forms: * * -# User login: username, password, tenantid, endpoint * -# Existing (valid) token: token, swift_endpoint * * As of 1.0.0-beta6, you may use `tenantname` instead of `tenantid`. * * The third method (token) can be used when the application has already * authenticated. In this case, a token has been generated and assigned * to an user and tenant. * * The following parameters may be set either in the stream context * or through OpenStack\Bootstrap::setConfiguration(): * * - token: An auth token. If this is supplied, authentication is skipped and * this token is used. NOTE: You MUST set swift_endpoint if using this * option. * - swift_endpoint: The URL to the swift instance. This is only necessary if * 'token' is set. Otherwise it is ignored. * - username: A username. MUST be accompanied by 'password' and 'tenantid' (or 'tenantname'). * - password: A password. MUST be accompanied by 'username' and 'tenantid' (or 'tenantname'). * - endpoint: The URL to the authentication endpoint. Necessary if you are not * using a 'token' and 'swift_endpoint'. * - use_swift_auth: If this is set to true, it will force the app to use * the deprecated swiftAuth instead of IdentityService authentication. * In general, you should avoid using this. * - content_type: This is effective only when writing files. It will * set the Content-Type of the file during upload. * - tenantid: The tenant ID for the services you will use. (A user may * have multiple tenancies associated.) * - tenantname: The tenant name for the services you will use. You may use * this in lieu of tenant ID. * * @see http://us3.php.net/manual/en/class.streamwrapper.php * * @todo The service catalog should be cached in the context like the token so that * it can be retrieved later. */ class StreamWrapper { const DEFAULT_SCHEME = 'swift'; /** * Cache of auth token -> service catalog. * * This will eventually be replaced by a better system, but for a system of * moderate complexity, many, many file operations may be run during the * course of a request. Caching the catalog can prevent numerous calls * to identity services. */ protected static $serviceCatalogCache = array(); /** * The stream context. * * This is set automatically when the stream wrapper is created by * PHP. Note that it is not set through a constructor. */ public $context; protected $contextArray = array(); protected $schemeName = self::DEFAULT_SCHEME; protected $authToken; // File flags. These should probably be replaced by O_ const's at some point. protected $isBinary = false; protected $isText = true; protected $isWriting = false; protected $isReading = false; protected $isTruncating = false; protected $isAppending = false; protected $noOverwrite = false; protected $createIfNotFound = true; /** * If this is true, no data is ever sent to the remote server. */ protected $isNeverDirty = false; protected $triggerErrors = false; /** * Indicate whether the local differs from remote. * * When the file is modified in such a way that * it needs to be written remotely, the isDirty flag * is set to true. */ protected $isDirty = false; /** * Object storage instance. */ protected $store; /** * The Container. */ protected $container; /** * The Object. */ protected $obj; /** * The IO stream for the Object. */ protected $objStream; /** * Directory listing. * * Used for directory methods. */ protected $dirListing = array(); protected $dirIndex = 0; protected $dirPrefix = ''; /** * Close a directory. * * This closes a directory handle, freeing up the resources. * * * * NB: Some versions of PHP 5.3 don't clear all buffers when * closing, and the handle can occasionally remain accessible for * some period of time. */ public function dir_closedir() { $this->dirIndex = 0; $this->dirListing = array(); //syslog(LOG_WARNING, "CLOSEDIR called."); return true; } /** * Open a directory for reading. * * * * See opendir() and scandir(). * * @param string $path The URL to open. * @param int $options Unused. * * @return boolean true if the directory is opened, false otherwise. */ public function dir_opendir($path, $options) { $url = $this->parseUrl($path); if (empty($url['host'])) { trigger_error('Container name is required.' , E_USER_WARNING); return false; } try { $this->initializeObjectStorage(); $container = $this->store->container($url['host']); if (empty($url['path'])) { $this->dirPrefix = ''; } else { $this->dirPrefix = $url['path']; } $sep = '/'; $this->dirListing = $container->objectsWithPrefix($this->dirPrefix, $sep); } catch (\OpenStack\Common\Exception $e) { trigger_error('Directory could not be opened: ' . $e->getMessage(), E_USER_WARNING); return false; } return true; } /** * Read an entry from the directory. * * This gets a single line from the directory. * * * * @return string The name of the resource or false when the directory has no * more entries. */ public function dir_readdir() { // If we are at the end of the listing, return false. if (count($this->dirListing) <= $this->dirIndex) { return false; } $curr = $this->dirListing[$this->dirIndex]; $this->dirIndex++; if ($curr instanceof \OpenStack\ObjectStore\v1\Resource\Subdir) { $fullpath = $curr->path(); } else { $fullpath = $curr->name(); } if (!empty($this->dirPrefix)) { $len = strlen($this->dirPrefix); $fullpath = substr($fullpath, $len); } return $fullpath; } /** * Rewind to the beginning of the listing. * * This repositions the read pointer at the first entry in the directory. * * */ public function dir_rewinddir() { $this->dirIndex = 0; } /* public function mkdir($path, $mode, $options) { } public function rmdir($path, $options) { } */ /** * Rename a swift object. * * This works by copying the object (metadata) and * then removing the original version. * * This DOES support cross-container renaming. * * @see Container::copy(). * * 'foo@example.com', * // 'tenantid' => '1234', // You can use this instead of tenantname * 'username' => 'foobar', * 'password' => 'baz', * 'endpoint' => 'https://auth.example.com', * )); * * $from = 'swift://containerOne/file.txt'; * $to = 'swift://containerTwo/file.txt'; * * // Rename can also take a context as a third param. * rename($from, $to); * * ?> * * @param string $path_from A swift URL that exists on the remote. * @param string $path_to A swift URL to another path. * * @return boolean true on success, false otherwise. */ public function rename($path_from, $path_to) { $this->initializeObjectStorage(); $src = $this->parseUrl($path_from); $dest = $this->parseUrl($path_to); if ($src['scheme'] != $dest['scheme']) { trigger_error("I'm too stupid to copy across protocols.", E_USER_WARNING); } if ( empty($src['host']) || empty($src['path']) || empty($dest['host']) || empty($dest['path'])) { trigger_error('Container and path are required for both source and destination URLs.', E_USER_WARNING); return false; } try { $container = $this->store->container($src['host']); $object = $container->remoteObject($src['path']); $ret = $container->copy($object, $dest['path'], $dest['host']); if ($ret) { return $container->delete($src['path']); } } catch (\OpenStack\Common\Exception $e) { trigger_error('Rename was not completed: ' . $e->getMessage(), E_USER_WARNING); return false; } } /* public function copy($path_from, $path_to) { throw new \Exception("UNDOCUMENTED."); } */ /** * Cast stream into a lower-level stream. * * This is used for stream_select() and perhaps others.Because it exposes * the lower-level buffer objects, this function can have unexpected * side effects. * * @return resource this returns the underlying stream. */ public function stream_cast($cast_as) { return $this->objStream; } /** * Close a stream, writing if necessary. * * * * This will close the present stream. Importantly, * this will also write to the remote object storage if * any changes have been made locally. * * @see stream_open(). */ public function stream_close() { try { $this->writeRemote(); } catch (\OpenStack\Common\Exception $e) { trigger_error('Error while closing: ' . $e->getMessage(), E_USER_NOTICE); return false; } // Force-clear the memory hogs. unset($this->obj); fclose($this->objStream); } /** * Check whether the stream has reached its end. * * This checks whether the stream has reached the * end of the object's contents. * * Called when `feof()` is called on a stream. * * @see stream_seek(). * * @return boolean true if it has reached the end, false otherwise. */ public function stream_eof() { return feof($this->objStream); } /** * Initiate saving data on the remote object storage. * * If the local copy of this object has been modified, * it is written remotely. * * Called when `fflush()` is called on a stream. */ public function stream_flush() { try { $this->writeRemote(); } catch (\OpenStack\Common\Exception $e) { syslog(LOG_WARNING, $e); trigger_error('Error while flushing: ' . $e->getMessage(), E_USER_NOTICE); return false; } } /** * Write data to the remote object storage. * * Internally, this is used by flush and close. */ protected function writeRemote() { $contentType = $this->cxt('content_type'); if (!empty($contentType)) { $this->obj->setContentType($contentType); } // Skip debug streams. if ($this->isNeverDirty) { return; } // Stream is dirty and needs a write. if ($this->isDirty) { $position = ftell($this->objStream); rewind($this->objStream); $this->container->save($this->obj, $this->objStream); fseek($this->objStream, SEEK_SET, $position); } $this->isDirty = false; } /* * Locking is currently unsupported. * * There is no remote support for locking a * file. public function stream_lock($operation) { } */ /** * Open a stream resource. * * This opens a given stream resource and prepares it for reading or writing. * * 'foobar', * 'tenantid' => '987654321', * 'password' => 'eieio', * 'endpoint' => 'https://auth.example.com', * )); * ?> * * $file = fopen('swift://myContainer/myObject.csv', 'rb', false, $cxt); * while ($bytes = fread($file, 8192)) { * print $bytes; * } * fclose($file); * ?> * * If a file is opened in write mode, its contents will be retrieved from the * remote storage and cached locally for manipulation. If the file is opened * in a write-only mode, the contents will be created locally and then pushed * remotely as necessary. * * During this operation, the remote host may need to be contacted for * authentication as well as for file retrieval. * * @param string $path The URL to the resource. See the class description for * details, but typically this expects URLs in the form `swift://CONTAINER/OBJECT`. * @param string $mode Any of the documented mode strings. See fopen(). For * any file that is in a writing mode, the file will be saved remotely on * flush or close. Note that there is an extra mode: 'nope'. It acts like * 'c+' except that it is never written remotely. This is useful for * debugging the stream locally without sending that data to object storage. * (Note that data is still fetched -- just never written.) * @param int $options An OR'd list of options. Only STREAM_REPORT_ERRORS has * any meaning to this wrapper, as it is not working with local files. * @param string $opened_path This is not used, as this wrapper deals only * with remote objects. */ public function stream_open($path, $mode, $options, &$opened_path) { //syslog(LOG_WARNING, "I received this URL: " . $path); // If STREAM_REPORT_ERRORS is set, we are responsible for // all error handling while opening the stream. if (STREAM_REPORT_ERRORS & $options) { $this->triggerErrors = true; } // Using the mode string, set the internal mode. $this->setMode($mode); // Parse the URL. $url = $this->parseUrl($path); //syslog(LOG_WARNING, print_r($url, true)); // Container name is required. if (empty($url['host'])) { //if ($this->triggerErrors) { trigger_error('No container name was supplied in ' . $path, E_USER_WARNING); //} return false; } // A path to an object is required. if (empty($url['path'])) { //if ($this->triggerErrors) { trigger_error('No object name was supplied in ' . $path, E_USER_WARNING); //} return false; } // We set this because it is possible to bind another scheme name, // and we need to know that name if it's changed. //$this->schemeName = isset($url['scheme']) ? $url['scheme'] : self::DEFAULT_SCHEME; if (isset($url['scheme'])) { $this->schemeName == $url['scheme']; } // Now we find out the container name. We walk a fine line here, because we don't // create a new container, but we don't want to incur heavy network // traffic, either. So we have to assume that we have a valid container // until we issue our first request. $containerName = $url['host']; // Object name. $objectName = $url['path']; // XXX: We reserve the query string for passing additional params. try { $this->initializeObjectStorage(); } catch (\OpenStack\Common\Exception $e) { trigger_error('Failed to init object storage: ' . $e->getMessage(), E_USER_WARNING); return false; } //syslog(LOG_WARNING, "Container: " . $containerName); // Now we need to get the container. Doing a server round-trip here gives // us the peace of mind that we have an actual container. // XXX: Should we make it possible to get a container blindly, without the // server roundtrip? try { $this->container = $this->store->container($containerName); } catch (\OpenStack\Common\Transport\Exception\FileNotFoundException $e) { trigger_error('Container not found.', E_USER_WARNING); return false; } try { // Now we fetch the file. Only under certain circumstances do we generate // an error if the file is not found. // FIXME: We should probably allow a context param that can be set to // mark the file as lazily fetched. $this->obj = $this->container->object($objectName); $stream = $this->obj->stream(); $streamMeta = stream_get_meta_data($stream); // Support 'x' and 'x+' modes. if ($this->noOverwrite) { //if ($this->triggerErrors) { trigger_error('File exists and cannot be overwritten.', E_USER_WARNING); //} return false; } // If we need to write to it, we need a writable // stream. Also, if we need to block reading, this // will require creating an alternate stream. if ($this->isWriting && ($streamMeta['mode'] == 'r' || !$this->isReading)) { $newMode = $this->isReading ? 'rb+' : 'wb'; $tmpStream = fopen('php://temp', $newMode); stream_copy_to_stream($stream, $tmpStream); // Skip rewinding if we can. if (!$this->isAppending) { rewind($tmpStream); } $this->objStream = $tmpStream; } else { $this->objStream = $this->obj->stream(); } // Append mode requires seeking to the end. if ($this->isAppending) { fseek($this->objStream, -1, SEEK_END); } } // If a 404 is thrown, we need to determine whether // or not a new file should be created. catch (\OpenStack\Common\Transport\Exception\FileNotFoundException $nf) { // For many modes, we just go ahead and create. if ($this->createIfNotFound) { $this->obj = new Object($objectName); $this->objStream = fopen('php://temp', 'rb+'); $this->isDirty = true; } else { //if ($this->triggerErrors) { trigger_error($nf->getMessage(), E_USER_WARNING); //} return false; } } // All other exceptions are fatal. catch (\OpenStack\Common\Exception $e) { //if ($this->triggerErrors) { trigger_error('Failed to fetch object: ' . $e->getMessage(), E_USER_WARNING); //} return false; } // At this point, we have a file that may be read-only. It also may be // reading off of a socket. It will be positioned at the beginning of // the stream. return true; } /** * Read N bytes from the stream. * * This will read up to the requested number of bytes. Or, upon * hitting the end of the file, it will return null. * * @see fread(), fgets(), and so on for examples. * * 'me@example.com', * 'username' => 'me@example.com', * 'password' => 'secret', * 'endpoint' => 'https://auth.example.com', * )); * * $content = file_get_contents('swift://public/myfile.txt', false, $cxt); * ?> * * @param int $count The number of bytes to read (usually 8192). * * @return string The data read. */ public function stream_read($count) { return fread($this->objStream, $count); } /** * Perform a seek. * * This is called whenever `fseek()` or `rewind()` is called on a * Swift stream. * * IMPORTANT: Unlike the PHP core, this library * allows you to `fseek()` inside of a file opened * in append mode ('a' or 'a+'). */ public function stream_seek($offset, $whence) { $ret = fseek($this->objStream, $offset, $whence); // fseek returns 0 for success, -1 for failure. // We need to return true for success, false for failure. return $ret === 0; } /** * Set options on the underlying stream. * * The settings here do not trickle down to the network socket, which is * left open for only a brief period of time. Instead, they impact the middle * buffer stream, where the file is read and written to between flush/close * operations. Thus, tuning these will not have any impact on network * performance. * * See stream_set_blocking(), stream_set_timeout(), and stream_write_buffer(). */ public function stream_set_option($option, $arg1, $arg2) { switch ($option) { case STREAM_OPTION_BLOCKING: return stream_set_blocking($this->objStream, $arg1); case STREAM_OPTION_READ_TIMEOUT: // XXX: Should this have any effect on the lower-level // socket, too? Or just the buffered tmp storage? return stream_set_timeout($this->objStream, $arg1, $arg2); case STREAM_OPTION_WRITE_BUFFER: return stream_set_write_buffer($this->objStream, $arg2); } } /** * Perform stat()/lstat() operations. * * * * To use standard `stat()` on a Swift stream, you will * need to set account information (tenant ID, username, password, * etc.) through \OpenStack\Bootstrap::setConfiguration(). * * @return array The stats array. */ public function stream_stat() { $stat = fstat($this->objStream); // FIXME: Need to calculate the length of the $objStream. //$contentLength = $this->obj->contentLength(); $contentLength = $stat['size']; return $this->generateStat($this->obj, $this->container, $contentLength); } /** * Get the current position in the stream. * * @see `ftell()` and `fseek()`. * * @return int The current position in the stream. */ public function stream_tell() { return ftell($this->objStream); } /** * Write data to stream. * * This writes data to the local stream buffer. Data * is not pushed remotely until stream_close() or * stream_flush() is called. * * @param string $data Data to write to the stream. * * @return int The number of bytes written. 0 indicates and error. */ public function stream_write($data) { $this->isDirty = true; return fwrite($this->objStream, $data); } /** * Unlink a file. * * This removes the remote copy of the file. Like a normal unlink operation, * it does not destroy the (local) file handle until the file is closed. * Therefore you can continue accessing the object locally. * * Note that OpenStack Swift does not draw a distinction between file objects * and "directory" objects (where the latter is a 0-byte object). This will * delete either one. If you are using directory markers, not that deleting * a marker will NOT delete the contents of the "directory". * * You will need to use \OpenStack\Bootstrap::setConfiguration() to set the * necessary stream configuration, since `unlink()` does not take a context. * * @param string $path The URL. * * @return boolean true if the file was deleted, false otherwise. */ public function unlink($path) { $url = $this->parseUrl($path); // Host is required. if (empty($url['host'])) { trigger_error('Container name is required.', E_USER_WARNING); return false; } // I suppose we could allow deleting containers, // but that isn't really the purpose of the // stream wrapper. if (empty($url['path'])) { trigger_error('Path is required.', E_USER_WARNING); return false; } try { $this->initializeObjectStorage(); // $container = $this->store->container($url['host']); $name = $url['host']; $token = $this->store->token(); $endpoint_url = $this->store->url() . '/' . rawurlencode($name); $client = $this->cxt('transport_client', null); $container = new \OpenStack\ObjectStore\v1\Resource\Container($name, $endpoint_url, $token, $client); return $container->delete($url['path']); } catch (\OpenStack\Common\Exception $e) { trigger_error('Error during unlink: ' . $e->getMessage(), E_USER_WARNING); return false; } } /** * @see stream_stat(). */ public function url_stat($path, $flags) { $url = $this->parseUrl($path); if (empty($url['host']) || empty($url['path'])) { if ($flags & STREAM_URL_STAT_QUIET) { trigger_error('Container name (host) and path are required.', E_USER_WARNING); } return false; } try { $this->initializeObjectStorage(); // Since we are throwing the $container away without really using its // internals, we create an unchecked container. It may not actually // exist on the server, which will cause a 404 error. //$container = $this->store->container($url['host']); $name = $url['host']; $token = $this->store->token(); $endpoint_url = $this->store->url() . '/' . rawurlencode($name); $client = $this->cxt('transport_client', null); $container = new \OpenStack\ObjectStore\v1\Resource\Container($name, $endpoint_url, $token, $client); $obj = $container->remoteObject($url['path']); } catch (\OpenStack\Common\Exception $e) { // Apparently file_exists does not set STREAM_URL_STAT_QUIET. //if ($flags & STREAM_URL_STAT_QUIET) { //trigger_error('Could not stat remote file: ' . $e->getMessage(), E_USER_WARNING); //} return false; } if ($flags & STREAM_URL_STAT_QUIET) { try { return @$this->generateStat($obj, $container, $obj->contentLength()); } catch (\OpenStack\Common\Exception $e) { return false; } } return $this->generateStat($obj, $container, $obj->contentLength()); } /** * Get the Object. * * This provides low-level access to the * \OpenStack\ObjectStore\v1\ObjectStorage::Object instance in which the content * is stored. * * Accessing the object's payload (Object::content()) is strongly * discouraged, as it will modify the pointers in the stream that the * stream wrapper is using. * * HOWEVER, accessing the Object's metadata properties, content type, * and so on is okay. Changes to this data will be written on the next * flush, provided that the file stream data has also been changed. * * To access this: * * object(); * ?> */ public function object() { return $this->obj; } /** * EXPERT: Get the ObjectStorage for this wrapper. * * @return object \OpenStack\ObjectStorage An ObjectStorage object. * @see object() */ public function objectStorage() { return $this->store; } /** * EXPERT: Get the auth token for this wrapper. * * @return string A token. * @see object() */ public function token() { return $this->store->token(); } /** * EXPERT: Get the service catalog (IdentityService) for this wrapper. * * This is only available when a file is opened via fopen(). * * @return array A service catalog. * @see object() */ public function serviceCatalog() { return self::$serviceCatalogCache[$this->token()]; } /** * Generate a reasonably accurate STAT array. * * Notes on mode: * - All modes are of the (octal) form 100XXX, where * XXX is replaced by the permission string. Thus, * this always reports that the type is "file" (100). * - Currently, only two permission sets are generated: * - 770: Represents the ACL::makePrivate() perm. * - 775: Represents the ACL::makePublic() perm. * * Notes on mtime/atime/ctime: * - For whatever reason, Swift only stores one timestamp. * We use that for mtime, atime, and ctime. * * Notes on size: * - Size must be calculated externally, as it will sometimes * be the remote's Content-Length, and it will sometimes be * the cached stat['size'] for the underlying buffer. */ protected function generateStat($object, $container, $size) { // This is not entirely accurate. Basically, if the // file is marked public, it gets 100775, and if // it is private, it gets 100770. // // Mode is always set to file (100XXX) because there // is no alternative that is more sensible. PHP docs // do not recommend an alternative. // // octdec(100770) == 33272 // octdec(100775) == 33277 $mode = $container->acl()->isPublic() ? 33277 : 33272; // We have to fake the UID value in order for is_readible()/is_writable() // to work. Note that on Windows systems, stat does not look for a UID. if (function_exists('posix_geteuid')) { $uid = posix_geteuid(); $gid = posix_getegid(); } else { $uid = 0; $gid = 0; } if ($object instanceof \OpenStack\ObjectStore\v1\Resource\RemoteObject) { $modTime = $object->lastModified(); } else { $modTime = 0; } $values = array( 'dev' => 0, 'ino' => 0, 'mode' => $mode, 'nlink' => 0, 'uid' => $uid, 'gid' => $gid, 'rdev' => 0, 'size' => $size, 'atime' => $modTime, 'mtime' => $modTime, 'ctime' => $modTime, 'blksize' => -1, 'blocks' => -1, ); $final = array_values($values) + $values; return $final; } /////////////////////////////////////////////////////////////////// // INTERNAL METHODS // All methods beneath this line are not part of the Stream API. /////////////////////////////////////////////////////////////////// /** * Set the fopen mode. * * @param string $mode The mode string, e.g. `r+` or `wb`. * * @return \OpenStack\ObjectStore\v1\Resource\StreamWrapper $this so the method * can be used in chaining. */ protected function setMode($mode) { $mode = strtolower($mode); // These are largely ignored, as the remote // object storage does not distinguish between // text and binary files. Per the PHP recommendation // files are treated as binary. $this->isBinary = strpos($mode, 'b') !== false; $this->isText = strpos($mode, 't') !== false; // Rewrite mode to remove b or t: $mode = preg_replace('/[bt]?/', '', $mode); switch ($mode) { case 'r+': $this->isWriting = true; case 'r': $this->isReading = true; $this->createIfNotFound = false; break; case 'w+': $this->isReading = true; case 'w': $this->isTruncating = true; $this->isWriting = true; break; case 'a+': $this->isReading = true; case 'a': $this->isAppending = true; $this->isWriting = true; break; case 'x+': $this->isReading = true; case 'x': $this->isWriting = true; $this->noOverwrite = true; break; case 'c+': $this->isReading = true; case 'c': $this->isWriting = true; break; // nope mode: Mock read/write support, // but never write to the remote server. // (This is accomplished by never marking // the stream as dirty.) case 'nope': $this->isReading = true; $this->isWriting = true; $this->isNeverDirty = true; break; // Default case is read/write // like c+. default: $this->isReading = true; $this->isWriting = true; break; } return $this; } /** * Get an item out of the context. * * @todo Should there be an option to NOT query the Bootstrap::conf()? * * @param string $name The name to look up. First look it up in the context, * then look it up in the Bootstrap config. * @param mixed $default The default value to return if no config param was * found. * * @return mixed The discovered result, or $default if specified, or null if * no $default is specified. */ protected function cxt($name, $default = null) { // Lazilly populate the context array. if (is_resource($this->context) && empty($this->contextArray)) { $cxt = stream_context_get_options($this->context); // If a custom scheme name has been set, use that. if (!empty($cxt[$this->schemeName])) { $this->contextArray = $cxt[$this->schemeName]; } // We fall back to this just in case. elseif (!empty($cxt[self::DEFAULT_SCHEME])) { $this->contextArray = $cxt[self::DEFAULT_SCHEME]; } } // Should this be array_key_exists()? if (isset($this->contextArray[$name])) { return $this->contextArray[$name]; } // Check to see if the value can be gotten from // \OpenStack\Bootstrap. $val = \OpenStack\Bootstrap::config($name, null); if (isset($val)) { return $val; } return $default; } /** * Parse a URL. * * In order to provide full UTF-8 support, URLs must be * rawurlencoded before they are passed into the stream wrapper. * * This parses the URL and urldecodes the container name and * the object name. * * @param string $url A Swift URL. * * @return array An array as documented in parse_url(). */ protected function parseUrl($url) { $res = parse_url($url); // These have to be decode because they will later // be encoded. foreach ($res as $key => $val) { if ($key == 'host') { $res[$key] = urldecode($val); } elseif ($key == 'path') { if (strpos($val, '/') === 0) { $val = substr($val, 1); } $res[$key] = urldecode($val); } } return $res; } /** * Based on the context, initialize the ObjectStorage. * * The following parameters may be set either in the stream context * or through \OpenStack\Bootstrap::setConfiguration(): * * - token: An auth token. If this is supplied, authentication is skipped and * this token is used. NOTE: You MUST set swift_endpoint if using this * option. * - swift_endpoint: The URL to the swift instance. This is only necessary if * 'token' is set. Otherwise it is ignored. * - username: A username. MUST be accompanied by 'password' and 'tenantname'. * - password: A password. MUST be accompanied by 'username' and 'tenantname'. * - endpoint: The URL to the authentication endpoint. Necessary if you are not * using a 'token' and 'swift_endpoint'. * - use_swift_auth: If this is set to true, it will force the app to use * the deprecated swiftAuth instead of IdentityService authentication. * In general, you should avoid using this. * - transport_client: A transport client for the HTTP requests. * * To find these params, the method first checks the supplied context. If the * key is not found there, it checks the Bootstrap::conf(). * * @fixme This should be rewritten to use \ObjectStorage::newFromServiceCatalog(). */ protected function initializeObjectStorage() { $token = $this->cxt('token'); $tenantId = $this->cxt('tenantid'); $tenantName = $this->cxt('tenantname'); $authUrl = $this->cxt('endpoint'); $endpoint = $this->cxt('swift_endpoint'); $client = $this->cxt('transport_client', null); $serviceCatalog = null; if (!empty($token) && isset(self::$serviceCatalogCache[$token])) { $serviceCatalog = self::$serviceCatalogCache[$token]; } // FIXME: If a token is invalidated, we should try to re-authenticate. // If context has the info we need, start from there. if (!empty($token) && !empty($endpoint)) { $this->store = new \OpenStack\ObjectStore\v1\ObjectStorage($token, $endpoint, $client); } // If we get here and tenant ID is not set, we can't get a container. elseif (empty($tenantId) && empty($tenantName)) { throw new \OpenStack\Common\Exception('Either Tenant ID (tenantid) or Tenant Name (tenantname) is required.'); } elseif (empty($authUrl)) { throw new \OpenStack\Common\Exception('An Identity Service Endpoint (endpoint) is required.'); } // Try to authenticate and get a new token. else { $ident = $this->authenticate(); // Update token and service catalog. The old pair may be out of date. $token = $ident->token(); $serviceCatalog = $ident->serviceCatalog(); self::$serviceCatalogCache[$token] = $serviceCatalog; $this->store = ObjectStorage::newFromServiceCatalog($serviceCatalog, $token, \OpenStack\ObjectStore\v1\ObjectStorage::DEFAULT_REGION, $client); /* $catalog = $ident->serviceCatalog(ObjectStorage::SERVICE_TYPE); if (empty($catalog) || empty($catalog[0]['endpoints'][0]['publicURL'])) { //throw new \OpenStack\Common\Exception('No object storage services could be found for this tenant ID.' . print_r($catalog, true)); throw new \OpenStack\Common\Exception('No object storage services could be found for this tenant ID.'); } $serviceURL = $catalog[0]['endpoints'][0]['publicURL']; $this->store = new ObjectStorage($token, $serviceURL); */ } return !empty($this->store); } protected function authenticate() { $username = $this->cxt('username'); $password = $this->cxt('password'); $tenantId = $this->cxt('tenantid'); $tenantName = $this->cxt('tenantname'); $authUrl = $this->cxt('endpoint'); $client = $this->cxt('transport_client', null); $ident = new \OpenStack\Identity\v2\IdentityService($authUrl, $client); // Frustrated? Go burninate. http://www.homestarrunner.com/trogdor.html if (!empty($username) && !empty($password)) { $token = $ident->authenticateAsUser($username, $password, $tenantId, $tenantName); } else { throw new \OpenStack\Common\Exception('Username/password must be provided.'); } // Cache the service catalog. self::$serviceCatalogCache[$token] = $ident->serviceCatalog(); return $ident; } }