MediaWiki  REL1_20
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 $swiftAnonUser; // string; username to handle unauthenticated requests
00044         protected $swiftUseCDN; // boolean; whether CloudFiles CDN is enabled
00045         protected $swiftCDNExpiry; // integer; how long to cache things in the CDN
00046         protected $swiftCDNPurgable; // boolean; whether object CDN purging is enabled
00047 
00049         protected $conn; // Swift connection handle
00050         protected $sessionStarted = 0; // integer UNIX timestamp
00051 
00053         protected $connException;
00054         protected $connErrorTime = 0; // UNIX timestamp
00055 
00057         protected $srvCache;
00058 
00060         protected $connContainerCache; // container object cache
00061 
00088         public function __construct( array $config ) {
00089                 parent::__construct( $config );
00090                 if ( !MWInit::classExists( 'CF_Constants' ) ) {
00091                         throw new MWException( 'SwiftCloudFiles extension not installed.' );
00092                 }
00093                 // Required settings
00094                 $this->auth = new CF_Authentication(
00095                         $config['swiftUser'],
00096                         $config['swiftKey'],
00097                         null, // account; unused
00098                         $config['swiftAuthUrl']
00099                 );
00100                 // Optional settings
00101                 $this->authTTL = isset( $config['swiftAuthTTL'] )
00102                         ? $config['swiftAuthTTL']
00103                         : 5 * 60; // some sane number
00104                 $this->swiftAnonUser = isset( $config['swiftAnonUser'] )
00105                         ? $config['swiftAnonUser']
00106                         : '';
00107                 $this->shardViaHashLevels = isset( $config['shardViaHashLevels'] )
00108                         ? $config['shardViaHashLevels']
00109                         : '';
00110                 $this->swiftUseCDN = isset( $config['swiftUseCDN'] )
00111                         ? $config['swiftUseCDN']
00112                         : false;
00113                 $this->swiftCDNExpiry = isset( $config['swiftCDNExpiry'] )
00114                         ? $config['swiftCDNExpiry']
00115                         : 12*3600; // 12 hours is safe (tokens last 24 hours per http://docs.openstack.org)
00116                 $this->swiftCDNPurgable = isset( $config['swiftCDNPurgable'] )
00117                         ? $config['swiftCDNPurgable']
00118                         : true;
00119                 // Cache container information to mask latency
00120                 $this->memCache = wfGetMainCache();
00121                 // Process cache for container info
00122                 $this->connContainerCache = new ProcessCacheLRU( 300 );
00123                 // Cache auth token information to avoid RTTs
00124                 if ( !empty( $config['cacheAuthInfo'] ) ) {
00125                         if ( php_sapi_name() === 'cli' ) {
00126                                 $this->srvCache = wfGetMainCache(); // preferrably memcached
00127                         } else {
00128                                 try { // look for APC, XCache, WinCache, ect...
00129                                         $this->srvCache = ObjectCache::newAccelerator( array() );
00130                                 } catch ( Exception $e ) {}
00131                         }
00132                 }
00133                 $this->srvCache = $this->srvCache ? $this->srvCache : new EmptyBagOStuff();
00134         }
00135 
00140         protected function resolveContainerPath( $container, $relStoragePath ) {
00141                 if ( !mb_check_encoding( $relStoragePath, 'UTF-8' ) ) { // mb_string required by CF
00142                         return null; // not UTF-8, makes it hard to use CF and the swift HTTP API
00143                 } elseif ( strlen( urlencode( $relStoragePath ) ) > 1024 ) {
00144                         return null; // too long for Swift
00145                 }
00146                 return $relStoragePath;
00147         }
00148 
00153         public function isPathUsableInternal( $storagePath ) {
00154                 list( $container, $rel ) = $this->resolveStoragePathReal( $storagePath );
00155                 if ( $rel === null ) {
00156                         return false; // invalid
00157                 }
00158 
00159                 try {
00160                         $this->getContainer( $container );
00161                         return true; // container exists
00162                 } catch ( NoSuchContainerException $e ) {
00163                 } catch ( CloudFilesException $e ) { // some other exception?
00164                         $this->handleException( $e, null, __METHOD__, array( 'path' => $storagePath ) );
00165                 }
00166 
00167                 return false;
00168         }
00169 
00174         protected function truncDisp( $disposition ) {
00175                 $res = '';
00176                 foreach ( explode( ';', $disposition ) as $part ) {
00177                         $part = trim( $part );
00178                         $new  = ( $res === '' ) ? $part : "{$res};{$part}";
00179                         if ( strlen( $new ) <= 255 ) {
00180                                 $res = $new;
00181                         } else {
00182                                 break; // too long; sigh
00183                         }
00184                 }
00185                 return $res;
00186         }
00187 
00192         protected function doCreateInternal( array $params ) {
00193                 $status = Status::newGood();
00194 
00195                 list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
00196                 if ( $dstRel === null ) {
00197                         $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
00198                         return $status;
00199                 }
00200 
00201                 // (a) Check the destination container and object
00202                 try {
00203                         $dContObj = $this->getContainer( $dstCont );
00204                         if ( empty( $params['overwrite'] ) &&
00205                                 $this->fileExists( array( 'src' => $params['dst'], 'latest' => 1 ) ) )
00206                         {
00207                                 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
00208                                 return $status;
00209                         }
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                         // Note: metadata keys stored as [Upper case char][[Lower case char]...]
00227                         $obj->metadata = array( 'Sha1base36' => $sha1Hash );
00228                         // Manually set the ETag (https://github.com/rackspace/php-cloudfiles/issues/59).
00229                         // The MD5 here will be checked within Swift against its own MD5.
00230                         $obj->set_etag( md5( $params['content'] ) );
00231                         // Use the same content type as StreamFile for security
00232                         $obj->content_type = StreamFile::contentTypeFromPath( $params['dst'] );
00233                         if ( !strlen( $obj->content_type ) ) { // special case
00234                                 $obj->content_type = 'unknown/unknown';
00235                         }
00236                         // Set the Content-Disposition header if requested
00237                         if ( isset( $params['disposition'] ) ) {
00238                                 $obj->headers['Content-Disposition'] = $this->truncDisp( $params['disposition'] );
00239                         }
00240                         if ( !empty( $params['async'] ) ) { // deferred
00241                                 $op = $obj->write_async( $params['content'] );
00242                                 $status->value = new SwiftFileOpHandle( $this, $params, 'Create', $op );
00243                                 if ( !empty( $params['overwrite'] ) ) { // file possibly mutated
00244                                         $status->value->affectedObjects[] = $obj;
00245                                 }
00246                         } else { // actually write the object in Swift
00247                                 $obj->write( $params['content'] );
00248                                 if ( !empty( $params['overwrite'] ) ) { // file possibly mutated
00249                                         $this->purgeCDNCache( array( $obj ) );
00250                                 }
00251                         }
00252                 } catch ( CDNNotEnabledException $e ) {
00253                         // CDN not enabled; nothing to see here
00254                 } catch ( BadContentTypeException $e ) {
00255                         $status->fatal( 'backend-fail-contenttype', $params['dst'] );
00256                 } catch ( CloudFilesException $e ) { // some other exception?
00257                         $this->handleException( $e, $status, __METHOD__, $params );
00258                 }
00259 
00260                 return $status;
00261         }
00262 
00266         protected function _getResponseCreate( CF_Async_Op $cfOp, Status $status, array $params ) {
00267                 try {
00268                         $cfOp->getLastResponse();
00269                 } catch ( BadContentTypeException $e ) {
00270                         $status->fatal( 'backend-fail-contenttype', $params['dst'] );
00271                 }
00272         }
00273 
00278         protected function doStoreInternal( array $params ) {
00279                 $status = Status::newGood();
00280 
00281                 list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
00282                 if ( $dstRel === null ) {
00283                         $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
00284                         return $status;
00285                 }
00286 
00287                 // (a) Check the destination container and object
00288                 try {
00289                         $dContObj = $this->getContainer( $dstCont );
00290                         if ( empty( $params['overwrite'] ) &&
00291                                 $this->fileExists( array( 'src' => $params['dst'], 'latest' => 1 ) ) )
00292                         {
00293                                 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
00294                                 return $status;
00295                         }
00296                 } catch ( NoSuchContainerException $e ) {
00297                         $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
00298                         return $status;
00299                 } catch ( CloudFilesException $e ) { // some other exception?
00300                         $this->handleException( $e, $status, __METHOD__, $params );
00301                         return $status;
00302                 }
00303 
00304                 // (b) Get a SHA-1 hash of the object
00305                 $sha1Hash = sha1_file( $params['src'] );
00306                 if ( $sha1Hash === false ) { // source doesn't exist?
00307                         $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
00308                         return $status;
00309                 }
00310                 $sha1Hash = wfBaseConvert( $sha1Hash, 16, 36, 31 );
00311 
00312                 // (c) Actually store the object
00313                 try {
00314                         // Create a fresh CF_Object with no fields preloaded.
00315                         // We don't want to preserve headers, metadata, and such.
00316                         $obj = new CF_Object( $dContObj, $dstRel, false, false ); // skip HEAD
00317                         // Note: metadata keys stored as [Upper case char][[Lower case char]...]
00318                         $obj->metadata = array( 'Sha1base36' => $sha1Hash );
00319                         // The MD5 here will be checked within Swift against its own MD5.
00320                         $obj->set_etag( md5_file( $params['src'] ) );
00321                         // Use the same content type as StreamFile for security
00322                         $obj->content_type = StreamFile::contentTypeFromPath( $params['dst'] );
00323                         if ( !strlen( $obj->content_type ) ) { // special case
00324                                 $obj->content_type = 'unknown/unknown';
00325                         }
00326                         // Set the Content-Disposition header if requested
00327                         if ( isset( $params['disposition'] ) ) {
00328                                 $obj->headers['Content-Disposition'] = $this->truncDisp( $params['disposition'] );
00329                         }
00330                         if ( !empty( $params['async'] ) ) { // deferred
00331                                 wfSuppressWarnings();
00332                                 $fp = fopen( $params['src'], 'rb' );
00333                                 wfRestoreWarnings();
00334                                 if ( !$fp ) {
00335                                         $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
00336                                 } else {
00337                                         $op = $obj->write_async( $fp, filesize( $params['src'] ), true );
00338                                         $status->value = new SwiftFileOpHandle( $this, $params, 'Store', $op );
00339                                         $status->value->resourcesToClose[] = $fp;
00340                                         if ( !empty( $params['overwrite'] ) ) { // file possibly mutated
00341                                                 $status->value->affectedObjects[] = $obj;
00342                                         }
00343                                 }
00344                         } else { // actually write the object in Swift
00345                                 $obj->load_from_filename( $params['src'], true ); // calls $obj->write()
00346                                 if ( !empty( $params['overwrite'] ) ) { // file possibly mutated
00347                                         $this->purgeCDNCache( array( $obj ) );
00348                                 }
00349                         }
00350                 } catch ( CDNNotEnabledException $e ) {
00351                         // CDN not enabled; nothing to see here
00352                 } catch ( BadContentTypeException $e ) {
00353                         $status->fatal( 'backend-fail-contenttype', $params['dst'] );
00354                 } catch ( IOException $e ) {
00355                         $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
00356                 } catch ( CloudFilesException $e ) { // some other exception?
00357                         $this->handleException( $e, $status, __METHOD__, $params );
00358                 }
00359 
00360                 return $status;
00361         }
00362 
00366         protected function _getResponseStore( CF_Async_Op $cfOp, Status $status, array $params ) {
00367                 try {
00368                         $cfOp->getLastResponse();
00369                 } catch ( BadContentTypeException $e ) {
00370                         $status->fatal( 'backend-fail-contenttype', $params['dst'] );
00371                 } catch ( IOException $e ) {
00372                         $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
00373                 }
00374         }
00375 
00380         protected function doCopyInternal( array $params ) {
00381                 $status = Status::newGood();
00382 
00383                 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
00384                 if ( $srcRel === null ) {
00385                         $status->fatal( 'backend-fail-invalidpath', $params['src'] );
00386                         return $status;
00387                 }
00388 
00389                 list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
00390                 if ( $dstRel === null ) {
00391                         $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
00392                         return $status;
00393                 }
00394 
00395                 // (a) Check the source/destination containers and destination object
00396                 try {
00397                         $sContObj = $this->getContainer( $srcCont );
00398                         $dContObj = $this->getContainer( $dstCont );
00399                         if ( empty( $params['overwrite'] ) &&
00400                                 $this->fileExists( array( 'src' => $params['dst'], 'latest' => 1 ) ) )
00401                         {
00402                                 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
00403                                 return $status;
00404                         }
00405                 } catch ( NoSuchContainerException $e ) {
00406                         $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
00407                         return $status;
00408                 } catch ( CloudFilesException $e ) { // some other exception?
00409                         $this->handleException( $e, $status, __METHOD__, $params );
00410                         return $status;
00411                 }
00412 
00413                 // (b) Actually copy the file to the destination
00414                 try {
00415                         $dstObj = new CF_Object( $dContObj, $dstRel, false, false ); // skip HEAD
00416                         $hdrs = array(); // source file headers to override with new values
00417                         if ( isset( $params['disposition'] ) ) {
00418                                 $hdrs['Content-Disposition'] = $this->truncDisp( $params['disposition'] );
00419                         }
00420                         if ( !empty( $params['async'] ) ) { // deferred
00421                                 $op = $sContObj->copy_object_to_async( $srcRel, $dContObj, $dstRel, null, $hdrs );
00422                                 $status->value = new SwiftFileOpHandle( $this, $params, 'Copy', $op );
00423                                 if ( !empty( $params['overwrite'] ) ) { // file possibly mutated
00424                                         $status->value->affectedObjects[] = $dstObj;
00425                                 }
00426                         } else { // actually write the object in Swift
00427                                 $sContObj->copy_object_to( $srcRel, $dContObj, $dstRel, null, $hdrs );
00428                                 if ( !empty( $params['overwrite'] ) ) { // file possibly mutated
00429                                         $this->purgeCDNCache( array( $dstObj ) );
00430                                 }
00431                         }
00432                 } catch ( CDNNotEnabledException $e ) {
00433                         // CDN not enabled; nothing to see here
00434                 } catch ( NoSuchObjectException $e ) { // source object does not exist
00435                         $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
00436                 } catch ( CloudFilesException $e ) { // some other exception?
00437                         $this->handleException( $e, $status, __METHOD__, $params );
00438                 }
00439 
00440                 return $status;
00441         }
00442 
00446         protected function _getResponseCopy( CF_Async_Op $cfOp, Status $status, array $params ) {
00447                 try {
00448                         $cfOp->getLastResponse();
00449                 } catch ( NoSuchObjectException $e ) { // source object does not exist
00450                         $status->fatal( 'backend-fail-copy', $params['src'], $params['dst'] );
00451                 }
00452         }
00453 
00458         protected function doMoveInternal( array $params ) {
00459                 $status = Status::newGood();
00460 
00461                 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
00462                 if ( $srcRel === null ) {
00463                         $status->fatal( 'backend-fail-invalidpath', $params['src'] );
00464                         return $status;
00465                 }
00466 
00467                 list( $dstCont, $dstRel ) = $this->resolveStoragePathReal( $params['dst'] );
00468                 if ( $dstRel === null ) {
00469                         $status->fatal( 'backend-fail-invalidpath', $params['dst'] );
00470                         return $status;
00471                 }
00472 
00473                 // (a) Check the source/destination containers and destination object
00474                 try {
00475                         $sContObj = $this->getContainer( $srcCont );
00476                         $dContObj = $this->getContainer( $dstCont );
00477                         if ( empty( $params['overwrite'] ) &&
00478                                 $this->fileExists( array( 'src' => $params['dst'], 'latest' => 1 ) ) )
00479                         {
00480                                 $status->fatal( 'backend-fail-alreadyexists', $params['dst'] );
00481                                 return $status;
00482                         }
00483                 } catch ( NoSuchContainerException $e ) {
00484                         $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
00485                         return $status;
00486                 } catch ( CloudFilesException $e ) { // some other exception?
00487                         $this->handleException( $e, $status, __METHOD__, $params );
00488                         return $status;
00489                 }
00490 
00491                 // (b) Actually move the file to the destination
00492                 try {
00493                         $srcObj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD
00494                         $dstObj = new CF_Object( $dContObj, $dstRel, false, false ); // skip HEAD
00495                         $hdrs = array(); // source file headers to override with new values
00496                         if ( isset( $params['disposition'] ) ) {
00497                                 $hdrs['Content-Disposition'] = $this->truncDisp( $params['disposition'] );
00498                         }
00499                         if ( !empty( $params['async'] ) ) { // deferred
00500                                 $op = $sContObj->move_object_to_async( $srcRel, $dContObj, $dstRel, null, $hdrs );
00501                                 $status->value = new SwiftFileOpHandle( $this, $params, 'Move', $op );
00502                                 $status->value->affectedObjects[] = $srcObj;
00503                                 if ( !empty( $params['overwrite'] ) ) { // file possibly mutated
00504                                         $status->value->affectedObjects[] = $dstObj;
00505                                 }
00506                         } else { // actually write the object in Swift
00507                                 $sContObj->move_object_to( $srcRel, $dContObj, $dstRel, null, $hdrs );
00508                                 $this->purgeCDNCache( array( $srcObj ) );
00509                                 if ( !empty( $params['overwrite'] ) ) { // file possibly mutated
00510                                         $this->purgeCDNCache( array( $dstObj ) );
00511                                 }
00512                         }
00513                 } catch ( CDNNotEnabledException $e ) {
00514                         // CDN not enabled; nothing to see here
00515                 } catch ( NoSuchObjectException $e ) { // source object does not exist
00516                         $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
00517                 } catch ( CloudFilesException $e ) { // some other exception?
00518                         $this->handleException( $e, $status, __METHOD__, $params );
00519                 }
00520 
00521                 return $status;
00522         }
00523 
00527         protected function _getResponseMove( CF_Async_Op $cfOp, Status $status, array $params ) {
00528                 try {
00529                         $cfOp->getLastResponse();
00530                 } catch ( NoSuchObjectException $e ) { // source object does not exist
00531                         $status->fatal( 'backend-fail-move', $params['src'], $params['dst'] );
00532                 }
00533         }
00534 
00539         protected function doDeleteInternal( array $params ) {
00540                 $status = Status::newGood();
00541 
00542                 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
00543                 if ( $srcRel === null ) {
00544                         $status->fatal( 'backend-fail-invalidpath', $params['src'] );
00545                         return $status;
00546                 }
00547 
00548                 try {
00549                         $sContObj = $this->getContainer( $srcCont );
00550                         $srcObj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD
00551                         if ( !empty( $params['async'] ) ) { // deferred
00552                                 $op = $sContObj->delete_object_async( $srcRel );
00553                                 $status->value = new SwiftFileOpHandle( $this, $params, 'Delete', $op );
00554                                 $status->value->affectedObjects[] = $srcObj;
00555                         } else { // actually write the object in Swift
00556                                 $sContObj->delete_object( $srcRel );
00557                                 $this->purgeCDNCache( array( $srcObj ) );
00558                         }
00559                 } catch ( CDNNotEnabledException $e ) {
00560                         // CDN not enabled; nothing to see here
00561                 } catch ( NoSuchContainerException $e ) {
00562                         $status->fatal( 'backend-fail-delete', $params['src'] );
00563                 } catch ( NoSuchObjectException $e ) {
00564                         if ( empty( $params['ignoreMissingSource'] ) ) {
00565                                 $status->fatal( 'backend-fail-delete', $params['src'] );
00566                         }
00567                 } catch ( CloudFilesException $e ) { // some other exception?
00568                         $this->handleException( $e, $status, __METHOD__, $params );
00569                 }
00570 
00571                 return $status;
00572         }
00573 
00577         protected function _getResponseDelete( CF_Async_Op $cfOp, Status $status, array $params ) {
00578                 try {
00579                         $cfOp->getLastResponse();
00580                 } catch ( NoSuchContainerException $e ) {
00581                         $status->fatal( 'backend-fail-delete', $params['src'] );
00582                 } catch ( NoSuchObjectException $e ) {
00583                         if ( empty( $params['ignoreMissingSource'] ) ) {
00584                                 $status->fatal( 'backend-fail-delete', $params['src'] );
00585                         }
00586                 }
00587         }
00588 
00593         protected function doPrepareInternal( $fullCont, $dir, array $params ) {
00594                 $status = Status::newGood();
00595 
00596                 // (a) Check if container already exists
00597                 try {
00598                         $contObj = $this->getContainer( $fullCont );
00599                         // NoSuchContainerException not thrown: container must exist
00600                         return $status; // already exists
00601                 } catch ( NoSuchContainerException $e ) {
00602                         // NoSuchContainerException thrown: container does not exist
00603                 } catch ( CloudFilesException $e ) { // some other exception?
00604                         $this->handleException( $e, $status, __METHOD__, $params );
00605                         return $status;
00606                 }
00607 
00608                 // (b) Create container as needed
00609                 try {
00610                         $contObj = $this->createContainer( $fullCont );
00611                         if ( !empty( $params['noAccess'] ) ) {
00612                                 // Make container private to end-users...
00613                                 $status->merge( $this->doSecureInternal( $fullCont, $dir, $params ) );
00614                         } else {
00615                                 // Make container public to end-users...
00616                                 $status->merge( $this->doPublishInternal( $fullCont, $dir, $params ) );
00617                         }
00618                         if ( $this->swiftUseCDN ) { // Rackspace style CDN
00619                                 $contObj->make_public( $this->swiftCDNExpiry );
00620                         }
00621                 } catch ( CDNNotEnabledException $e ) {
00622                         // CDN not enabled; nothing to see here
00623                 } catch ( CloudFilesException $e ) { // some other exception?
00624                         $this->handleException( $e, $status, __METHOD__, $params );
00625                         return $status;
00626                 }
00627 
00628                 return $status;
00629         }
00630 
00635         protected function doSecureInternal( $fullCont, $dir, array $params ) {
00636                 $status = Status::newGood();
00637                 if ( empty( $params['noAccess'] ) ) {
00638                         return $status; // nothing to do
00639                 }
00640 
00641                 // Restrict 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 private to end-users...
00649                         $status->merge( $this->setContainerAccess(
00650                                 $contObj,
00651                                 array( $this->auth->username ), // read
00652                                 array( $this->auth->username ) // write
00653                         ) );
00654                         if ( $this->swiftUseCDN && $contObj->is_public() ) { // Rackspace style CDN
00655                                 $contObj->make_private();
00656                         }
00657                 } catch ( CDNNotEnabledException $e ) {
00658                         // CDN not enabled; nothing to see here
00659                 } catch ( CloudFilesException $e ) { // some other exception?
00660                         $this->handleException( $e, $status, __METHOD__, $params );
00661                 }
00662 
00663                 return $status;
00664         }
00665 
00670         protected function doPublishInternal( $fullCont, $dir, array $params ) {
00671                 $status = Status::newGood();
00672 
00673                 // Unrestrict container from end-users...
00674                 try {
00675                         // doPrepareInternal() should have been called,
00676                         // so the Swift container should already exist...
00677                         $contObj = $this->getContainer( $fullCont ); // normally a cache hit
00678                         // NoSuchContainerException not thrown: container must exist
00679 
00680                         // Make container public to end-users...
00681                         if ( $this->swiftAnonUser != '' ) {
00682                                 $status->merge( $this->setContainerAccess(
00683                                         $contObj,
00684                                         array( $this->auth->username, $this->swiftAnonUser ), // read
00685                                         array( $this->auth->username, $this->swiftAnonUser ) // write
00686                                 ) );
00687                         } else {
00688                                 $status->merge( $this->setContainerAccess(
00689                                         $contObj,
00690                                         array( $this->auth->username, '.r:*' ), // read
00691                                         array( $this->auth->username ) // write
00692                                 ) );
00693                         }
00694                         if ( $this->swiftUseCDN && !$contObj->is_public() ) { // Rackspace style CDN
00695                                 $contObj->make_public();
00696                         }
00697                 } catch ( CDNNotEnabledException $e ) {
00698                         // CDN not enabled; nothing to see here
00699                 } catch ( CloudFilesException $e ) { // some other exception?
00700                         $this->handleException( $e, $status, __METHOD__, $params );
00701                 }
00702 
00703                 return $status;
00704         }
00705 
00710         protected function doCleanInternal( $fullCont, $dir, array $params ) {
00711                 $status = Status::newGood();
00712 
00713                 // Only containers themselves can be removed, all else is virtual
00714                 if ( $dir != '' ) {
00715                         return $status; // nothing to do
00716                 }
00717 
00718                 // (a) Check the container
00719                 try {
00720                         $contObj = $this->getContainer( $fullCont, true );
00721                 } catch ( NoSuchContainerException $e ) {
00722                         return $status; // ok, nothing to do
00723                 } catch ( CloudFilesException $e ) { // some other exception?
00724                         $this->handleException( $e, $status, __METHOD__, $params );
00725                         return $status;
00726                 }
00727 
00728                 // (b) Delete the container if empty
00729                 if ( $contObj->object_count == 0 ) {
00730                         try {
00731                                 $this->deleteContainer( $fullCont );
00732                         } catch ( NoSuchContainerException $e ) {
00733                                 return $status; // race?
00734                         } catch ( NonEmptyContainerException $e ) {
00735                                 return $status; // race? consistency delay?
00736                         } catch ( CloudFilesException $e ) { // some other exception?
00737                                 $this->handleException( $e, $status, __METHOD__, $params );
00738                                 return $status;
00739                         }
00740                 }
00741 
00742                 return $status;
00743         }
00744 
00749         protected function doGetFileStat( array $params ) {
00750                 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
00751                 if ( $srcRel === null ) {
00752                         return false; // invalid storage path
00753                 }
00754 
00755                 $stat = false;
00756                 try {
00757                         $contObj = $this->getContainer( $srcCont );
00758                         $srcObj = $contObj->get_object( $srcRel, $this->headersFromParams( $params ) );
00759                         $this->addMissingMetadata( $srcObj, $params['src'] );
00760                         $stat = array(
00761                                 // Convert dates like "Tue, 03 Jan 2012 22:01:04 GMT" to TS_MW
00762                                 'mtime' => wfTimestamp( TS_MW, $srcObj->last_modified ),
00763                                 'size'  => (int)$srcObj->content_length,
00764                                 'sha1'  => $srcObj->metadata['Sha1base36']
00765                         );
00766                 } catch ( NoSuchContainerException $e ) {
00767                 } catch ( NoSuchObjectException $e ) {
00768                 } catch ( CloudFilesException $e ) { // some other exception?
00769                         $stat = null;
00770                         $this->handleException( $e, null, __METHOD__, $params );
00771                 }
00772 
00773                 return $stat;
00774         }
00775 
00784         protected function addMissingMetadata( CF_Object $obj, $path ) {
00785                 if ( isset( $obj->metadata['Sha1base36'] ) ) {
00786                         return true; // nothing to do
00787                 }
00788                 wfProfileIn( __METHOD__ );
00789                 $status = Status::newGood();
00790                 $scopeLockS = $this->getScopedFileLocks( array( $path ), LockManager::LOCK_UW, $status );
00791                 if ( $status->isOK() ) {
00792                         # Do not stat the file in getLocalCopy() to avoid infinite loops
00793                         $tmpFile = $this->getLocalCopy( array( 'src' => $path, 'latest' => 1, 'nostat' => 1 ) );
00794                         if ( $tmpFile ) {
00795                                 $hash = $tmpFile->getSha1Base36();
00796                                 if ( $hash !== false ) {
00797                                         $obj->metadata['Sha1base36'] = $hash;
00798                                         $obj->sync_metadata(); // save to Swift
00799                                         wfProfileOut( __METHOD__ );
00800                                         return true; // success
00801                                 }
00802                         }
00803                 }
00804                 $obj->metadata['Sha1base36'] = false;
00805                 wfProfileOut( __METHOD__ );
00806                 return false; // failed
00807         }
00808 
00813         public function getFileContents( array $params ) {
00814                 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
00815                 if ( $srcRel === null ) {
00816                         return false; // invalid storage path
00817                 }
00818 
00819                 if ( !$this->fileExists( $params ) ) {
00820                         return null;
00821                 }
00822 
00823                 $data = false;
00824                 try {
00825                         $sContObj = $this->getContainer( $srcCont );
00826                         $obj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD
00827                         $data = $obj->read( $this->headersFromParams( $params ) );
00828                 } catch ( NoSuchContainerException $e ) {
00829                 } catch ( CloudFilesException $e ) { // some other exception?
00830                         $this->handleException( $e, null, __METHOD__, $params );
00831                 }
00832 
00833                 return $data;
00834         }
00835 
00840         protected function doDirectoryExists( $fullCont, $dir, array $params ) {
00841                 try {
00842                         $container = $this->getContainer( $fullCont );
00843                         $prefix = ( $dir == '' ) ? null : "{$dir}/";
00844                         return ( count( $container->list_objects( 1, null, $prefix ) ) > 0 );
00845                 } catch ( NoSuchContainerException $e ) {
00846                         return false;
00847                 } catch ( CloudFilesException $e ) { // some other exception?
00848                         $this->handleException( $e, null, __METHOD__,
00849                                 array( 'cont' => $fullCont, 'dir' => $dir ) );
00850                 }
00851 
00852                 return null; // error
00853         }
00854 
00859         public function getDirectoryListInternal( $fullCont, $dir, array $params ) {
00860                 return new SwiftFileBackendDirList( $this, $fullCont, $dir, $params );
00861         }
00862 
00867         public function getFileListInternal( $fullCont, $dir, array $params ) {
00868                 return new SwiftFileBackendFileList( $this, $fullCont, $dir, $params );
00869         }
00870 
00881         public function getDirListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
00882                 $dirs = array();
00883                 if ( $after === INF ) {
00884                         return $dirs; // nothing more
00885                 }
00886                 wfProfileIn( __METHOD__ . '-' . $this->name );
00887 
00888                 try {
00889                         $container = $this->getContainer( $fullCont );
00890                         $prefix = ( $dir == '' ) ? null : "{$dir}/";
00891                         // Non-recursive: only list dirs right under $dir
00892                         if ( !empty( $params['topOnly'] ) ) {
00893                                 $objects = $container->list_objects( $limit, $after, $prefix, null, '/' );
00894                                 foreach ( $objects as $object ) { // files and dirs
00895                                         if ( substr( $object, -1 ) === '/' ) {
00896                                                 $dirs[] = $object; // directories end in '/'
00897                                         }
00898                                 }
00899                         // Recursive: list all dirs under $dir and its subdirs
00900                         } else {
00901                                 // Get directory from last item of prior page
00902                                 $lastDir = $this->getParentDir( $after ); // must be first page
00903                                 $objects = $container->list_objects( $limit, $after, $prefix );
00904                                 foreach ( $objects as $object ) { // files
00905                                         $objectDir = $this->getParentDir( $object ); // directory of object
00906                                         if ( $objectDir !== false ) { // file has a parent dir
00907                                                 // Swift stores paths in UTF-8, using binary sorting.
00908                                                 // See function "create_container_table" in common/db.py.
00909                                                 // If a directory is not "greater" than the last one,
00910                                                 // then it was already listed by the calling iterator.
00911                                                 if ( strcmp( $objectDir, $lastDir ) > 0 ) {
00912                                                         $pDir = $objectDir;
00913                                                         do { // add dir and all its parent dirs
00914                                                                 $dirs[] = "{$pDir}/";
00915                                                                 $pDir = $this->getParentDir( $pDir );
00916                                                         } while ( $pDir !== false // sanity
00917                                                                 && strcmp( $pDir, $lastDir ) > 0 // not done already
00918                                                                 && strlen( $pDir ) > strlen( $dir ) // within $dir
00919                                                         );
00920                                                 }
00921                                                 $lastDir = $objectDir;
00922                                         }
00923                                 }
00924                         }
00925                         if ( count( $objects ) < $limit ) {
00926                                 $after = INF; // avoid a second RTT
00927                         } else {
00928                                 $after = end( $objects ); // update last item
00929                         }
00930                 } catch ( NoSuchContainerException $e ) {
00931                 } catch ( CloudFilesException $e ) { // some other exception?
00932                         $this->handleException( $e, null, __METHOD__,
00933                                 array( 'cont' => $fullCont, 'dir' => $dir ) );
00934                 }
00935 
00936                 wfProfileOut( __METHOD__ . '-' . $this->name );
00937                 return $dirs;
00938         }
00939 
00940         protected function getParentDir( $path ) {
00941                 return ( strpos( $path, '/' ) !== false ) ? dirname( $path ) : false;
00942         }
00943 
00954         public function getFileListPageInternal( $fullCont, $dir, &$after, $limit, array $params ) {
00955                 $files = array();
00956                 if ( $after === INF ) {
00957                         return $files; // nothing more
00958                 }
00959                 wfProfileIn( __METHOD__ . '-' . $this->name );
00960 
00961                 try {
00962                         $container = $this->getContainer( $fullCont );
00963                         $prefix = ( $dir == '' ) ? null : "{$dir}/";
00964                         // Non-recursive: only list files right under $dir
00965                         if ( !empty( $params['topOnly'] ) ) { // files and dirs
00966                                 $objects = $container->list_objects( $limit, $after, $prefix, null, '/' );
00967                                 foreach ( $objects as $object ) {
00968                                         if ( substr( $object, -1 ) !== '/' ) {
00969                                                 $files[] = $object; // directories end in '/'
00970                                         }
00971                                 }
00972                         // Recursive: list all files under $dir and its subdirs
00973                         } else { // files
00974                                 $objects = $container->list_objects( $limit, $after, $prefix );
00975                                 $files = $objects;
00976                         }
00977                         if ( count( $objects ) < $limit ) {
00978                                 $after = INF; // avoid a second RTT
00979                         } else {
00980                                 $after = end( $objects ); // update last item
00981                         }
00982                 } catch ( NoSuchContainerException $e ) {
00983                 } catch ( CloudFilesException $e ) { // some other exception?
00984                         $this->handleException( $e, null, __METHOD__,
00985                                 array( 'cont' => $fullCont, 'dir' => $dir ) );
00986                 }
00987 
00988                 wfProfileOut( __METHOD__ . '-' . $this->name );
00989                 return $files;
00990         }
00991 
00996         protected function doGetFileSha1base36( array $params ) {
00997                 $stat = $this->getFileStat( $params );
00998                 if ( $stat ) {
00999                         return $stat['sha1'];
01000                 } else {
01001                         return false;
01002                 }
01003         }
01004 
01009         protected function doStreamFile( array $params ) {
01010                 $status = Status::newGood();
01011 
01012                 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
01013                 if ( $srcRel === null ) {
01014                         $status->fatal( 'backend-fail-invalidpath', $params['src'] );
01015                 }
01016 
01017                 try {
01018                         $cont = $this->getContainer( $srcCont );
01019                 } catch ( NoSuchContainerException $e ) {
01020                         $status->fatal( 'backend-fail-stream', $params['src'] );
01021                         return $status;
01022                 } catch ( CloudFilesException $e ) { // some other exception?
01023                         $this->handleException( $e, $status, __METHOD__, $params );
01024                         return $status;
01025                 }
01026 
01027                 try {
01028                         $output = fopen( 'php://output', 'wb' );
01029                         $obj = new CF_Object( $cont, $srcRel, false, false ); // skip HEAD
01030                         $obj->stream( $output, $this->headersFromParams( $params ) );
01031                 } catch ( NoSuchObjectException $e ) {
01032                         $status->fatal( 'backend-fail-stream', $params['src'] );
01033                 } catch ( CloudFilesException $e ) { // some other exception?
01034                         $this->handleException( $e, $status, __METHOD__, $params );
01035                 }
01036 
01037                 return $status;
01038         }
01039 
01044         public function getLocalCopy( array $params ) {
01045                 list( $srcCont, $srcRel ) = $this->resolveStoragePathReal( $params['src'] );
01046                 if ( $srcRel === null ) {
01047                         return null;
01048                 }
01049 
01050                 // Blindly create a tmp file and stream to it, catching any exception if the file does
01051                 // not exist. Also, doing a stat here will cause infinite loops when filling metadata.
01052                 $tmpFile = null;
01053                 try {
01054                         $sContObj = $this->getContainer( $srcCont );
01055                         $obj = new CF_Object( $sContObj, $srcRel, false, false ); // skip HEAD
01056                         // Get source file extension
01057                         $ext = FileBackend::extensionFromPath( $srcRel );
01058                         // Create a new temporary file...
01059                         $tmpFile = TempFSFile::factory( 'localcopy_', $ext );
01060                         if ( $tmpFile ) {
01061                                 $handle = fopen( $tmpFile->getPath(), 'wb' );
01062                                 if ( $handle ) {
01063                                         $obj->stream( $handle, $this->headersFromParams( $params ) );
01064                                         fclose( $handle );
01065                                 } else {
01066                                         $tmpFile = null; // couldn't open temp file
01067                                 }
01068                         }
01069                 } catch ( NoSuchContainerException $e ) {
01070                         $tmpFile = null;
01071                 } catch ( NoSuchObjectException $e ) {
01072                         $tmpFile = null;
01073                 } catch ( CloudFilesException $e ) { // some other exception?
01074                         $tmpFile = null;
01075                         $this->handleException( $e, null, __METHOD__, $params );
01076                 }
01077 
01078                 return $tmpFile;
01079         }
01080 
01085         protected function directoriesAreVirtual() {
01086                 return true;
01087         }
01088 
01097         protected function headersFromParams( array $params ) {
01098                 $hdrs = array();
01099                 if ( !empty( $params['latest'] ) ) {
01100                         $hdrs[] = 'X-Newest: true';
01101                 }
01102                 return $hdrs;
01103         }
01104 
01109         protected function doExecuteOpHandlesInternal( array $fileOpHandles ) {
01110                 $statuses = array();
01111 
01112                 $cfOps = array(); // list of CF_Async_Op objects
01113                 foreach ( $fileOpHandles as $index => $fileOpHandle ) {
01114                         $cfOps[$index] = $fileOpHandle->cfOp;
01115                 }
01116                 $batch = new CF_Async_Op_Batch( $cfOps );
01117 
01118                 $cfOps = $batch->execute();
01119                 foreach ( $cfOps as $index => $cfOp ) {
01120                         $status = Status::newGood();
01121                         try { // catch exceptions; update status
01122                                 $function = '_getResponse' . $fileOpHandles[$index]->call;
01123                                 $this->$function( $cfOp, $status, $fileOpHandles[$index]->params );
01124                                 $this->purgeCDNCache( $fileOpHandles[$index]->affectedObjects );
01125                         } catch ( CloudFilesException $e ) { // some other exception?
01126                                 $this->handleException( $e, $status,
01127                                         __CLASS__ . ":$function", $fileOpHandles[$index]->params );
01128                         }
01129                         $statuses[$index] = $status;
01130                 }
01131 
01132                 return $statuses;
01133         }
01134 
01161         protected function setContainerAccess(
01162                 CF_Container $contObj, array $readGrps, array $writeGrps
01163         ) {
01164                 $creds = $contObj->cfs_auth->export_credentials();
01165 
01166                 $url = $creds['storage_url'] . '/' . rawurlencode( $contObj->name );
01167 
01168                 // Note: 10 second timeout consistent with php-cloudfiles
01169                 $req = MWHttpRequest::factory( $url, array( 'method' => 'POST', 'timeout' => 10 ) );
01170                 $req->setHeader( 'X-Auth-Token', $creds['auth_token'] );
01171                 $req->setHeader( 'X-Container-Read', implode( ',', $readGrps ) );
01172                 $req->setHeader( 'X-Container-Write', implode( ',', $writeGrps ) );
01173 
01174                 return $req->execute(); // should return 204
01175         }
01176 
01184         public function purgeCDNCache( array $objects ) {
01185                 if ( $this->swiftUseCDN && $this->swiftCDNPurgable ) {
01186                         foreach ( $objects as $object ) {
01187                                 try {
01188                                         $object->purge_from_cdn();
01189                                 } catch ( CDNNotEnabledException $e ) {
01190                                         // CDN not enabled; nothing to see here
01191                                 } catch ( CloudFilesException $e ) {
01192                                         $this->handleException( $e, null, __METHOD__,
01193                                                 array( 'cont' => $object->container->name, 'obj' => $object->name ) );
01194                                 }
01195                         }
01196                 }
01197         }
01198 
01205         protected function getConnection() {
01206                 if ( $this->connException instanceof CloudFilesException ) {
01207                         if ( ( time() - $this->connErrorTime ) < 60 ) {
01208                                 throw $this->connException; // failed last attempt; don't bother
01209                         } else { // actually retry this time
01210                                 $this->connException = null;
01211                                 $this->connErrorTime = 0;
01212                         }
01213                 }
01214                 // Session keys expire after a while, so we renew them periodically
01215                 $reAuth = ( ( time() - $this->sessionStarted ) > $this->authTTL );
01216                 // Authenticate with proxy and get a session key...
01217                 if ( !$this->conn || $reAuth ) {
01218                         $this->sessionStarted = 0;
01219                         $this->connContainerCache->clear();
01220                         $cacheKey = $this->getCredsCacheKey( $this->auth->username );
01221                         $creds = $this->srvCache->get( $cacheKey ); // credentials
01222                         if ( is_array( $creds ) ) { // cache hit
01223                                 $this->auth->load_cached_credentials(
01224                                         $creds['auth_token'], $creds['storage_url'], $creds['cdnm_url'] );
01225                                 $this->sessionStarted = time() - ceil( $this->authTTL/2 ); // skew for worst case
01226                         } else { // cache miss
01227                                 try {
01228                                         $this->auth->authenticate();
01229                                         $creds = $this->auth->export_credentials();
01230                                         $this->srvCache->add( $cacheKey, $creds, ceil( $this->authTTL/2 ) ); // cache
01231                                         $this->sessionStarted = time();
01232                                 } catch ( CloudFilesException $e ) {
01233                                         $this->connException = $e; // don't keep re-trying
01234                                         $this->connErrorTime = time();
01235                                         throw $e; // throw it back
01236                                 }
01237                         }
01238                         if ( $this->conn ) { // re-authorizing?
01239                                 $this->conn->close(); // close active cURL handles in CF_Http object
01240                         }
01241                         $this->conn = new CF_Connection( $this->auth );
01242                 }
01243                 return $this->conn;
01244         }
01245 
01251         protected function closeConnection() {
01252                 if ( $this->conn ) {
01253                         $this->conn->close(); // close active cURL handles in CF_Http object
01254                         $this->sessionStarted = 0;
01255                         $this->connContainerCache->clear();
01256                 }
01257         }
01258 
01265         private function getCredsCacheKey( $username ) {
01266                 return wfMemcKey( 'backend', $this->getName(), 'usercreds', $username );
01267         }
01268 
01272         protected function doClearCache( array $paths = null ) {
01273                 $this->connContainerCache->clear(); // clear container object cache
01274         }
01275 
01285         protected function getContainer( $container, $bypassCache = false ) {
01286                 $conn = $this->getConnection(); // Swift proxy connection
01287                 if ( $bypassCache ) { // purge cache
01288                         $this->connContainerCache->clear( $container );
01289                 } elseif ( !$this->connContainerCache->has( $container, 'obj' ) ) {
01290                         $this->primeContainerCache( array( $container ) ); // check persistent cache
01291                 }
01292                 if ( !$this->connContainerCache->has( $container, 'obj' ) ) {
01293                         $contObj = $conn->get_container( $container );
01294                         // NoSuchContainerException not thrown: container must exist
01295                         $this->connContainerCache->set( $container, 'obj', $contObj ); // cache it
01296                         if ( !$bypassCache ) {
01297                                 $this->setContainerCache( $container, // update persistent cache
01298                                         array( 'bytes' => $contObj->bytes_used, 'count' => $contObj->object_count )
01299                                 );
01300                         }
01301                 }
01302                 return $this->connContainerCache->get( $container, 'obj' );
01303         }
01304 
01312         protected function createContainer( $container ) {
01313                 $conn = $this->getConnection(); // Swift proxy connection
01314                 $contObj = $conn->create_container( $container );
01315                 $this->connContainerCache->set( $container, 'obj', $contObj ); // cache
01316                 return $contObj;
01317         }
01318 
01326         protected function deleteContainer( $container ) {
01327                 $conn = $this->getConnection(); // Swift proxy connection
01328                 $this->connContainerCache->clear( $container ); // purge
01329                 $conn->delete_container( $container );
01330         }
01331 
01336         protected function doPrimeContainerCache( array $containerInfo ) {
01337                 try {
01338                         $conn = $this->getConnection(); // Swift proxy connection
01339                         foreach ( $containerInfo as $container => $info ) {
01340                                 $contObj = new CF_Container( $conn->cfs_auth, $conn->cfs_http,
01341                                         $container, $info['count'], $info['bytes'] );
01342                                 $this->connContainerCache->set( $container, 'obj', $contObj );
01343                         }
01344                 } catch ( CloudFilesException $e ) { // some other exception?
01345                         $this->handleException( $e, null, __METHOD__, array() );
01346                 }
01347         }
01348 
01359         protected function handleException( Exception $e, $status, $func, array $params ) {
01360                 if ( $status instanceof Status ) {
01361                         if ( $e instanceof AuthenticationException ) {
01362                                 $status->fatal( 'backend-fail-connect', $this->name );
01363                         } else {
01364                                 $status->fatal( 'backend-fail-internal', $this->name );
01365                         }
01366                 }
01367                 if ( $e->getMessage() ) {
01368                         trigger_error( "$func: " . $e->getMessage(), E_USER_WARNING );
01369                 }
01370                 if ( $e instanceof InvalidResponseException ) { // possibly a stale token
01371                         $this->srvCache->delete( $this->getCredsCacheKey( $this->auth->username ) );
01372                         $this->closeConnection(); // force a re-connect and re-auth next time
01373                 }
01374                 wfDebugLog( 'SwiftBackend',
01375                         get_class( $e ) . " in '{$func}' (given '" . FormatJson::encode( $params ) . "')" .
01376                         ( $e->getMessage() ? ": {$e->getMessage()}" : "" )
01377                 );
01378         }
01379 }
01380 
01384 class SwiftFileOpHandle extends FileBackendStoreOpHandle {
01386         public $cfOp;
01388         public $affectedObjects = array();
01389 
01390         public function __construct( $backend, array $params, $call, CF_Async_Op $cfOp ) {
01391                 $this->backend = $backend;
01392                 $this->params = $params;
01393                 $this->call = $call;
01394                 $this->cfOp = $cfOp;
01395         }
01396 }
01397 
01405 abstract class SwiftFileBackendList implements Iterator {
01407         protected $bufferIter = array();
01408         protected $bufferAfter = null; // string; list items *after* this path
01409         protected $pos = 0; // integer
01411         protected $params = array();
01412 
01414         protected $backend;
01415         protected $container; // string; container name
01416         protected $dir; // string; storage directory
01417         protected $suffixStart; // integer
01418 
01419         const PAGE_SIZE = 9000; // file listing buffer size
01420 
01427         public function __construct( SwiftFileBackend $backend, $fullCont, $dir, array $params ) {
01428                 $this->backend = $backend;
01429                 $this->container = $fullCont;
01430                 $this->dir = $dir;
01431                 if ( substr( $this->dir, -1 ) === '/' ) {
01432                         $this->dir = substr( $this->dir, 0, -1 ); // remove trailing slash
01433                 }
01434                 if ( $this->dir == '' ) { // whole container
01435                         $this->suffixStart = 0;
01436                 } else { // dir within container
01437                         $this->suffixStart = strlen( $this->dir ) + 1; // size of "path/to/dir/"
01438                 }
01439                 $this->params = $params;
01440         }
01441 
01446         public function key() {
01447                 return $this->pos;
01448         }
01449 
01454         public function next() {
01455                 // Advance to the next file in the page
01456                 next( $this->bufferIter );
01457                 ++$this->pos;
01458                 // Check if there are no files left in this page and
01459                 // advance to the next page if this page was not empty.
01460                 if ( !$this->valid() && count( $this->bufferIter ) ) {
01461                         $this->bufferIter = $this->pageFromList(
01462                                 $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
01463                         ); // updates $this->bufferAfter
01464                 }
01465         }
01466 
01471         public function rewind() {
01472                 $this->pos = 0;
01473                 $this->bufferAfter = null;
01474                 $this->bufferIter = $this->pageFromList(
01475                         $this->container, $this->dir, $this->bufferAfter, self::PAGE_SIZE, $this->params
01476                 ); // updates $this->bufferAfter
01477         }
01478 
01483         public function valid() {
01484                 if ( $this->bufferIter === null ) {
01485                         return false; // some failure?
01486                 } else {
01487                         return ( current( $this->bufferIter ) !== false ); // no paths can have this value
01488                 }
01489         }
01490 
01501         abstract protected function pageFromList( $container, $dir, &$after, $limit, array $params );
01502 }
01503 
01507 class SwiftFileBackendDirList extends SwiftFileBackendList {
01512         public function current() {
01513                 return substr( current( $this->bufferIter ), $this->suffixStart, -1 );
01514         }
01515 
01520         protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
01521                 return $this->backend->getDirListPageInternal( $container, $dir, $after, $limit, $params );
01522         }
01523 }
01524 
01528 class SwiftFileBackendFileList extends SwiftFileBackendList {
01533         public function current() {
01534                 return substr( current( $this->bufferIter ), $this->suffixStart );
01535         }
01536 
01541         protected function pageFromList( $container, $dir, &$after, $limit, array $params ) {
01542                 return $this->backend->getFileListPageInternal( $container, $dir, $after, $limit, $params );
01543         }
01544 }