MediaWiki
REL1_20
|
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 }