4
* (c) Copyright 2012-2014 Hewlett-Packard Development Company, L.P.
5
* (c) Copyright 2014 Rackspace US, Inc.
7
* Licensed under the Apache License, Version 2.0 (the "License"); you may
8
* not use this file except in compliance with the License. You may obtain
9
* a copy of the License at
11
* http://www.apache.org/licenses/LICENSE-2.0
13
* Unless required by applicable law or agreed to in writing, software
14
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
15
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
16
* License for the specific language governing permissions and limitations
20
namespace OpenStack\ObjectStore\v1\Resource;
22
use \OpenStack\Bootstrap;
23
use OpenStack\Common\Transport\Exception\ResourceNotFoundException;
24
use \OpenStack\ObjectStore\v1\ObjectStorage;
25
use OpenStack\Common\Exception;
28
* Provides stream wrapping for Swift.
30
* This provides a full stream wrapper to expose `swift://` URLs to the
33
* Swift streams provide authenticated and priviledged access to the
34
* swift data store. These URLs are not generally used for granting
35
* unauthenticated access to files (which can be done using the HTTP
36
* stream wrapper -- no need for swift-specific logic).
40
* This takes URLs of the following form:
42
* `swift://CONTAINER/FILE`
46
* `swift://public/example.txt`
48
* The example above would access the `public` container and attempt to
49
* retrieve the file named `example.txt`.
51
* Slashes are legal in Swift filenames, so a pathlike URL can be constructed
54
* `swift://public/path/like/file/name.txt`
56
* The above would attempt to find a file in object storage named
57
* `path/like/file/name.txt`.
59
* A note on UTF-8 and URLs: PHP does not yet natively support many UTF-8
60
* characters in URLs. Thus, you ought to rawurlencode() your container name
61
* and object name (path) if there is any possibility that it will contain
66
* This library does not support locking (e.g. flock()). This is because the
67
* OpenStack Object Storage implementation does not support locking. But there
68
* are a few related things you should keep in mind:
70
* - Working with a stream is essentially working with a COPY OF a remote file.
71
* Local locking is not an issue.
72
* - If you open two streams for the same object, you will be working with
73
* TWO COPIES of the object. This can, of course, lead to nasty race
74
* conditions if each copy is modified.
78
* The principle purpose of this wrapper is to make it easy to access and
79
* manipulate objects on a remote object storage instance. Managing
80
* containers is a secondary concern (and can often better be managed using
81
* the OpenStack API). Consequently, almost all actions done through the
82
* stream wrapper are focused on objects, not containers, servers, etc.
84
* Retrieving an Existing Object
86
* Retrieving an object is done by opening a file handle to that object.
90
* Nothing is written to the remote storage until the file is closed. This
91
* keeps network traffic at a minimum, and respects the more-or-less stateless
92
* nature of ObjectStorage.
94
* USING FILE/STREAM RESOURCES
96
* In general, you should access files like this:
99
* \OpenStack\Bootstrap::useStreamWrappers();
100
* // Set up the context.
101
* $context = stream_context_create(
102
* array('swift' => array(
103
* 'username' => USERNAME,
104
* 'password' => PASSWORD,
105
* 'tenantid' => TENANT_ID,
106
* 'tenantname' => TENANT_NAME, // Optional instead of tenantid.
107
* 'endpoint' => AUTH_ENDPOINT_URL,
112
* $handle = fopen('swift://mycontainer/myobject.txt', 'r+', false, $context);
114
* // You can get the entire file, or use fread() to loop through the file.
115
* $contents = stream_get_contents($handle);
121
* - file_get_contents() works fine.
122
* - You can write to a stream, too. Nothing is pushed to the server until
123
* fflush() or fclose() is called.
124
* - Mode strings (w, r, w+, r+, c, c+, a, a+, x, x+) all work, though certain
125
* constraints are slightly relaxed to accomodate efficient local buffering.
126
* - Files are buffered locally.
128
* USING FILE-LEVEL FUNCTIONS
130
* PHP provides a number of file-level functions that stream wrappers can
131
* optionally support. Here are a few such functions:
139
* The OpenStack stream wrapper provides support for these file-level functions.
140
* But there are a few things you should know:
142
* - Each call to one of these functions generates at least one request. It may
143
* be as many as three:
145
* * A request for the container (to get container permissions)
146
* * A request for the object
147
* - IMPORTANT: Unlike the fopen()/fclose()... functions NONE of these functions
148
* retrieves the body of the file. If you are working with large files, using
149
* these functions may be orders of magnitude faster than using fopen(), etc.
150
* (The crucial detail: These kick off a HEAD request, will fopen() does a
152
* - You must use Bootstrap::setConfiguration() to pass in all of the values you
153
* would normally pass into a stream context:
157
* - Most of the information from this family of calls can also be obtained using
158
* fstat(). If you were going to open a stream anyway, you might as well use
160
* - stat() and fstat() fake the permissions and ownership as follows:
161
* * uid/gid are always sset to the current user. This basically assumes that if
162
* the current user can access the object, the current user has ownership over
163
* the file. As the OpenStack ACL system developers, this may change.
164
* * Mode is faked. Swift uses ACLs, not UNIX mode strings. So we fake the string:
165
* - 770: The ACL has the object marked as private.
166
* - 775: The ACL has the object marked as public.
167
* - ACLs are actually set on the container, so every file in a public container
169
* - stat/fstat provide only one timestamp. Swift only tracks mtime, so mtime, atime,
170
* and ctime are all set to the last modified time.
174
* OpenStack Swift does not really have directories. Rather, it allows
175
* characters such as '/' to be used to designate namespaces on object
176
* names. (For simplicity, this library uses only '/' as a separator).
178
* This allows for simulated directory listings. Requesting
179
* `scandir('swift://foo/bar/')` is really a request to "find all of the items
180
* in the 'foo' container whose names start with 'bar/'".
184
* - Directory reading functions like scandir(), opendir(), readdir()
185
* and so forth are supported.
186
* - Functions to create or remove directories (mkdir() and rmdir()) are
187
* meaningless, and thus not supported.
189
* Swift still has support for "directory markers" (special zero-byte files
190
* that act like directories). However, since there are no standards for how
191
* said markers ought to be created, they are not supported by the stream
194
* As usual, the underlying \OpenStack\ObjectStore\v1\Resource\Container class
195
* supports the full range of Swift features.
197
* SUPPORTED CONTEXT PARAMETERS
199
* This section details paramters that can be passed either
200
* through a stream context or through
201
* \OpenStack\Bootstrap\setConfiguration().
203
* PHP functions that do not allow you to pass a context may still be supported
204
* here IF you have set options using Bootstrap::setConfiguration().
206
* You are required to pass in authentication information. This
207
* comes in one of three forms:
209
* -# User login: username, password, tenantid, endpoint
210
* -# Existing (valid) token: token, swift_endpoint
212
* As of 1.0.0-beta6, you may use `tenantname` instead of `tenantid`.
214
* The third method (token) can be used when the application has already
215
* authenticated. In this case, a token has been generated and assigned
216
* to an user and tenant.
218
* The following parameters may be set either in the stream context
219
* or through OpenStack\Bootstrap::setConfiguration():
221
* - token: An auth token. If this is supplied, authentication is skipped and
222
* this token is used. NOTE: You MUST set swift_endpoint if using this
224
* - swift_endpoint: The URL to the swift instance. This is only necessary if
225
* 'token' is set. Otherwise it is ignored.
226
* - username: A username. MUST be accompanied by 'password' and 'tenantid' (or 'tenantname').
227
* - password: A password. MUST be accompanied by 'username' and 'tenantid' (or 'tenantname').
228
* - endpoint: The URL to the authentication endpoint. Necessary if you are not
229
* using a 'token' and 'swift_endpoint'.
230
* - content_type: This is effective only when writing files. It will
231
* set the Content-Type of the file during upload.
232
* - tenantid: The tenant ID for the services you will use. (A user may
233
* have multiple tenancies associated.)
234
* - tenantname: The tenant name for the services you will use. You may use
235
* this in lieu of tenant ID.
237
* @see http://us3.php.net/manual/en/class.streamwrapper.php
239
* @todo The service catalog should be cached in the context like the token so that
240
* it can be retrieved later.
244
const DEFAULT_SCHEME = 'swift';
247
* Cache of auth token -> service catalog.
249
* This will eventually be replaced by a better system, but for a system of
250
* moderate complexity, many, many file operations may be run during the
251
* course of a request. Caching the catalog can prevent numerous calls
252
* to identity services.
254
protected static $serviceCatalogCache = [];
257
* The stream context.
259
* This is set automatically when the stream wrapper is created by
260
* PHP. Note that it is not set through a constructor.
263
protected $contextArray = [];
265
protected $schemeName = self::DEFAULT_SCHEME;
266
protected $authToken;
268
// File flags. These should probably be replaced by O_ const's at some point.
269
protected $isBinary = false;
270
protected $isText = true;
271
protected $isWriting = false;
272
protected $isReading = false;
273
protected $isTruncating = false;
274
protected $isAppending = false;
275
protected $noOverwrite = false;
276
protected $createIfNotFound = true;
279
* If this is true, no data is ever sent to the remote server.
281
protected $isNeverDirty = false;
283
protected $triggerErrors = false;
286
* Indicate whether the local differs from remote.
288
* When the file is modified in such a way that
289
* it needs to be written remotely, the isDirty flag
292
protected $isDirty = false;
295
* Object storage instance.
302
protected $container;
310
* The IO stream for the Object.
312
protected $objStream;
317
* Used for directory methods.
319
protected $dirListing = [];
320
protected $dirIndex = 0;
321
protected $dirPrefix = '';
326
* This closes a directory handle, freeing up the resources.
330
* // Assuming a valid context in $cxt...
332
* // Get the container as if it were a directory.
333
* $dir = opendir('swift://mycontainer', $cxt);
335
* // Do something with $dir
340
* NB: Some versions of PHP 5.3 don't clear all buffers when
341
* closing, and the handle can occasionally remain accessible for
342
* some period of time.
344
public function dir_closedir()
347
$this->dirListing = [];
349
//syslog(LOG_WARNING, "CLOSEDIR called.");
354
* Open a directory for reading.
358
* // Assuming a valid context in $cxt...
360
* // Get the container as if it were a directory.
361
* $dir = opendir('swift://mycontainer', $cxt);
363
* // Do something with $dir
368
* See opendir() and scandir().
370
* @param string $path The URL to open.
371
* @param int $options Unused.
373
* @return boolean true if the directory is opened, false otherwise.
375
public function dir_opendir($path, $options)
377
$url = $this->parseUrl($path);
379
if (empty($url['host'])) {
380
trigger_error('Container name is required.' , E_USER_WARNING);
386
$this->initializeObjectStorage();
387
$container = $this->store->container($url['host']);
389
if (empty($url['path'])) {
390
$this->dirPrefix = '';
392
$this->dirPrefix = $url['path'];
397
$this->dirListing = $container->objectsWithPrefix($this->dirPrefix, $sep);
398
} catch (\OpenStack\Common\Exception $e) {
399
trigger_error('Directory could not be opened: ' . $e->getMessage(), E_USER_WARNING);
408
* Read an entry from the directory.
410
* This gets a single line from the directory.
414
* // Assuming a valid context in $cxt...
416
* // Get the container as if it were a directory.
417
* $dir = opendir('swift://mycontainer', $cxt);
419
* while (($entry = readdir($dir)) !== false) {
420
* print $entry . PHP_EOL;
426
* @return string The name of the resource or false when the directory has no
429
public function dir_readdir()
431
// If we are at the end of the listing, return false.
432
if (count($this->dirListing) <= $this->dirIndex) {
436
$curr = $this->dirListing[$this->dirIndex];
439
if ($curr instanceof \OpenStack\ObjectStore\v1\Resource\Subdir) {
440
$fullpath = $curr->path();
442
$fullpath = $curr->name();
445
if (!empty($this->dirPrefix)) {
446
$len = strlen($this->dirPrefix);
447
$fullpath = substr($fullpath, $len);
454
* Rewind to the beginning of the listing.
456
* This repositions the read pointer at the first entry in the directory.
460
* // Assuming a valid context in $cxt...
462
* // Get the container as if it were a directory.
463
* $dir = opendir('swift://mycontainer', $cxt);
465
* while (($entry = readdir($dir)) !== false) {
466
* print $entry . PHP_EOL;
471
* $first = readdir($dir);
476
public function dir_rewinddir()
482
public function mkdir($path, $mode, $options)
486
public function rmdir($path, $options)
492
* Rename a swift object.
494
* This works by copying the object (metadata) and
495
* then removing the original version.
497
* This DOES support cross-container renaming.
499
* @see Container::copy().
502
* Bootstrap::setConfiguration(array(
503
* 'tenantname' => 'foo@example.com',
504
* // 'tenantid' => '1234', // You can use this instead of tenantname
505
* 'username' => 'foobar',
506
* 'password' => 'baz',
507
* 'endpoint' => 'https://auth.example.com',
510
* $from = 'swift://containerOne/file.txt';
511
* $to = 'swift://containerTwo/file.txt';
513
* // Rename can also take a context as a third param.
514
* rename($from, $to);
518
* @param string $path_from A swift URL that exists on the remote.
519
* @param string $path_to A swift URL to another path.
521
* @return boolean true on success, false otherwise.
523
public function rename($path_from, $path_to)
525
$this->initializeObjectStorage();
526
$src = $this->parseUrl($path_from);
527
$dest = $this->parseUrl($path_to);
529
if ($src['scheme'] != $dest['scheme']) {
530
trigger_error("I'm too stupid to copy across protocols.", E_USER_WARNING);
533
if ( empty($src['host']) || empty($src['path'])
534
|| empty($dest['host']) || empty($dest['path'])) {
535
trigger_error('Container and path are required for both source and destination URLs.', E_USER_WARNING);
541
$container = $this->store->container($src['host']);
543
$object = $container->proxyObject($src['path']);
545
$ret = $container->copy($object, $dest['path'], $dest['host']);
547
return $container->delete($src['path']);
549
} catch (\OpenStack\Common\Exception $e) {
550
trigger_error('Rename was not completed: ' . $e->getMessage(), E_USER_WARNING);
557
public function copy($path_from, $path_to)
559
throw new \Exception("UNDOCUMENTED.");
564
* Cast stream into a lower-level stream.
566
* This is used for stream_select() and perhaps others.Because it exposes
567
* the lower-level buffer objects, this function can have unexpected
570
* @return resource this returns the underlying stream.
572
public function stream_cast($cast_as)
574
return $this->objStream;
578
* Close a stream, writing if necessary.
582
* // Assuming $cxt has a valid context.
584
* $file = fopen('swift://container/file.txt', 'r', false, $cxt);
590
* This will close the present stream. Importantly,
591
* this will also write to the remote object storage if
592
* any changes have been made locally.
594
* @see stream_open().
596
public function stream_close()
599
$this->writeRemote();
600
} catch (\OpenStack\Common\Exception $e) {
601
trigger_error('Error while closing: ' . $e->getMessage(), E_USER_NOTICE);
606
// Force-clear the memory hogs.
609
fclose($this->objStream);
613
* Check whether the stream has reached its end.
615
* This checks whether the stream has reached the
616
* end of the object's contents.
618
* Called when `feof()` is called on a stream.
620
* @see stream_seek().
622
* @return boolean true if it has reached the end, false otherwise.
624
public function stream_eof()
626
return feof($this->objStream);
630
* Initiate saving data on the remote object storage.
632
* If the local copy of this object has been modified,
633
* it is written remotely.
635
* Called when `fflush()` is called on a stream.
637
public function stream_flush()
640
$this->writeRemote();
641
} catch (\OpenStack\Common\Exception $e) {
642
syslog(LOG_WARNING, $e);
643
trigger_error('Error while flushing: ' . $e->getMessage(), E_USER_NOTICE);
650
* Write data to the remote object storage.
652
* Internally, this is used by flush and close.
654
protected function writeRemote()
656
$contentType = $this->cxt('content_type');
657
if (!empty($contentType)) {
658
$this->obj->setContentType($contentType);
661
// Skip debug streams.
662
if ($this->isNeverDirty) {
666
// Stream is dirty and needs a write.
667
if ($this->isDirty) {
668
$position = ftell($this->objStream);
670
rewind($this->objStream);
671
$this->container->save($this->obj, $this->objStream);
673
fseek($this->objStream, SEEK_SET, $position);
676
$this->isDirty = false;
680
* Locking is currently unsupported.
682
* There is no remote support for locking a
684
public function stream_lock($operation)
690
* Open a stream resource.
692
* This opens a given stream resource and prepares it for reading or writing.
695
* $cxt = stream_context_create(array(
696
* 'username' => 'foobar',
697
* 'tenantid' => '987654321',
698
* 'password' => 'eieio',
699
* 'endpoint' => 'https://auth.example.com',
703
* $file = fopen('swift://myContainer/myObject.csv', 'rb', false, $cxt);
704
* while ($bytes = fread($file, 8192)) {
710
* If a file is opened in write mode, its contents will be retrieved from the
711
* remote storage and cached locally for manipulation. If the file is opened
712
* in a write-only mode, the contents will be created locally and then pushed
713
* remotely as necessary.
715
* During this operation, the remote host may need to be contacted for
716
* authentication as well as for file retrieval.
718
* @param string $path The URL to the resource. See the class description for
719
* details, but typically this expects URLs in the form `swift://CONTAINER/OBJECT`.
720
* @param string $mode Any of the documented mode strings. See fopen(). For
721
* any file that is in a writing mode, the file will be saved remotely on
722
* flush or close. Note that there is an extra mode: 'nope'. It acts like
723
* 'c+' except that it is never written remotely. This is useful for
724
* debugging the stream locally without sending that data to object storage.
725
* (Note that data is still fetched -- just never written.)
726
* @param int $options An OR'd list of options. Only STREAM_REPORT_ERRORS has
727
* any meaning to this wrapper, as it is not working with local files.
728
* @param string $opened_path This is not used, as this wrapper deals only
729
* with remote objects.
731
public function stream_open($path, $mode, $options, &$opened_path)
733
//syslog(LOG_WARNING, "I received this URL: " . $path);
735
// If STREAM_REPORT_ERRORS is set, we are responsible for
736
// all error handling while opening the stream.
737
if (STREAM_REPORT_ERRORS & $options) {
738
$this->triggerErrors = true;
741
// Using the mode string, set the internal mode.
742
$this->setMode($mode);
745
$url = $this->parseUrl($path);
746
//syslog(LOG_WARNING, print_r($url, true));
748
// Container name is required.
749
if (empty($url['host'])) {
750
//if ($this->triggerErrors) {
751
trigger_error('No container name was supplied in ' . $path, E_USER_WARNING);
756
// A path to an object is required.
757
if (empty($url['path'])) {
758
//if ($this->triggerErrors) {
759
trigger_error('No object name was supplied in ' . $path, E_USER_WARNING);
764
// We set this because it is possible to bind another scheme name,
765
// and we need to know that name if it's changed.
766
//$this->schemeName = isset($url['scheme']) ? $url['scheme'] : self::DEFAULT_SCHEME;
767
if (isset($url['scheme'])) {
768
$this->schemeName == $url['scheme'];
771
// Now we find out the container name. We walk a fine line here, because we don't
772
// create a new container, but we don't want to incur heavy network
773
// traffic, either. So we have to assume that we have a valid container
774
// until we issue our first request.
775
$containerName = $url['host'];
778
$objectName = $url['path'];
780
// XXX: We reserve the query string for passing additional params.
783
$this->initializeObjectStorage();
784
} catch (\OpenStack\Common\Exception $e) {
785
trigger_error('Failed to init object storage: ' . $e->getMessage(), E_USER_WARNING);
790
//syslog(LOG_WARNING, "Container: " . $containerName);
792
// Now we need to get the container. Doing a server round-trip here gives
793
// us the peace of mind that we have an actual container.
794
// XXX: Should we make it possible to get a container blindly, without the
797
$this->container = $this->store->container($containerName);
798
} catch (ResourceNotFoundException $e) {
799
trigger_error('Container not found.', E_USER_WARNING);
804
// Now we fetch the file. Only under certain circumstances do we generate
805
// an error if the file is not found.
806
// FIXME: We should probably allow a context param that can be set to
807
// mark the file as lazily fetched.
808
$this->obj = $this->container->object($objectName);
809
$stream = $this->obj->stream();
810
$streamMeta = stream_get_meta_data($stream);
812
// Support 'x' and 'x+' modes.
813
if ($this->noOverwrite) {
814
//if ($this->triggerErrors) {
815
trigger_error('File exists and cannot be overwritten.', E_USER_WARNING);
820
// If we need to write to it, we need a writable
821
// stream. Also, if we need to block reading, this
822
// will require creating an alternate stream.
823
if ($this->isWriting && ($streamMeta['mode'] == 'r' || !$this->isReading)) {
824
$newMode = $this->isReading ? 'rb+' : 'wb';
825
$tmpStream = fopen('php://temp', $newMode);
826
stream_copy_to_stream($stream, $tmpStream);
828
// Skip rewinding if we can.
829
if (!$this->isAppending) {
833
$this->objStream = $tmpStream;
835
$this->objStream = $this->obj->stream();
838
// Append mode requires seeking to the end.
839
if ($this->isAppending) {
840
fseek($this->objStream, -1, SEEK_END);
842
} catch (ResourceNotFoundException $nf) {
843
// If a 404 is thrown, we need to determine whether
844
// or not a new file should be created.
846
// For many modes, we just go ahead and create.
847
if ($this->createIfNotFound) {
848
$this->obj = new Object($objectName);
849
$this->objStream = fopen('php://temp', 'rb+');
851
$this->isDirty = true;
853
//if ($this->triggerErrors) {
854
trigger_error($nf->getMessage(), E_USER_WARNING);
859
} catch (Exception $e) {
860
// All other exceptions are fatal.
861
//if ($this->triggerErrors) {
862
trigger_error('Failed to fetch object: ' . $e->getMessage(), E_USER_WARNING);
867
// At this point, we have a file that may be read-only. It also may be
868
// reading off of a socket. It will be positioned at the beginning of
874
* Read N bytes from the stream.
876
* This will read up to the requested number of bytes. Or, upon
877
* hitting the end of the file, it will return null.
879
* @see fread(), fgets(), and so on for examples.
882
* $cxt = stream_context_create(array(
883
* 'tenantname' => 'me@example.com',
884
* 'username' => 'me@example.com',
885
* 'password' => 'secret',
886
* 'endpoint' => 'https://auth.example.com',
889
* $content = file_get_contents('swift://public/myfile.txt', false, $cxt);
892
* @param int $count The number of bytes to read (usually 8192).
894
* @return string The data read.
896
public function stream_read($count)
898
return fread($this->objStream, $count);
904
* This is called whenever `fseek()` or `rewind()` is called on a
907
* IMPORTANT: Unlike the PHP core, this library
908
* allows you to `fseek()` inside of a file opened
909
* in append mode ('a' or 'a+').
911
public function stream_seek($offset, $whence)
913
$ret = fseek($this->objStream, $offset, $whence);
915
// fseek returns 0 for success, -1 for failure.
916
// We need to return true for success, false for failure.
921
* Set options on the underlying stream.
923
* The settings here do not trickle down to the network socket, which is
924
* left open for only a brief period of time. Instead, they impact the middle
925
* buffer stream, where the file is read and written to between flush/close
926
* operations. Thus, tuning these will not have any impact on network
929
* See stream_set_blocking(), stream_set_timeout(), and stream_write_buffer().
931
public function stream_set_option($option, $arg1, $arg2)
934
case STREAM_OPTION_BLOCKING:
935
return stream_set_blocking($this->objStream, $arg1);
936
case STREAM_OPTION_READ_TIMEOUT:
937
// XXX: Should this have any effect on the lower-level
938
// socket, too? Or just the buffered tmp storage?
939
return stream_set_timeout($this->objStream, $arg1, $arg2);
940
case STREAM_OPTION_WRITE_BUFFER:
941
return stream_set_write_buffer($this->objStream, $arg2);
947
* Perform stat()/lstat() operations.
950
* $file = fopen('swift://foo/bar', 'r+', false, $cxt);
951
* $stats = fstat($file);
954
* To use standard `stat()` on a Swift stream, you will
955
* need to set account information (tenant ID, username, password,
956
* etc.) through \OpenStack\Bootstrap::setConfiguration().
958
* @return array The stats array.
960
public function stream_stat()
962
$stat = fstat($this->objStream);
964
// FIXME: Need to calculate the length of the $objStream.
965
//$contentLength = $this->obj->contentLength();
966
$contentLength = $stat['size'];
968
return $this->generateStat($this->obj, $this->container, $contentLength);
972
* Get the current position in the stream.
974
* @see `ftell()` and `fseek()`.
976
* @return int The current position in the stream.
978
public function stream_tell()
980
return ftell($this->objStream);
984
* Write data to stream.
986
* This writes data to the local stream buffer. Data
987
* is not pushed remotely until stream_close() or
988
* stream_flush() is called.
990
* @param string $data Data to write to the stream.
992
* @return int The number of bytes written. 0 indicates and error.
994
public function stream_write($data)
996
$this->isDirty = true;
998
return fwrite($this->objStream, $data);
1004
* This removes the remote copy of the file. Like a normal unlink operation,
1005
* it does not destroy the (local) file handle until the file is closed.
1006
* Therefore you can continue accessing the object locally.
1008
* Note that OpenStack Swift does not draw a distinction between file objects
1009
* and "directory" objects (where the latter is a 0-byte object). This will
1010
* delete either one. If you are using directory markers, not that deleting
1011
* a marker will NOT delete the contents of the "directory".
1013
* You will need to use \OpenStack\Bootstrap::setConfiguration() to set the
1014
* necessary stream configuration, since `unlink()` does not take a context.
1016
* @param string $path The URL.
1018
* @return boolean true if the file was deleted, false otherwise.
1020
public function unlink($path)
1022
$url = $this->parseUrl($path);
1024
// Host is required.
1025
if (empty($url['host'])) {
1026
trigger_error('Container name is required.', E_USER_WARNING);
1031
// I suppose we could allow deleting containers,
1032
// but that isn't really the purpose of the
1034
if (empty($url['path'])) {
1035
trigger_error('Path is required.', E_USER_WARNING);
1041
$this->initializeObjectStorage();
1042
// $container = $this->store->container($url['host']);
1043
$name = $url['host'];
1044
$token = $this->store->token();
1045
$endpoint_url = $this->store->url() . '/' . rawurlencode($name);
1046
$client = $this->cxt('transport_client', null);
1047
$container = new \OpenStack\ObjectStore\v1\Resource\Container($name, $endpoint_url, $token, $client);
1049
return $container->delete($url['path']);
1050
} catch (\OpenStack\Common\Exception $e) {
1051
trigger_error('Error during unlink: ' . $e->getMessage(), E_USER_WARNING);
1059
* @see stream_stat().
1061
public function url_stat($path, $flags)
1063
$url = $this->parseUrl($path);
1065
if (empty($url['host']) || empty($url['path'])) {
1066
if ($flags & STREAM_URL_STAT_QUIET) {
1067
trigger_error('Container name (host) and path are required.', E_USER_WARNING);
1074
$this->initializeObjectStorage();
1076
// Since we are throwing the $container away without really using its
1077
// internals, we create an unchecked container. It may not actually
1078
// exist on the server, which will cause a 404 error.
1079
//$container = $this->store->container($url['host']);
1080
$name = $url['host'];
1081
$token = $this->store->token();
1082
$endpoint_url = $this->store->url() . '/' . rawurlencode($name);
1083
$client = $this->cxt('transport_client', null);
1084
$container = new \OpenStack\ObjectStore\v1\Resource\Container($name, $endpoint_url, $token, $client);
1085
$obj = $container->proxyObject($url['path']);
1086
} catch (\OpenStack\Common\Exception $e) {
1087
// Apparently file_exists does not set STREAM_URL_STAT_QUIET.
1088
//if ($flags & STREAM_URL_STAT_QUIET) {
1089
//trigger_error('Could not stat remote file: ' . $e->getMessage(), E_USER_WARNING);
1094
if ($flags & STREAM_URL_STAT_QUIET) {
1096
return @$this->generateStat($obj, $container, $obj->contentLength());
1097
} catch (\OpenStack\Common\Exception $e) {
1102
return $this->generateStat($obj, $container, $obj->contentLength());
1108
* This provides low-level access to the
1109
* \OpenStack\ObjectStore\v1\ObjectStorage::Object instance in which the content
1112
* Accessing the object's payload (Object::content()) is strongly
1113
* discouraged, as it will modify the pointers in the stream that the
1114
* stream wrapper is using.
1116
* HOWEVER, accessing the Object's metadata properties, content type,
1117
* and so on is okay. Changes to this data will be written on the next
1118
* flush, provided that the file stream data has also been changed.
1123
* $handle = fopen('swift://container/test.txt', 'rb', $cxt);
1124
* $md = stream_get_meta_data($handle);
1125
* $obj = $md['wrapper_data']->object();
1128
public function object()
1134
* EXPERT: Get the ObjectStorage for this wrapper.
1136
* @return object \OpenStack\ObjectStorage An ObjectStorage object.
1139
public function objectStorage()
1141
return $this->store;
1145
* EXPERT: Get the auth token for this wrapper.
1147
* @return string A token.
1150
public function token()
1152
return $this->store->token();
1156
* EXPERT: Get the service catalog (IdentityService) for this wrapper.
1158
* This is only available when a file is opened via fopen().
1160
* @return array A service catalog.
1163
public function serviceCatalog()
1165
return self::$serviceCatalogCache[$this->token()];
1169
* Generate a reasonably accurate STAT array.
1172
* - All modes are of the (octal) form 100XXX, where
1173
* XXX is replaced by the permission string. Thus,
1174
* this always reports that the type is "file" (100).
1175
* - Currently, only two permission sets are generated:
1176
* - 770: Represents the ACL::makePrivate() perm.
1177
* - 775: Represents the ACL::makePublic() perm.
1179
* Notes on mtime/atime/ctime:
1180
* - For whatever reason, Swift only stores one timestamp.
1181
* We use that for mtime, atime, and ctime.
1184
* - Size must be calculated externally, as it will sometimes
1185
* be the remote's Content-Length, and it will sometimes be
1186
* the cached stat['size'] for the underlying buffer.
1188
protected function generateStat($object, $container, $size)
1190
// This is not entirely accurate. Basically, if the
1191
// file is marked public, it gets 100775, and if
1192
// it is private, it gets 100770.
1194
// Mode is always set to file (100XXX) because there
1195
// is no alternative that is more sensible. PHP docs
1196
// do not recommend an alternative.
1198
// octdec(100770) == 33272
1199
// octdec(100775) == 33277
1200
$mode = $container->acl()->isPublic() ? 33277 : 33272;
1202
// We have to fake the UID value in order for is_readible()/is_writable()
1203
// to work. Note that on Windows systems, stat does not look for a UID.
1204
if (function_exists('posix_geteuid')) {
1205
$uid = posix_geteuid();
1206
$gid = posix_getegid();
1212
if ($object instanceof \OpenStack\ObjectStore\v1\Resource\RemoteObject) {
1213
$modTime = $object->lastModified();
1226
'atime' => $modTime,
1227
'mtime' => $modTime,
1228
'ctime' => $modTime,
1233
$final = array_values($values) + $values;
1239
///////////////////////////////////////////////////////////////////
1241
// All methods beneath this line are not part of the Stream API.
1242
///////////////////////////////////////////////////////////////////
1245
* Set the fopen mode.
1247
* @param string $mode The mode string, e.g. `r+` or `wb`.
1249
* @return \OpenStack\ObjectStore\v1\Resource\StreamWrapper $this so the method
1250
* can be used in chaining.
1252
protected function setMode($mode)
1254
$mode = strtolower($mode);
1256
// These are largely ignored, as the remote
1257
// object storage does not distinguish between
1258
// text and binary files. Per the PHP recommendation
1259
// files are treated as binary.
1260
$this->isBinary = strpos($mode, 'b') !== false;
1261
$this->isText = strpos($mode, 't') !== false;
1263
// Rewrite mode to remove b or t:
1264
$mode = preg_replace('/[bt]?/', '', $mode);
1268
$this->isWriting = true;
1270
$this->isReading = true;
1271
$this->createIfNotFound = false;
1276
$this->isReading = true;
1278
$this->isTruncating = true;
1279
$this->isWriting = true;
1284
$this->isReading = true;
1286
$this->isAppending = true;
1287
$this->isWriting = true;
1292
$this->isReading = true;
1294
$this->isWriting = true;
1295
$this->noOverwrite = true;
1299
$this->isReading = true;
1301
$this->isWriting = true;
1304
// nope mode: Mock read/write support,
1305
// but never write to the remote server.
1306
// (This is accomplished by never marking
1307
// the stream as dirty.)
1309
$this->isReading = true;
1310
$this->isWriting = true;
1311
$this->isNeverDirty = true;
1314
// Default case is read/write
1317
$this->isReading = true;
1318
$this->isWriting = true;
1327
* Get an item out of the context.
1329
* @todo Should there be an option to NOT query the Bootstrap::conf()?
1331
* @param string $name The name to look up. First look it up in the context,
1332
* then look it up in the Bootstrap config.
1333
* @param mixed $default The default value to return if no config param was
1336
* @return mixed The discovered result, or $default if specified, or null if
1337
* no $default is specified.
1339
protected function cxt($name, $default = null)
1341
// Lazilly populate the context array.
1342
if (is_resource($this->context) && empty($this->contextArray)) {
1343
$cxt = stream_context_get_options($this->context);
1345
// If a custom scheme name has been set, use that.
1346
if (!empty($cxt[$this->schemeName])) {
1347
$this->contextArray = $cxt[$this->schemeName];
1349
// We fall back to this just in case.
1350
elseif (!empty($cxt[self::DEFAULT_SCHEME])) {
1351
$this->contextArray = $cxt[self::DEFAULT_SCHEME];
1355
// Should this be array_key_exists()?
1356
if (isset($this->contextArray[$name])) {
1357
return $this->contextArray[$name];
1360
// Check to see if the value can be gotten from
1361
// \OpenStack\Bootstrap.
1362
$val = \OpenStack\Bootstrap::config($name, null);
1373
* In order to provide full UTF-8 support, URLs must be
1374
* rawurlencoded before they are passed into the stream wrapper.
1376
* This parses the URL and urldecodes the container name and
1379
* @param string $url A Swift URL.
1381
* @return array An array as documented in parse_url().
1383
protected function parseUrl($url)
1385
$res = parse_url($url);
1388
// These have to be decode because they will later
1390
foreach ($res as $key => $val) {
1391
if ($key == 'host') {
1392
$res[$key] = urldecode($val);
1393
} elseif ($key == 'path') {
1394
if (strpos($val, '/') === 0) {
1395
$val = substr($val, 1);
1397
$res[$key] = urldecode($val);
1406
* Based on the context, initialize the ObjectStorage.
1408
* The following parameters may be set either in the stream context
1409
* or through \OpenStack\Bootstrap::setConfiguration():
1411
* - token: An auth token. If this is supplied, authentication is skipped and
1412
* this token is used. NOTE: You MUST set swift_endpoint if using this
1414
* - swift_endpoint: The URL to the swift instance. This is only necessary if
1415
* 'token' is set. Otherwise it is ignored.
1416
* - username: A username. MUST be accompanied by 'password' and 'tenantname'.
1417
* - password: A password. MUST be accompanied by 'username' and 'tenantname'.
1418
* - endpoint: The URL to the authentication endpoint. Necessary if you are not
1419
* using a 'token' and 'swift_endpoint'.
1420
* - transport_client: A transport client for the HTTP requests.
1422
* To find these params, the method first checks the supplied context. If the
1423
* key is not found there, it checks the Bootstrap::conf().
1425
protected function initializeObjectStorage()
1427
$token = $this->cxt('token');
1429
$tenantId = $this->cxt('tenantid');
1430
$tenantName = $this->cxt('tenantname');
1431
$authUrl = $this->cxt('endpoint');
1432
$endpoint = $this->cxt('swift_endpoint');
1433
$client = $this->cxt('transport_client', null);
1435
$serviceCatalog = null;
1437
if (!empty($token) && isset(self::$serviceCatalogCache[$token])) {
1438
$serviceCatalog = self::$serviceCatalogCache[$token];
1441
// FIXME: If a token is invalidated, we should try to re-authenticate.
1442
// If context has the info we need, start from there.
1443
if (!empty($token) && !empty($endpoint)) {
1444
$this->store = new \OpenStack\ObjectStore\v1\ObjectStorage($token, $endpoint, $client);
1446
// If we get here and tenant ID is not set, we can't get a container.
1447
elseif (empty($tenantId) && empty($tenantName)) {
1448
throw new \OpenStack\Common\Exception('Either Tenant ID (tenantid) or Tenant Name (tenantname) is required.');
1449
} elseif (empty($authUrl)) {
1450
throw new \OpenStack\Common\Exception('An Identity Service Endpoint (endpoint) is required.');
1452
// Try to authenticate and get a new token.
1454
$ident = $this->authenticate();
1456
// Update token and service catalog. The old pair may be out of date.
1457
$token = $ident->token();
1458
$serviceCatalog = $ident->serviceCatalog();
1459
self::$serviceCatalogCache[$token] = $serviceCatalog;
1461
$region = $this->cxt('openstack.swift.region');
1463
$this->store = ObjectStorage::newFromServiceCatalog($serviceCatalog, $token, $region, $client);
1466
return !empty($this->store);
1469
protected function authenticate()
1471
$username = $this->cxt('username');
1472
$password = $this->cxt('password');
1474
$tenantId = $this->cxt('tenantid');
1475
$tenantName = $this->cxt('tenantname');
1476
$authUrl = $this->cxt('endpoint');
1478
$client = $this->cxt('transport_client', null);
1480
$ident = new \OpenStack\Identity\v2\IdentityService($authUrl, $client);
1482
// Frustrated? Go burninate. http://www.homestarrunner.com/trogdor.html
1484
if (!empty($username) && !empty($password)) {
1485
$token = $ident->authenticateAsUser($username, $password, $tenantId, $tenantName);
1487
throw new \OpenStack\Common\Exception('Username/password must be provided.');
1489
// Cache the service catalog.
1490
self::$serviceCatalogCache[$token] = $ident->serviceCatalog();