MediaWiki  master
SwiftFileBackend.php
Go to the documentation of this file.
00001 <?php
00039 class SwiftFileBackend extends FileBackendStore {
00041         protected $auth; // Swift authentication handler
00042         protected $authTTL; // integer seconds
00043         protected $swiftTempUrlKey; // string; shared secret value for making temp urls
00044         protected $swiftAnonUser; // string; username to handle unauthenticated requests
00045         protected $swiftUseCDN; // boolean; whether CloudFiles CDN is enabled
00046         protected $swiftCDNExpiry; // integer; how long to cache things in the CDN
00047         protected $swiftCDNPurgable; // boolean; whether object CDN purging is enabled
00048 
00050         protected $conn; // Swift connection handle
00051         protected $sessionStarted = 0; // integer UNIX timestamp
00052 
00054         protected $connException;
00055         protected $connErrorTime = 0; // UNIX timestamp
00056 
00058         protected $srvCache;
00059 
00061         protected $connContainerCache; // container object cache
00062 
00091         public function __construct( array $config ) {
00092                 parent::__construct( $config );
00093                 if ( !MWInit::classExists( 'CF_Constants' ) ) {
00094                         throw new MWException( 'SwiftCloudFiles extension not installed.' );
00095                 }
00096                 // Required settings
00097                 $this->auth = new CF_Authentication(
00098                         $config['swiftUser'],
00099                         $config['swiftKey'],
00100                         null, // account; unused
00101                         $config['swiftAuthUrl']
00102                 );
00103                 // Optional settings
00104                 $this->authTTL = isset( $config['swiftAuthTTL'] )
00105                         ? $config['swiftAuthTTL']
00106                         : 5 * 60; // some sane number
00107                 $this->swiftAnonUser = isset( $config['swiftAnonUser'] )
00108                         ? $config['swiftAnonUser']
00109                         : '';
00110                 $this->swiftTempUrlKey = isset( $config['swiftTempUrlKey'] )
00111                         ? $config['swiftTempUrlKey']
00112                         : '';
00113                 $this->shardViaHashLevels = isset( $config['shardViaHashLevels'] )
00114                         ? $config['shardViaHashLevels']
00115                         : '';
00116                 $this->swiftUseCDN = isset( $config['swiftUseCDN'] )
00117                         ? $config['swiftUseCDN']
00118                         : false;
00119                 $this->swiftCDNExpiry = isset( $config['swiftCDNExpiry'] )
00120                         ? $config['swiftCDNExpiry']
00121                         : 12*3600; // 12 hours is safe (tokens last 24 hours per http://docs.openstack.org)
00122                 $this->swiftCDNPurgable = isset( $config['swiftCDNPurgable'] )
00123                         ? $config['swiftCDNPurgable']
00124                         : true;
00125                 // Cache container information to mask latency
00126                 $this->memCache = wfGetMainCache();
00127                 // Process cache for container info
00128                 $this->connContainerCache = new ProcessCacheLRU( 300 );
00129                 // Cache auth token information to avoid RTTs
00130                 if ( !empty( $config['cacheAuthInfo'] ) ) {
00131                         if ( php_sapi_name() === 'cli' ) {
00132                                 $this->srvCache = wfGetMainCache(); // preferrably memcached
00133                         } else {
00134                                 try { // look for APC, XCache, WinCache, ect...
00135                                         $this->srvCache = ObjectCache::newAccelerator( array() );
00136                                 } catch ( Exception $e ) {}
00137                         }
00138                 }
00139                 $this->srvCache = $this->srvCache ? $this->srvCache : new EmptyBagOStuff();
00140         }
00141 
00146         protected function resolveContainerPath( $container, $relStoragePath ) {
00147                 if ( !mb_check_encoding( $relStoragePath, 'UTF-8' ) ) { // mb_string required by CF
00148                         return null; // not UTF-8, makes it hard to use CF and the swift HTTP API
00149                 } elseif ( strlen( urlencode( $relStoragePath ) ) > 1024 ) {
00150                         return null; // too long for Swift
00151                 }
00152                 return $relStoragePath;
00153         }
00154 
00159         public function isPathUsableInternal( $storagePath ) {
00160                 list( $container, $rel ) = $this->resolveStoragePathReal( $storagePath );
00161                 if ( $rel === null ) {
00162                         return false; // invalid
00163                 }
00164 
00165                 try {
00166                         $this->getContainer( $container );
00167                         return true; // container exists
00168                 } catch ( NoSuchContainerException $e ) {
00169                 } catch ( CloudFilesException $e ) { // some other exception?
00170                         $this->handleException( $e, null, __METHOD__, array( 'path' => $storagePath ) );
00171                 }
00172 
00173                 return false;
00174         }
00175 
00180         protected function truncDisp( $disposition ) {
00181                 $res = '';
00182                 foreach ( explode( ';', $disposition ) as $part ) {
00183                         $part = trim( $part );
00184                         $new  = ( $res === '' ) ? $part : "{$res};{$part}";
00185                         if ( strlen( $new ) <= 255 ) {
00186                                 $res = $new;
00187                         } else {
00188                                 break; // too long; sigh
00189                         }
00190                 }
00191                 return $res;
00192         }
00193 
00198         protected function doCreateInternal( array $params ) {
00199                 $status = Status::newGood();
00200 
00201                 list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
00202                 if ( $dstRel === null ) {
00203                         $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
00204                         return $status;
00205                 }
00206 
00207                 // (a) Check the destination container and object
00208                 try {
00209                         $dContObj = $this->getContainer( $dstCont );
00210                 } catch ( NoSuchContainerException $e ) {
00211                         $status->fatal( 'backend-fail-create', $params['dst'] );
00212                         return $status;
00213                 } catch ( CloudFilesException $e ) { // some other exception?
00214                         $this->handleException( $e, $status, __METHOD__, $params );
00215                         return $status;
00216                 }
00217 
00218                 // (b) Get a SHA-1 hash of the object
00219                 $sha1Hash = wfBaseConvert( sha1( $params['content'] ), 16, 36, 31 );
00220 
00221                 // (c) Actually create the object
00222                 try {
00223                         // Create a fresh CF_Object with no fields preloaded.
00224                         // We don't want to preserve headers, metadata, and such.
00225                         $obj = new CF_Object( $dContObj, $dstRel, false, false ); // skip HEAD
00226                         $obj->setMetadataValues( array( 'Sha1base36' => $sha1Hash ) );
00227                         // Manually set the ETag (https://github.com/rackspace/php-cloudfiles/issues/59).
00228                         // The MD5 here will be checked within Swift against its own MD5.
00229                         $obj->set_etag( md5( $params['content'] ) );
00230                         // Use the same content type as StreamFile for security
00231                         $obj->content_type = StreamFile::contentTypeFromPath( $params['dst'] );
00232                         if ( !strlen( $obj->content_type ) ) { // special case
00233                                 $obj->content_type = 'unknown/unknown';
00234                         }
00235                         // Set the Content-Disposition header if requested
00236                         if ( isset( $params['disposition'] ) ) {
00237                                 $obj->headers['Content-Disposition'] = $this->truncDisp( $params['disposition'] );
00238                         }
00239                         if ( !empty( $params['async'] ) ) { // deferred
00240                                 $op = $obj->write_async( $params['content'] );
00241                                 $status->value = new SwiftFileOpHandle( $this, $params, 'Create', $op );
00242                                 $status->value->affectedObjects[] = $obj;
00243                         } else { // actually write the object in Swift
00244                                 $obj->write( $params['content'] );
00245                                 $this->purgeCDNCache( array( $obj ) );
00246                         }
00247                 } catch ( CDNNotEnabledException $e ) {
00248                         // CDN not enabled; nothing to see here
00249                 } catch ( BadContentTypeException $e ) {
00250                         $status->fatal( 'backend-fail-contenttype', $params['dst'] );
00251                 } catch ( CloudFilesException $e ) { // some other exception?
00252                         $this->handleException( $e, $status, __METHOD__, $params );
00253                 }
00254 
00255                 return $status;
00256         }
00257 
00261         protected function _getResponseCreate( CF_Async_Op $cfOp, Status $status, array $params ) {
00262                 try {
00263                         $cfOp->getLastResponse();
00264                 } catch ( BadContentTypeException $e ) {
00265                         $status->fatal( 'backend-fail-contenttype', $params['dst'] );
00266                 }
00267         }
00268 
00273         protected function doStoreInternal( array $params ) {
00274                 $status = Status::newGood();
00275 
00276                 list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
00277                 if ( $dstRel === null ) {
00278                         $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
00279                         return $status;
00280                 }
00281 
00282                 // (a) Check the destination container and object
00283                 try {
00284                         $dContObj = $this->getContainer( $dstCont );
00285                 } catch ( NoSuchContainerException $e ) {
00286                         $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
00287                         return $status;
00288                 } catch ( CloudFilesException $e ) { // some other exception?
00289                         $this->handleException( $e, $status, __METHOD__, $params );
00290                         return $status;
00291                 }
00292 
00293                 // (b) Get a SHA-1 hash of the object
00294                 $sha1Hash = sha1_file( $params['src'] );
00295                 if ( $sha1Hash === false ) { // source doesn't exist?
00296                         $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
00297                         return $status;
00298                 }
00299                 $sha1Hash = wfBaseConvert( $sha1Hash, 16, 36, 31 );
00300 
00301                 // (c) Actually store the object
00302                 try {
00303                         // Create a fresh CF_Object with no fields preloaded.
00304                         // We don't want to preserve headers, metadata, and such.
00305                         $obj = new CF_Object( $dContObj, $dstRel, false, false ); // skip HEAD
00306                         $obj->setMetadataValues( array( 'Sha1base36' => $sha1Hash ) );
00307                         // The MD5 here will be checked within Swift against its own MD5.
00308                         $obj->set_etag( md5_file( $params['src'] ) );
00309                         // Use the same content type as StreamFile for security
00310                         $obj->content_type = StreamFile::contentTypeFromPath( $params['dst'] );
00311                         if ( !strlen( $obj->content_type ) ) { // special case
00312                                 $obj->content_type = 'unknown/unknown';
00313                         }
00314                         // Set the Content-Disposition header if requested
00315                         if ( isset( $params['disposition'] ) ) {
00316                                 $obj->headers['Content-Disposition'] = $this->truncDisp( $params['disposition'] );
00317                         }
00318                         if ( !empty( $params['async'] ) ) { // deferred
00319                                 wfSuppressWarnings();
00320                                 $fp = fopen( $params['src'], 'rb' );
00321                                 wfRestoreWarnings();
00322                                 if ( !$fp ) {
00323                                         $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
00324                                 } else {
00325                                         $op = $obj->write_async( $fp, filesize( $params['src'] ), true );
00326                                         $status->value = new SwiftFileOpHandle( $this, $params, 'Store', $op );
00327                                         $status->value->resourcesToClose[] = $fp;
00328                                         $status->value->affectedObjects[] = $obj;
00329                                 }
00330                         } else { // actually write the object in Swift
00331                                 $obj->load_from_filename( $params['src'], true ); // calls $obj->write()
00332                                 $this->purgeCDNCache( array( $obj ) );
00333                         }
00334                 } catch ( CDNNotEnabledException $e ) {
00335                         // CDN not enabled; nothing to see here
00336                 } catch ( BadContentTypeException $e ) {
00337                         $status->fatal( 'backend-fail-contenttype', $params['dst'] );
00338                 } catch ( IOException $e ) {
00339                         $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
00340                 } catch ( CloudFilesException $e ) { // some other exception?
00341                         $this->handleException( $e, $status, __METHOD__, $params );
00342                 }
00343 
00344                 return $status;
00345         }
00346 
00350         protected function _getResponseStore( CF_Async_Op $cfOp, Status $status, array $params ) {
00351                 try {
00352                         $cfOp->getLastResponse();
00353                 } catch ( BadContentTypeException $e ) {
00354                         $status->fatal( 'backend-fail-contenttype', $params['dst'] );
00355                 } catch ( IOException $e ) {
00356                         $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
00357                 }
00358         }
00359 
00364         protected function doCopyInternal( array $params ) {
00365                 $status = Status::newGood();
00366 
00367                 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
00368                 if ( $srcRel === null ) {
00369                         $status->fatal( 'backend-fail-invalidpath', $params['src'] );
00370                         return $status;
00371                 }
00372 
00373                 list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
00374                 if ( $dstRel === null ) {
00375                         $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
00376                         return $status;
00377                 }
00378 
00379                 // (a) Check the source/destination containers and destination object
00380                 try {
00381                         $sContObj = $this->getContainer( $srcCont );
00382                         $dContObj = $this->getContainer( $dstCont );
00383                 } catch ( NoSuchContainerException $e ) {
00384                         $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
00385                         return $status;
00386                 } catch ( CloudFilesException $e ) { // some other exception?
00387                         $this->handleException( $e, $status, __METHOD__, $params );
00388                         return $status;
00389                 }
00390 
00391                 // (b) Actually copy the file to the destination
00392                 try {
00393                         $dstObj = new CF_Object( $dContObj, $dstRel, false, false ); // skip HEAD
00394                         $hdrs = array(); // source file headers to override with new values
00395                         if ( isset( $params['disposition'] ) ) {
00396                                 $hdrs['Content-Disposition'] = $this->truncDisp( $params['disposition'] );
00397                         }
00398                         if ( !empty( $params['async'] ) ) { // deferred
00399                                 $op = $sContObj->copy_object_to_async( $srcRel, $dContObj, $dstRel, null, $hdrs );
00400                                 $status->value = new SwiftFileOpHandle( $this, $params, 'Copy', $op );
00401                                 $status->value->affectedObjects[] = $dstObj;
00402                         } else { // actually write the object in Swift
00403                                 $sContObj->copy_object_to( $srcRel, $dContObj, $dstRel, null, $hdrs );
00404                                 $this->purgeCDNCache( array( $dstObj ) );
00405                         }
00406                 } catch ( CDNNotEnabledException $e ) {
00407                         // CDN not enabled; nothing to see here
00408                 } catch ( NoSuchObjectException $e ) { // source object does not exist
00409                         if ( empty( $params['ignoreMissingSource'] ) ) {
00410                                 $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
00411                         }
00412                 } catch ( CloudFilesException $e ) { // some other exception?
00413                         $this->handleException( $e, $status, __METHOD__, $params );
00414                 }
00415 
00416                 return $status;
00417         }
00418 
00422         protected function _getResponseCopy( CF_Async_Op $cfOp, Status $status, array $params ) {
00423                 try {
00424                         $cfOp->getLastResponse();
00425                 } catch ( NoSuchObjectException $e ) { // source object does not exist
00426                         $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
00427                 }
00428         }
00429 
00434         protected function doMoveInternal( array $params ) {
00435                 $status = Status::newGood();
00436 
00437                 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
00438                 if ( $srcRel === null ) {
00439                         $status->fatal( 'backend-fail-invalidpath', $params['src'] );
00440                         return $status;
00441                 }
00442 
00443                 list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
00444                 if ( $dstRel === null ) {
00445                         $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
00446                         return $status;
00447                 }
00448 
00449                 // (a) Check the source/destination containers and destination object
00450                 try {
00451                         $sContObj = $this->getContainer( $srcCont );
00452                         $dContObj = $this->getContainer( $dstCont );
00453                 } catch ( NoSuchContainerException $e ) {
00454                         $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
00455                         return $status;
00456                 } catch ( CloudFilesException $e ) { // some other exception?
00457                         $this->handleException( $e, $status, __METHOD__, $params );
00458                         return $status;
00459                 }
00460 
00461                 // (b) Actually move the file to the destination
00462                 try {
00463                         $srcObj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD
00464                         $dstObj = new CF_Object( $dContObj, $dstRel, false, false ); // skip HEAD
00465                         $hdrs = array(); // source file headers to override with new values
00466                         if ( isset( $params['disposition'] ) ) {
00467                                 $hdrs['Content-Disposition'] = $this->truncDisp( $params['disposition'] );
00468                         }
00469                         if ( !empty( $params['async'] ) ) { // deferred
00470                                 $op = $sContObj->move_object_to_async( $srcRel, $dContObj, $dstRel, null, $hdrs );
00471                                 $status->value = new SwiftFileOpHandle( $this, $params, 'Move', $op );
00472                                 $status->value->affectedObjects[] = $srcObj;
00473                                 $status->value->affectedObjects[] = $dstObj;
00474                         } else { // actually write the object in Swift
00475                                 $sContObj->move_object_to( $srcRel, $dContObj, $dstRel, null, $hdrs );
00476                                 $this->purgeCDNCache( array( $srcObj ) );
00477                                 $this->purgeCDNCache( array( $dstObj ) );
00478                         }
00479                 } catch ( CDNNotEnabledException $e ) {
00480                         // CDN not enabled; nothing to see here
00481                 } catch ( NoSuchObjectException $e ) { // source object does not exist
00482                         if ( empty( $params['ignoreMissingSource'] ) ) {
00483                                 $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
00484                         }
00485                 } catch ( CloudFilesException $e ) { // some other exception?
00486                         $this->handleException( $e, $status, __METHOD__, $params );
00487                 }
00488 
00489                 return $status;
00490         }
00491 
00495         protected function _getResponseMove( CF_Async_Op $cfOp, Status $status, array $params ) {
00496                 try {
00497                         $cfOp->getLastResponse();
00498                 } catch ( NoSuchObjectException $e ) { // source object does not exist
00499                         $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
00500                 }
00501         }
00502 
00507         protected function doDeleteInternal( array $params ) {
00508                 $status = Status::newGood();
00509 
00510                 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
00511                 if ( $srcRel === null ) {
00512                         $status->fatal( 'backend-fail-invalidpath', $params['src'] );
00513                         return $status;
00514                 }
00515 
00516                 try {
00517                         $sContObj = $this->getContainer( $srcCont );
00518                         $srcObj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD
00519                         if ( !empty( $params['async'] ) ) { // deferred
00520                                 $op = $sContObj->delete_object_async( $srcRel );
00521                                 $status->value = new SwiftFileOpHandle( $this, $params, 'Delete', $op );
00522                                 $status->value->affectedObjects[] = $srcObj;
00523                         } else { // actually write the object in Swift
00524                                 $sContObj->delete_object( $srcRel );
00525                                 $this->purgeCDNCache( array( $srcObj ) );
00526                         }
00527                 } catch ( CDNNotEnabledException $e ) {
00528                         // CDN not enabled; nothing to see here
00529                 } catch ( NoSuchContainerException $e ) {
00530                         $status->fatal( 'backend-fail-delete', $params['src'] );
00531                 } catch ( NoSuchObjectException $e ) {
00532                         if ( empty( $params['ignoreMissingSource'] ) ) {
00533                                 $status->fatal( 'backend-fail-delete', $params['src'] );
00534                         }
00535                 } catch ( CloudFilesException $e ) { // some other exception?
00536                         $this->handleException( $e, $status, __METHOD__, $params );
00537                 }
00538 
00539                 return $status;
00540         }
00541 
00545         protected function _getResponseDelete( CF_Async_Op $cfOp, Status $status, array $params ) {
00546                 try {
00547                         $cfOp->getLastResponse();
00548                 } catch ( NoSuchContainerException $e ) {
00549                         $status->fatal( 'backend-fail-delete', $params['src'] );
00550                 } catch ( NoSuchObjectException $e ) {
00551                         if ( empty( $params['ignoreMissingSource'] ) ) {
00552                                 $status->fatal( 'backend-fail-delete', $params['src'] );
00553                         }
00554                 }
00555         }
00556 
00561         protected function doPrepareInternal( $fullCont, $dir, array $params ) {
00562                 $status = Status::newGood();
00563 
00564                 // (a) Check if container already exists
00565                 try {
00566                         $contObj = $this->getContainer( $fullCont );
00567                         // NoSuchContainerException not thrown: container must exist
00568                         return $status; // already exists
00569                 } catch ( NoSuchContainerException $e ) {
00570                         // NoSuchContainerException thrown: container does not exist
00571                 } catch ( CloudFilesException $e ) { // some other exception?
00572                         $this->handleException( $e, $status, __METHOD__, $params );
00573                         return $status;
00574                 }
00575 
00576                 // (b) Create container as needed
00577                 try {
00578                         $contObj = $this->createContainer( $fullCont );
00579                         if ( !empty( $params['noAccess'] ) ) {
00580                                 // Make container private to end-users...
00581                                 $status->merge( $this->doSecureInternal( $fullCont, $dir, $params ) );
00582                         } else {
00583                                 // Make container public to end-users...
00584                                 $status->merge( $this->doPublishInternal( $fullCont, $dir, $params ) );
00585                         }
00586                         if ( $this->swiftUseCDN ) { // Rackspace style CDN
00587                                 $contObj->make_public( $this->swiftCDNExpiry );
00588                         }
00589                 } catch ( CDNNotEnabledException $e ) {
00590                         // CDN not enabled; nothing to see here
00591                 } catch ( CloudFilesException $e ) { // some other exception?
00592                         $this->handleException( $e, $status, __METHOD__, $params );
00593                         return $status;
00594                 }
00595 
00596                 return $status;
00597         }
00598 
00603         protected function doSecureInternal( $fullCont, $dir, array $params ) {
00604                 $status = Status::newGood();
00605                 if ( empty( $params['noAccess'] ) ) {
00606                         return $status; // nothing to do
00607                 }
00608 
00609                 // Restrict container from end-users...
00610                 try {
00611                         // doPrepareInternal() should have been called,
00612                         // so the Swift container should already exist...
00613                         $contObj = $this->getContainer( $fullCont ); // normally a cache hit
00614                         // NoSuchContainerException not thrown: container must exist
00615 
00616                         // Make container private to end-users...
00617                         $status->merge( $this->setContainerAccess(
00618                                 $contObj,
00619                                 array( $this->auth->username ), // read
00620                                 array( $this->auth->username ) // write
00621                         ) );
00622                         if ( $this->swiftUseCDN && $contObj->is_public() ) { // Rackspace style CDN
00623                                 $contObj->make_private();
00624                         }
00625                 } catch ( CDNNotEnabledException $e ) {
00626                         // CDN not enabled; nothing to see here
00627                 } catch ( CloudFilesException $e ) { // some other exception?
00628                         $this->handleException( $e, $status, __METHOD__, $params );
00629                 }
00630 
00631                 return $status;
00632         }
00633 
00638         protected function doPublishInternal( $fullCont, $dir, array $params ) {
00639                 $status = Status::newGood();
00640 
00641                 // Unrestrict container from end-users...
00642                 try {
00643                         // doPrepareInternal() should have been called,
00644                         // so the Swift container should already exist...
00645                         $contObj = $this->getContainer( $fullCont ); // normally a cache hit
00646                         // NoSuchContainerException not thrown: container must exist
00647 
00648                         // Make container public to end-users...
00649                         if ( $this->swiftAnonUser != '' ) {
00650                                 $status->merge( $this->setContainerAccess(
00651                                         $contObj,
00652                                         array( $this->auth->username, $this->swiftAnonUser ), // read
00653                                         array( $this->auth->username, $this->swiftAnonUser ) // write
00654                                 ) );
00655                         } else {
00656                                 $status->merge( $this->setContainerAccess(
00657                                         $contObj,
00658                                         array( $this->auth->username, '.r:*' ), // read
00659                                         array( $this->auth->username ) // write
00660                                 ) );
00661                         }
00662                         if ( $this->swiftUseCDN && !$contObj->is_public() ) { // Rackspace style CDN
00663                                 $contObj->make_public();
00664                         }
00665                 } catch ( CDNNotEnabledException $e ) {
00666                         // CDN not enabled; nothing to see here
00667                 } catch ( CloudFilesException $e ) { // some other exception?
00668                         $this->handleException( $e, $status, __METHOD__, $params );
00669                 }
00670 
00671                 return $status;
00672         }
00673 
00678         protected function doCleanInternal( $fullCont, $dir, array $params ) {
00679                 $status = Status::newGood();
00680 
00681                 // Only containers themselves can be removed, all else is virtual
00682                 if ( $dir != '' ) {
00683                         return $status; // nothing to do
00684                 }
00685 
00686                 // (a) Check the container
00687                 try {
00688                         $contObj = $this->getContainer( $fullCont, true );
00689                 } catch ( NoSuchContainerException $e ) {
00690                         return $status; // ok, nothing to do
00691                 } catch ( CloudFilesException $e ) { // some other exception?
00692                         $this->handleException( $e, $status, __METHOD__, $params );
00693                         return $status;
00694                 }
00695 
00696                 // (b) Delete the container if empty
00697                 if ( $contObj->object_count == 0 ) {
00698                         try {
00699                                 $this->deleteContainer( $fullCont );
00700                         } catch ( NoSuchContainerException $e ) {
00701                                 return $status; // race?
00702                         } catch ( NonEmptyContainerException $e ) {
00703                                 return $status; // race? consistency delay?
00704                         } catch ( CloudFilesException $e ) { // some other exception?
00705                                 $this->handleException( $e, $status, __METHOD__, $params );
00706                                 return $status;
00707                         }
00708                 }
00709 
00710                 return $status;
00711         }
00712 
00717         protected function doGetFileStat( array $params ) {
00718                 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
00719                 if ( $srcRel === null ) {
00720                         return false; // invalid storage path
00721                 }
00722 
00723                 $stat = false;
00724                 try {
00725                         $contObj = $this->getContainer( $srcCont );
00726                         $srcObj = $contObj->get_object( $srcRel, $this->headersFromParams( $params ) );
00727                         $this->addMissingMetadata( $srcObj, $params['src'] );
00728                         $stat = array(
00729                                 // Convert dates like "Tue, 03 Jan 2012 22:01:04 GMT" to TS_MW
00730                                 'mtime' => wfTimestamp( TS_MW, $srcObj->last_modified ),
00731                                 'size'  => (int)$srcObj->content_length,
00732                                 'sha1'  => $srcObj->getMetadataValue( 'Sha1base36' )
00733                         );
00734                 } catch ( NoSuchContainerException $e ) {
00735                 } catch ( NoSuchObjectException $e ) {
00736                 } catch ( CloudFilesException $e ) { // some other exception?
00737                         $stat = null;
00738                         $this->handleException( $e, null, __METHOD__, $params );
00739                 }
00740 
00741                 return $stat;
00742         }
00743 
00752         protected function addMissingMetadata( CF_Object $obj, $path ) {
00753                 if ( $obj->getMetadataValue( 'Sha1base36' ) !== null ) {
00754                         return true; // nothing to do
00755                 }
00756                 wfProfileIn( __METHOD__ );
00757                 trigger_error( "$path was not stored with SHA-1 metadata.", E_USER_WARNING );
00758                 $status = Status::newGood();
00759                 $scopeLockS = $this->getScopedFileLocks( array( $path ), LockManager::LOCK_UW, $status );
00760                 if ( $status->isOK() ) {
00761                         $tmpFile = $this->getLocalCopy( array( 'src' => $path, 'latest' => 1 ) );
00762                         if ( $tmpFile ) {
00763                                 $hash = $tmpFile->getSha1Base36();
00764                                 if ( $hash !== false ) {
00765                                         $obj->setMetadataValues( array( 'Sha1base36' => $hash ) );
00766                                         $obj->sync_metadata(); // save to Swift
00767                                         wfProfileOut( __METHOD__ );
00768                                         return true; // success
00769                                 }
00770                         }
00771                 }
00772                 $obj->setMetadataValues( array( 'Sha1base36' => false ) );
00773                 wfProfileOut( __METHOD__ );
00774                 return false; // failed
00775         }
00776 
00781         protected function doGetFileContentsMulti( array $params ) {
00782                 $contents = array();
00783 
00784                 $ep = array_diff_key( $params, array( 'srcs' => 1 ) ); // for error logging
00785                 // Blindly create tmp files and stream to them, catching any exception if the file does
00786                 // not exist. Doing stats here is useless and will loop infinitely in addMissingMetadata().
00787                 foreach ( array_chunk( $params['srcs'], $params['concurrency'] ) as $pathBatch ) {
00788                         $cfOps = array(); // (path => CF_Async_Op)
00789 
00790                         foreach ( $pathBatch as $path ) { // each path in this concurrent batch
00791                                 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
00792                                 if ( $srcRel === null ) {
00793                                         $contents[$path] = false;
00794                                         continue;
00795                                 }
00796                                 $data = false;
00797                                 try {
00798                                         $sContObj = $this->getContainer( $srcCont );
00799                                         $obj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD
00800                                         // Get source file extension
00801                                         $ext = FileBackend::extensionFromPath( $path );
00802                                         // Create a new temporary memory file...
00803                                         $handle = fopen( 'php://temp', 'wb' );
00804                                         if ( $handle ) {
00805                                                 $headers = $this->headersFromParams( $params );
00806                                                 if ( count( $pathBatch ) > 1 ) {
00807                                                         $cfOps[$path] = $obj->stream_async( $handle, $headers );
00808                                                         $cfOps[$path]->_file_handle = $handle; // close this later
00809                                                 } else {
00810                                                         $obj->stream( $handle, $headers );
00811                                                         rewind( $handle ); // start from the beginning
00812                                                         $data = stream_get_contents( $handle );
00813                                                         fclose( $handle );
00814                                                 }
00815                                         } else {
00816                                                 $data = false;
00817                                         }
00818                                 } catch ( NoSuchContainerException $e ) {
00819                                         $data = false;
00820                                 } catch ( NoSuchObjectException $e ) {
00821                                         $data = false;
00822                                 } catch ( CloudFilesException $e ) { // some other exception?
00823                                         $data = false;
00824                                         $this->handleException( $e, null, __METHOD__, array( 'src' => $path ) + $ep );
00825                                 }
00826                                 $contents[$path] = $data;
00827                         }
00828 
00829                         $batch = new CF_Async_Op_Batch( $cfOps );
00830                         $cfOps = $batch->execute();
00831                         foreach ( $cfOps as $path => $cfOp ) {
00832                                 try {
00833                                         $cfOp->getLastResponse();
00834                                         rewind( $cfOp->_file_handle ); // start from the beginning
00835                                         $contents[$path] = stream_get_contents( $cfOp->_file_handle );
00836                                 } catch ( NoSuchContainerException $e ) {
00837                                         $contents[$path] = false;
00838                                 } catch ( NoSuchObjectException $e ) {
00839                                         $contents[$path] = false;
00840                                 } catch ( CloudFilesException $e ) { // some other exception?
00841                                         $contents[$path] = false;
00842                                         $this->handleException( $e, null, __METHOD__, array( 'src' => $path ) + $ep );
00843                                 }
00844                                 fclose( $cfOp->_file_handle ); // close open handle
00845                         }
00846                 }
00847 
00848                 return $contents;
00849         }
00850 
00855         protected function doDirectoryExists( $fullCont, $dir, array $params ) {
00856                 try {
00857                         $container = $this->getContainer( $fullCont );
00858                         $prefix = ( $dir == '' ) ? null : "{$dir}/";
00859                         return ( count( $container->list_objects( 1, null, $prefix ) ) > 0 );
00860                 } catch ( NoSuchContainerException $e ) {
00861                         return false;
00862                 } catch ( CloudFilesException $e ) { // some other exception?
00863                         $this->handleException( $e, null, __METHOD__,
00864                                 array( 'cont' => $fullCont, 'dir' => $dir ) );
00865                 }
00866 
00867                 return null; // error
00868         }
00869 
00874         public function getDirectoryListInternal( $fullCont, $dir, array $params ) {
00875                 return new SwiftFileBackendDirList( $this, $fullCont, $dir, $params );
00876         }
00877 
00882         public function getFileListInternal( $fullCont, $dir, array $params ) {
00883                 return new SwiftFileBackendFileList( $this, $fullCont, $dir, $params );
00884         }
00885 
00896         public function getDirListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
00897                 $dirs = array();
00898                 if ( $after === INF ) {
00899                         return $dirs; // nothing more
00900                 }
00901                 wfProfileIn( __METHOD__ . '-' . $this->name );
00902 
00903                 try {
00904                         $container = $this->getContainer( $fullCont );
00905                         $prefix = ( $dir == '' ) ? null : "{$dir}/";
00906                         // Non-recursive: only list dirs right under $dir
00907                         if ( !empty( $params['topOnly'] ) ) {
00908                                 $objects = $container->list_objects( $limit, $after, $prefix, null, '/' );
00909                                 foreach ( $objects as $object ) { // files and dirs
00910                                         if ( substr( $object, -1 ) === '/' ) {
00911                                                 $dirs[] = $object; // directories end in '/'
00912                                         }
00913                                 }
00914                         // Recursive: list all dirs under $dir and its subdirs
00915                         } else {
00916                                 // Get directory from last item of prior page
00917                                 $lastDir = $this->getParentDir( $after ); // must be first page
00918                                 $objects = $container->list_objects( $limit, $after, $prefix );
00919                                 foreach ( $objects as $object ) { // files
00920                                         $objectDir = $this->getParentDir( $object ); // directory of object
00921                                         if ( $objectDir !== false ) { // file has a parent dir
00922                                                 // Swift stores paths in UTF-8, using binary sorting.
00923                                                 // See function "create_container_table" in common/db.py.
00924                                                 // If a directory is not "greater" than the last one,
00925                                                 // then it was already listed by the calling iterator.
00926                                                 if ( strcmp( $objectDir, $lastDir ) > 0 ) {
00927                                                         $pDir = $objectDir;
00928                                                         do { // add dir and all its parent dirs
00929                                                                 $dirs[] = "{$pDir}/";
00930                                                                 $pDir = $this->getParentDir( $pDir );
00931                                                         } while ( $pDir !== false // sanity
00932                                                                 && strcmp( $pDir, $lastDir ) > 0 // not done already
00933                                                                 && strlen( $pDir ) > strlen( $dir ) // within $dir
00934                                                         );
00935                                                 }
00936                                                 $lastDir = $objectDir;
00937                                         }
00938                                 }
00939                         }
00940                         if ( count( $objects ) < $limit ) {
00941                                 $after = INF; // avoid a second RTT
00942                         } else {
00943                                 $after = end( $objects ); // update last item
00944                         }
00945                 } catch ( NoSuchContainerException $e ) {
00946                 } catch ( CloudFilesException $e ) { // some other exception?
00947                         $this->handleException( $e, null, __METHOD__,
00948                                 array( 'cont' => $fullCont, 'dir' => $dir ) );
00949                 }
00950 
00951                 wfProfileOut( __METHOD__ . '-' . $this->name );
00952                 return $dirs;
00953         }
00954 
00955         protected function getParentDir( $path ) {
00956                 return ( strpos( $path, '/' ) !== false ) ? dirname( $path ) : false;
00957         }
00958 
00969         public function getFileListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
00970                 $files = array();
00971                 if ( $after === INF ) {
00972                         return $files; // nothing more
00973                 }
00974                 wfProfileIn( __METHOD__ . '-' . $this->name );
00975 
00976                 try {
00977                         $container = $this->getContainer( $fullCont );
00978                         $prefix = ( $dir == '' ) ? null : "{$dir}/";
00979                         // Non-recursive: only list files right under $dir
00980                         if ( !empty( $params['topOnly'] ) ) { // files and dirs
00981                                 $objects = $container->list_objects( $limit, $after, $prefix, null, '/' );
00982                                 foreach ( $objects as $object ) {
00983                                         if ( substr( $object, -1 ) !== '/' ) {
00984                                                 $files[] = $object; // directories end in '/'
00985                                         }
00986                                 }
00987                         // Recursive: list all files under $dir and its subdirs
00988                         } else { // files
00989                                 $objects = $container->list_objects( $limit, $after, $prefix );
00990                                 $files = $objects;
00991                         }
00992                         if ( count( $objects ) < $limit ) {
00993                                 $after = INF; // avoid a second RTT
00994                         } else {
00995                                 $after = end( $objects ); // update last item
00996                         }
00997                 } catch ( NoSuchContainerException $e ) {
00998                 } catch ( CloudFilesException $e ) { // some other exception?
00999                         $this->handleException( $e, null, __METHOD__,
01000                                 array( 'cont' => $fullCont, 'dir' => $dir ) );
01001                 }
01002 
01003                 wfProfileOut( __METHOD__ . '-' . $this->name );
01004                 return $files;
01005         }
01006 
01011         protected function doGetFileSha1base36( array $params ) {
01012                 $stat = $this->getFileStat( $params );
01013                 if ( $stat ) {
01014                         return $stat['sha1'];
01015                 } else {
01016                         return false;
01017                 }
01018         }
01019 
01024         protected function doStreamFile( array $params ) {
01025                 $status = Status::newGood();
01026 
01027                 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
01028                 if ( $srcRel === null ) {
01029                         $status->fatal( 'backend-fail-invalidpath', $params['src'] );
01030                 }
01031 
01032                 try {
01033                         $cont = $this->getContainer( $srcCont );
01034                 } catch ( NoSuchContainerException $e ) {
01035                         $status->fatal( 'backend-fail-stream', $params['src'] );
01036                         return $status;
01037                 } catch ( CloudFilesException $e ) { // some other exception?
01038                         $this->handleException( $e, $status, __METHOD__, $params );
01039                         return $status;
01040                 }
01041 
01042                 try {
01043                         $output = fopen( 'php://output', 'wb' );
01044                         $obj = new CF_Object( $cont, $srcRel, false, false ); // skip HEAD
01045                         $obj->stream( $output, $this->headersFromParams( $params ) );
01046                 } catch ( NoSuchObjectException $e ) {
01047                         $status->fatal( 'backend-fail-stream', $params['src'] );
01048                 } catch ( CloudFilesException $e ) { // some other exception?
01049                         $this->handleException( $e, $status, __METHOD__, $params );
01050                 }
01051 
01052                 return $status;
01053         }
01054 
01059         protected function doGetLocalCopyMulti( array $params ) {
01060                 $tmpFiles = array();
01061 
01062                 $ep = array_diff_key( $params, array( 'srcs' => 1 ) ); // for error logging
01063                 // Blindly create tmp files and stream to them, catching any exception if the file does
01064                 // not exist. Doing a stat here is useless causes infinite loops in addMissingMetadata().
01065                 foreach ( array_chunk( $params['srcs'], $params['concurrency'] ) as $pathBatch ) {
01066                         $cfOps = array(); // (path => CF_Async_Op)
01067 
01068                         foreach ( $pathBatch as $path ) { // each path in this concurrent batch
01069                                 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $path );
01070                                 if ( $srcRel === null ) {
01071                                         $tmpFiles[$path] = null;
01072                                         continue;
01073                                 }
01074                                 $tmpFile = null;
01075                                 try {
01076                                         $sContObj = $this->getContainer( $srcCont );
01077                                         $obj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD
01078                                         // Get source file extension
01079                                         $ext = FileBackend::extensionFromPath( $path );
01080                                         // Create a new temporary file...
01081                                         $tmpFile = TempFSFile::factory( 'localcopy_', $ext );
01082                                         if ( $tmpFile ) {
01083                                                 $handle = fopen( $tmpFile->getPath(), 'wb' );
01084                                                 if ( $handle ) {
01085                                                         $headers = $this->headersFromParams( $params );
01086                                                         if ( count( $pathBatch ) > 1 ) {
01087                                                                 $cfOps[$path] = $obj->stream_async( $handle, $headers );
01088                                                                 $cfOps[$path]->_file_handle = $handle; // close this later
01089                                                         } else {
01090                                                                 $obj->stream( $handle, $headers );
01091                                                                 fclose( $handle );
01092                                                         }
01093                                                 } else {
01094                                                         $tmpFile = null;
01095                                                 }
01096                                         }
01097                                 } catch ( NoSuchContainerException $e ) {
01098                                         $tmpFile = null;
01099                                 } catch ( NoSuchObjectException $e ) {
01100                                         $tmpFile = null;
01101                                 } catch ( CloudFilesException $e ) { // some other exception?
01102                                         $tmpFile = null;
01103                                         $this->handleException( $e, null, __METHOD__, array( 'src' => $path ) + $ep );
01104                                 }
01105                                 $tmpFiles[$path] = $tmpFile;
01106                         }
01107 
01108                         $batch = new CF_Async_Op_Batch( $cfOps );
01109                         $cfOps = $batch->execute();
01110                         foreach ( $cfOps as $path => $cfOp ) {
01111                                 try {
01112                                         $cfOp->getLastResponse();
01113                                 } catch ( NoSuchContainerException $e ) {
01114                                         $tmpFiles[$path] = null;
01115                                 } catch ( NoSuchObjectException $e ) {
01116                                         $tmpFiles[$path] = null;
01117                                 } catch ( CloudFilesException $e ) { // some other exception?
01118                                         $tmpFiles[$path] = null;
01119                                         $this->handleException( $e, null, __METHOD__, array( 'src' => $path ) + $ep );
01120                                 }
01121                                 fclose( $cfOp->_file_handle ); // close open handle
01122                         }
01123                 }
01124 
01125                 return $tmpFiles;
01126         }
01127 
01132         public function getFileHttpUrl( array $params ) {
01133                 if ( $this->swiftTempUrlKey != '' ) { // temp urls enabled
01134                         list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
01135                         if ( $srcRel === null ) {
01136                                 return null; // invalid path
01137                         }
01138                         try {
01139                                 $sContObj = $this->getContainer( $srcCont );
01140                                 $obj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD
01141                                 return $obj->get_temp_url( $this->swiftTempUrlKey, 86400, "GET" );
01142                         } catch ( NoSuchContainerException $e ) {
01143                         } catch ( CloudFilesException $e ) { // some other exception?
01144                                 $this->handleException( $e, null, __METHOD__, $params );
01145                         }
01146                 }
01147                 return null;
01148         }
01149 
01154         protected function directoriesAreVirtual() {
01155                 return true;
01156         }
01157 
01166         protected function headersFromParams( array $params ) {
01167                 $hdrs = array();
01168                 if ( !empty( $params['latest'] ) ) {
01169                         $hdrs[] = 'X-Newest: true';
01170                 }
01171                 return $hdrs;
01172         }
01173 
01178         protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
01179                 $statuses = array();
01180 
01181                 $cfOps = array(); // list of CF_Async_Op objects
01182                 foreach ( $fileOpHandles as $index => $fileOpHandle ) {
01183                         $cfOps[$index] = $fileOpHandle->cfOp;
01184                 }
01185                 $batch = new CF_Async_Op_Batch( $cfOps );
01186 
01187                 $cfOps = $batch->execute();
01188                 foreach ( $cfOps as $index => $cfOp ) {
01189                         $status = Status::newGood();
01190                         try { // catch exceptions; update status
01191                                 $function = '_getResponse' . $fileOpHandles[$index]->call;
01192                                 $this->$function( $cfOp, $status, $fileOpHandles[$index]->params );
01193                                 $this->purgeCDNCache( $fileOpHandles[$index]->affectedObjects );
01194                         } catch ( CloudFilesException $e ) { // some other exception?
01195                                 $this->handleException( $e, $status,
01196                                         __CLASS__ . ":$function", $fileOpHandles[$index]->params );
01197                         }
01198                         $statuses[$index] = $status;
01199                 }
01200 
01201                 return $statuses;
01202         }
01203 
01230         protected function setContainerAccess(
01231                 CF_Container $contObj, array $readGrps, array $writeGrps
01232         ) {
01233                 $creds = $contObj->cfs_auth->export_credentials();
01234 
01235                 $url = $creds['storage_url'] . '/' . rawurlencode( $contObj->name );
01236 
01237                 // Note: 10 second timeout consistent with php-cloudfiles
01238                 $req = MWHttpRequest::factory( $url, array( 'method' => 'POST', 'timeout' => 10 ) );
01239                 $req->setHeader( 'X-Auth-Token', $creds['auth_token'] );
01240                 $req->setHeader( 'X-Container-Read', implode( ',', $readGrps ) );
01241                 $req->setHeader( 'X-Container-Write', implode( ',', $writeGrps ) );
01242 
01243                 return $req->execute(); // should return 204
01244         }
01245 
01253         public function purgeCDNCache( array $objects ) {
01254                 if ( $this->swiftUseCDN && $this->swiftCDNPurgable ) {
01255                         foreach ( $objects as $object ) {
01256                                 try {
01257                                         $object->purge_from_cdn();
01258                                 } catch ( CDNNotEnabledException $e ) {
01259                                         // CDN not enabled; nothing to see here
01260                                 } catch ( CloudFilesException $e ) {
01261                                         $this->handleException( $e, null, __METHOD__,
01262                                                 array( 'cont' => $object->container->name, 'obj' => $object->name ) );
01263                                 }
01264                         }
01265                 }
01266         }
01267 
01274         protected function getConnection() {
01275                 if ( $this->connException instanceof CloudFilesException ) {
01276                         if ( ( time() - $this->connErrorTime ) < 60 ) {
01277                                 throw $this->connException; // failed last attempt; don't bother
01278                         } else { // actually retry this time
01279                                 $this->connException = null;
01280                                 $this->connErrorTime = 0;
01281                         }
01282                 }
01283                 // Session keys expire after a while, so we renew them periodically
01284                 $reAuth = ( ( time() - $this->sessionStarted ) > $this->authTTL );
01285                 // Authenticate with proxy and get a session key...
01286                 if ( !$this->conn || $reAuth ) {
01287                         $this->sessionStarted = 0;
01288                         $this->connContainerCache->clear();
01289                         $cacheKey = $this->getCredsCacheKey( $this->auth->username );
01290                         $creds = $this->srvCache->get( $cacheKey ); // credentials
01291                         if ( is_array( $creds ) ) { // cache hit
01292                                 $this->auth->load_cached_credentials(
01293                                         $creds['auth_token'], $creds['storage_url'], $creds['cdnm_url'] );
01294                                 $this->sessionStarted = time() - ceil( $this->authTTL/2 ); // skew for worst case
01295                         } else { // cache miss
01296                                 try {
01297                                         $this->auth->authenticate();
01298                                         $creds = $this->auth->export_credentials();
01299                                         $this->srvCache->add( $cacheKey, $creds, ceil( $this->authTTL/2 ) ); // cache
01300                                         $this->sessionStarted = time();
01301                                 } catch ( CloudFilesException $e ) {
01302                                         $this->connException = $e; // don't keep re-trying
01303                                         $this->connErrorTime = time();
01304                                         throw $e; // throw it back
01305                                 }
01306                         }
01307                         if ( $this->conn ) { // re-authorizing?
01308                                 $this->conn->close(); // close active cURL handles in CF_Http object
01309                         }
01310                         $this->conn = new CF_Connection( $this->auth );
01311                 }
01312                 return $this->conn;
01313         }
01314 
01320         protected function closeConnection() {
01321                 if ( $this->conn ) {
01322                         $this->conn->close(); // close active cURL handles in CF_Http object
01323                         $this->sessionStarted = 0;
01324                         $this->connContainerCache->clear();
01325                 }
01326         }
01327 
01334         private function getCredsCacheKey( $username ) {
01335                 return wfMemcKey( 'backend', $this->getName(), 'usercreds', $username );
01336         }
01337 
01347         protected function getContainer( $container, $bypassCache = false ) {
01348                 $conn = $this->getConnection(); // Swift proxy connection
01349                 if ( $bypassCache ) { // purge cache
01350                         $this->connContainerCache->clear( $container );
01351                 } elseif ( !$this->connContainerCache->has( $container, 'obj' ) ) {
01352                         $this->primeContainerCache( array( $container ) ); // check persistent cache
01353                 }
01354                 if ( !$this->connContainerCache->has( $container, 'obj' ) ) {
01355                         $contObj = $conn->get_container( $container );
01356                         // NoSuchContainerException not thrown: container must exist
01357                         $this->connContainerCache->set( $container, 'obj', $contObj ); // cache it
01358                         if ( !$bypassCache ) {
01359                                 $this->setContainerCache( $container, // update persistent cache
01360                                         array( 'bytes' => $contObj->bytes_used, 'count' => $contObj->object_count )
01361                                 );
01362                         }
01363                 }
01364                 return $this->connContainerCache->get( $container, 'obj' );
01365         }
01366 
01374         protected function createContainer( $container ) {
01375                 $conn = $this->getConnection(); // Swift proxy connection
01376                 $contObj = $conn->create_container( $container );
01377                 $this->connContainerCache->set( $container, 'obj', $contObj ); // cache
01378                 return $contObj;
01379         }
01380 
01388         protected function deleteContainer( $container ) {
01389                 $conn = $this->getConnection(); // Swift proxy connection
01390                 $this->connContainerCache->clear( $container ); // purge
01391                 $conn->delete_container( $container );
01392         }
01393 
01398         protected function doPrimeContainerCache( array $containerInfo ) {
01399                 try {
01400                         $conn = $this->getConnection(); // Swift proxy connection
01401                         foreach ( $containerInfo as $container => $info ) {
01402                                 $contObj = new CF_Container( $conn->cfs_auth, $conn->cfs_http,
01403                                         $container, $info['count'], $info['bytes'] );
01404                                 $this->connContainerCache->set( $container, 'obj', $contObj );
01405                         }
01406                 } catch ( CloudFilesException $e ) { // some other exception?
01407                         $this->handleException( $e, null, __METHOD__, array() );
01408                 }
01409         }
01410 
01421         protected function handleException( Exception $e, $status, $func, array $params ) {
01422                 if ( $status instanceof Status ) {
01423                         if ( $e instanceof AuthenticationException ) {
01424                                 $status->fatal( 'backend-fail-connect', $this->name );
01425                         } else {
01426                                 $status->fatal( 'backend-fail-internal', $this->name );
01427                         }
01428                 }
01429                 if ( $e->getMessage() ) {
01430                         trigger_error( "$func: " . $e->getMessage(), E_USER_WARNING );
01431                 }
01432                 if ( $e instanceof InvalidResponseException ) { // possibly a stale token
01433                         $this->srvCache->delete( $this->getCredsCacheKey( $this->auth->username ) );
01434                         $this->closeConnection(); // force a re-connect and re-auth next time
01435                 }
01436                 wfDebugLog( 'SwiftBackend',
01437                         get_class( $e ) . " in '{$func}' (given '" . FormatJson::encode( $params ) . "')" .
01438                         ( $e->getMessage() ? ": {$e->getMessage()}" : "" )
01439                 );
01440         }
01441 }
01442 
01446 class SwiftFileOpHandle extends FileBackendStoreOpHandle {
01448         public $cfOp;
01450         public $affectedObjects = array();
01451 
01452         public function __construct( $backend, array $params, $call, CF_Async_Op $cfOp ) {
01453                 $this->backend = $backend;
01454                 $this->params = $params;
01455                 $this->call = $call;
01456                 $this->cfOp = $cfOp;
01457         }
01458 }
01459 
01467 abstract class SwiftFileBackendList implements Iterator {
01469         protected $bufferIter = array();
01470         protected $bufferAfter = null; // string; list items *after* this path
01471         protected $pos = 0; // integer
01473         protected $params = array();
01474 
01476         protected $backend;
01477         protected $container; // string; container name
01478         protected $dir; // string; storage directory
01479         protected $suffixStart; // integer
01480 
01481         const PAGE_SIZE = 9000; // file listing buffer size
01482 
01489         public function __construct( SwiftFileBackend $backend, $fullCont, $dir, array $params ) {
01490                 $this->backend = $backend;
01491                 $this->container = $fullCont;
01492                 $this->dir = $dir;
01493                 if ( substr( $this->dir, -1 ) === '/' ) {
01494                         $this->dir = substr( $this->dir, 0, -1 ); // remove trailing slash
01495                 }
01496                 if ( $this->dir == '' ) { // whole container
01497                         $this->suffixStart = 0;
01498                 } else { // dir within container
01499                         $this->suffixStart = strlen( $this->dir ) + 1; // size of "path/to/dir/"
01500                 }
01501                 $this->params = $params;
01502         }
01503 
01508         public function key() {
01509                 return $this->pos;
01510         }
01511 
01516         public function next() {
01517                 // Advance to the next file in the page
01518                 next( $this->bufferIter );
01519                 ++$this->pos;
01520                 // Check if there are no files left in this page and
01521                 // advance to the next page if this page was not empty.
01522                 if ( !$this->valid() && count( $this->bufferIter ) ) {
01523                         $this->bufferIter = $this->pageFromList(
01524                                 $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
01525                         ); // updates $this->bufferAfter
01526                 }
01527         }
01528 
01533         public function rewind() {
01534                 $this->pos = 0;
01535                 $this->bufferAfter = null;
01536                 $this->bufferIter = $this->pageFromList(
01537                         $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
01538                 ); // updates $this->bufferAfter
01539         }
01540 
01545         public function valid() {
01546                 if ( $this->bufferIter === null ) {
01547                         return false; // some failure?
01548                 } else {
01549                         return ( current( $this->bufferIter ) !== false ); // no paths can have this value
01550                 }
01551         }
01552 
01563         abstract protected function pageFromList( $container, $dir, &$after, $limit, array $params );
01564 }
01565 
01569 class SwiftFileBackendDirList extends SwiftFileBackendList {
01574         public function current() {
01575                 return substr( current( $this->bufferIter ), $this->suffixStart, -1 );
01576         }
01577 
01582         protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
01583                 return $this->backend->getDirListPageInternal( $container, $dir, $after, $limit, $params );
01584         }
01585 }
01586 
01590 class SwiftFileBackendFileList extends SwiftFileBackendList {
01595         public function current() {
01596                 return substr( current( $this->bufferIter ), $this->suffixStart );
01597         }
01598 
01603         protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
01604                 return $this->backend->getFileListPageInternal( $container, $dir, $after, $limit, $params );
01605         }
01606 }